第 1 天 · 串口扫描与开关
技术栈:.NET 8 + WinForms +
System.IO.Ports
目标:程序启动时自动扫描并列出所有可用 COM 口,点击按钮能打开 / 关闭串口
预计耗时:1 小时
运行结果
一、今日目标
做出一个能"发现串口、打开串口、关闭串口"的小程序,界面长这样:
┌─────────────────────────────────────────────────────┐ │ 串口号:[COM3 ▼] 波特率:[9600 ▼] [刷新] │ │ │ │ [打开串口] [关闭串口] │ │ │ │ 状态:已打开 COM3,9600 bps │ └─────────────────────────────────────────────────────┘学完今天,你要能回答三个问题:
SerialPort.GetPortNames()是干什么用的?- 串口那 5 个参数为什么必须和下位机一致?
- 为什么要在
FormClosing里Close+Dispose?
二、新建项目步骤
- 打开Visual Studio 2022
- 新建项目 → 搜索“Windows Forms App”→ 选择.NET 8.0→ 命名为
Day01_SerialPort - 安装 NuGet 包:右键项目 → 管理 NuGet 程序包 → 搜索
System.IO.Ports→ 安装 - 按下面的「控件清单」拖好 6 个控件
- 用本文档里的代码替换
Form1.cs和Form1.Designer.cs
注意:.NET 8 中
SerialPort不在框架里,必须装System.IO.Ports这个 NuGet 包。.NET Framework 4.8 是自带的,这是两者最大的区别。
三、控件清单
一共 6 个控件,照着拖:
| 控件类型 | Name | 作用 | 关键属性 |
|---|---|---|---|
| 下拉框 ComboBox | cmbPortName | 选串口号 | DropDownStyle = DropDownList |
| 下拉框 ComboBox | cmbBaudRate | 选波特率 | DropDownStyle = DropDownList,默认 9600 |
| 按钮 Button | btnRefresh | 重新扫描 COM 口 | Text = “刷新” |
| 按钮 Button | btnOpen | 打开串口 | Text = “打开串口” |
| 按钮 Button | btnClose | 关闭串口 | Text = “关闭串口”,初始禁用 |
| 标签 Label | lblStatus | 显示当前状态 | 放底部,显示状态文字 |
关于下拉框(ComboBox):WinForms 工具箱 → 所有 Windows 窗体 → 找到ComboBox。它让用户从固定选项里挑一个,防止手输出错。
四、重点答疑:下拉框设默认值
这是今天最容易踩的坑,单独讲清楚。
问题 1:在属性窗口填了Text = "9600",运行后没生效
原因:下拉框的样式设成了DropDownList。这种样式只能显示列表里已有的选项,不能直接显示你手输的文字。
DropDownStyle三种模式的区别:
| 模式 | Text 属性填值的效果 |
|---|---|
DropDown | 能显示,但不会自动加进列表 |
DropDownList | 填了也不生效,只允许选列表里已有的项 |
Simple | 能显示,但变成列表框,不好看 |
第 1 天用的是DropDownList(防止手输乱码),所以填Text没用。
问题 2:在属性窗口找不到SelectedIndex
正常,VS 设计器里有时不显示这个属性,别找了。默认值用代码设最稳。
✅ 正确做法(记这一种)
先在代码里往列表加选项,再加SelectedIndex = 0:
// 在构造函数里,InitializeComponent() 之后写cmbBaudRate.Items.AddRange(newobject[]{"9600","19200","38400","57600","115200"});cmbBaudRate.SelectedIndex=0;// 默认显示 9600// COM 口是程序启动时扫描的RefreshComPorts();if(cmbPortName.Items.Count>0)cmbPortName.SelectedIndex=0;顺序不能反:先Items.Add加选项,再SelectedIndex = 0设默认。反过来会报错或空白。
设计器里"能做的"只有这些
- 选中
cmbBaudRate - 属性窗口点Items→
...→ 一行一个填9600/19200/38400/57600/115200 DropDownStyle设DropDownList- SelectedIndex 不用管,代码里写
SelectedIndex = 0
五、完整代码
5.1 Form1.cs
usingSystem;usingSystem.IO.Ports;// .NET 8 中 SerialPort 在这个 NuGet 包里usingSystem.Windows.Forms;namespaceDay01_SerialPort{/// <summary>/// 第 1 天:自动扫描 COM 口 + 打开/关闭串口/// 目标:程序启动时列出所有可用串口,点击按钮能打开/关闭/// </summary>publicpartialclassForm1:Form{// ============ 字段 ============/// <summary>/// SerialPort 对象,整个窗体共用这一个实例/// 用可空类型(SerialPort?)表示它初始可以为 null/// </summary>privateSerialPort?_serialPort;// ============ 构造函数 ============publicForm1(){// 这一行是 VS 设计器自动生成的,初始化所有拖上去的控件InitializeComponent();// ---- 手动新增的初始化代码 ----// 程序启动时自动扫描并填充 COM 口列表RefreshComPorts();// 设置窗口标题this.Text="第 1 天 - 串口扫描与开关";// 初始状态下"关闭串口"按钮不可用(还没打开呢)btnClose.Enabled=false;}// ============ 方法 ============/// <summary>/// 扫描系统中所有可用的串口,填充到 ComboBox 下拉框/// </summary>privatevoidRefreshComPorts(){// SerialPort.GetPortNames() 返回 string[],如 { "COM1", "COM3", "COM4" }// 每次刷新前先清空,防止重复添加cmbPortName.Items.Clear();string[]ports=SerialPort.GetPortNames();if(ports.Length==0){// 没找到任何串口,给用户一个提示cmbPortName.Items.Add("(未检测到串口)");cmbPortName.SelectedIndex=0;lblStatus.Text="状态:未检测到任何串口,请检查设备管理器";btnOpen.Enabled=false;// 没串口就不能点"打开"}else{// 把所有 COM 口号加进下拉框cmbPortName.Items.AddRange(ports);// 默认选中第一个cmbPortName.SelectedIndex=0;lblStatus.Text=$"状态:检测到{ports.Length}个串口";btnOpen.Enabled=true;}}// ============ 事件:点击"刷新"按钮 ============privatevoidbtnRefresh_Click(objectsender,EventArgse){// 重新扫描 COM 口RefreshComPorts();}// ============ 事件:点击"打开串口"按钮 ============privatevoidbtnOpen_Click(objectsender,EventArgse){// ---- 1. 获取用户选择的串口号 ----stringportName=cmbPortName.SelectedItem?.ToString()??"";if(string.IsNullOrEmpty(portName)||portName.Contains("未检测")){MessageBox.Show("请先选择一个有效的串口号!","提示",MessageBoxButtons.OK,MessageBoxIcon.Warning);return;}// ---- 2. 从下拉框读取波特率等参数 ----// cmbBaudRate 里预设了 9600/19200/38400/57600/115200intbaudRate=int.Parse(cmbBaudRate.SelectedItem?.ToString()??"9600");// ---- 3. 创建 SerialPort 对象 ----// 五个参数必须和下位机(单片机/模块)完全一致,否则通信失败_serialPort=newSerialPort(portName,// 串口号,如 "COM3"baudRate,// 波特率,如 9600Parity.None,// 校验位:None/Odd/Even8,// 数据位:通常 8StopBits.One// 停止位:通常 One);// ---- 4. 打开串口 ----try{_serialPort.Open();// 打开成功后更新 UI 状态lblStatus.Text=$"状态:已打开{portName},{baudRate}bps";btnOpen.Enabled=false;// 打开后不能再点"打开"btnClose.Enabled=true;// 允许关闭cmbPortName.Enabled=false;// 打开期间不允许切换串口号cmbBaudRate.Enabled=false;btnRefresh.Enabled=false;// 标题栏也显示一下this.Text=$"第 1 天 -{portName}已打开";}catch(UnauthorizedAccessException){// 串口被别的程序占用了(比如串口调试助手没关)MessageBox.Show($"无法打开{portName},该端口可能被其他程序占用。\n"+"请关闭其他串口工具后重试。","打开失败",MessageBoxButtons.OK,MessageBoxIcon.Error);}catch(Exceptionex){// 其他异常(端口不存在、驱动问题等)MessageBox.Show($"打开串口时发生错误:\n{ex.Message}","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);}}// ============ 事件:点击"关闭串口"按钮 ============privatevoidbtnClose_Click(objectsender,EventArgse){// ---- 安全检查:确认对象存在且确实打开了 ----if(_serialPort!=null&&_serialPort.IsOpen){try{// 关闭串口_serialPort.Close();// 释放资源(好习惯,防止下次打开时报错)_serialPort.Dispose();_serialPort=null;// 更新 UI 状态lblStatus.Text="状态:串口已关闭";btnOpen.Enabled=true;btnClose.Enabled=false;cmbPortName.Enabled=true;cmbBaudRate.Enabled=true;btnRefresh.Enabled=true;this.Text="第 1 天 - 串口扫描与开关";}catch(Exceptionex){MessageBox.Show($"关闭串口时发生错误:\n{ex.Message}","错误",MessageBoxButtons.OK,MessageBoxIcon.Error);}}}// ============ 事件:窗体关闭时确保释放串口 ============privatevoidForm1_FormClosing(objectsender,FormClosingEventArgse){// 用户点右上角 × 时,如果串口还开着,自动关掉// 防止下次启动程序时报"端口被占用"if(_serialPort!=null&&_serialPort.IsOpen){_serialPort.Close();_serialPort.Dispose();_serialPort=null;}}}}5.2 Form1.Designer.cs
namespaceDay01_SerialPort{partialclassForm1{/// <summary> 必需的设计器变量 </summary>privateSystem.ComponentModel.IContainercomponents=null;/// <summary> 清理资源 </summary>protectedoverridevoidDispose(booldisposing){if(disposing&&(components!=null)){components.Dispose();}base.Dispose(disposing);}#regionWindows 窗体设计器生成的代码privatevoidInitializeComponent(){// ---- 串口号标签 ----LabellblPort=newLabel();lblPort.AutoSize=true;lblPort.Location=newSystem.Drawing.Point(20,25);lblPort.Text="串口号:";// ---- 串口号下拉框 ----cmbPortName=newComboBox();cmbPortName.DropDownStyle=ComboBoxStyle.DropDownList;cmbPortName.Location=newSystem.Drawing.Point(80,22);cmbPortName.Size=newSystem.Drawing.Size(120,25);// ---- 波特率标签 ----LabellblBaud=newLabel();lblBaud.AutoSize=true;lblBaud.Location=newSystem.Drawing.Point(220,25);lblBaud.Text="波特率:";// ---- 波特率下拉框 ----cmbBaudRate=newComboBox();cmbBaudRate.DropDownStyle=ComboBoxStyle.DropDownList;cmbBaudRate.Location=newSystem.Drawing.Point(280,22);cmbBaudRate.Size=newSystem.Drawing.Size(100,25);cmbBaudRate.Items.AddRange(newobject[]{"9600","19200","38400","57600","115200"});cmbBaudRate.SelectedIndex=0;// 默认 9600// ---- 刷新按钮 ----btnRefresh=newButton();btnRefresh.Location=newSystem.Drawing.Point(400,20);btnRefresh.Size=newSystem.Drawing.Size(80,28);btnRefresh.Text="刷新";btnRefresh.Click+=newEventHandler(btnRefresh_Click);// ---- 打开串口按钮 ----btnOpen=newButton();btnOpen.Location=newSystem.Drawing.Point(80,70);btnOpen.Size=newSystem.Drawing.Size(120,35);btnOpen.Text="打开串口";btnOpen.Click+=newEventHandler(btnOpen_Click);// ---- 关闭串口按钮 ----btnClose=newButton();btnClose.Location=newSystem.Drawing.Point(220,70);btnClose.Size=newSystem.Drawing.Size(120,35);btnClose.Text="关闭串口";btnClose.Click+=newEventHandler(btnClose_Click);// ---- 状态标签 ----lblStatus=newLabel();lblStatus.AutoSize=true;lblStatus.Location=newSystem.Drawing.Point(20,130);lblStatus.Size=newSystem.Drawing.Size(400,20);lblStatus.Text="状态:等待操作...";// ---- 窗体本身 ----this.AutoScaleDimensions=newSystem.Drawing.SizeF(7F,17F);this.AutoScaleMode=AutoScaleMode.Font;this.ClientSize=newSystem.Drawing.Size(520,180);this.Controls.Add(lblPort);this.Controls.Add(cmbPortName);this.Controls.Add(lblBaud);this.Controls.Add(cmbBaudRate);this.Controls.Add(btnRefresh);this.Controls.Add(btnOpen);this.Controls.Add(btnClose);this.Controls.Add(lblStatus);this.Name="Form1";this.StartPosition=FormStartPosition.CenterScreen;this.Text="第 1 天 - 串口扫描与开关";this.FormClosing+=newFormClosingEventHandler(Form1_FormClosing);this.ResumeLayout(false);this.PerformLayout();}#endregion// ---- 控件声明(Designer 自动管理) ----privateComboBoxcmbPortName=new();privateComboBoxcmbBaudRate=new();privateButtonbtnRefresh=new();privateButtonbtnOpen=new();privateButtonbtnClose=new();privateLabellblStatus=new();}}5.3 Program.cs(.NET 8 入口,新建项目时自动生成,不用改)
namespaceDay01_SerialPort{internalstaticclassProgram{/// <summary>/// The main entry point for the application./// </summary>[STAThread]staticvoidMain(){ApplicationConfiguration.Initialize();Application.Run(newForm1());}}}5.4 Day01_SerialPort.csproj(项目文件)
<ProjectSdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>WinExe</OutputType><TargetFramework>net8.0-windows</TargetFramework><UseWindowsForms>true</UseWindowsForms><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup><ItemGroup><PackageReferenceInclude="System.IO.Ports"Version="8.0.0"/></ItemGroup></Project>六、今天重点理解 3 件事
| 要点 | 为什么重要 |
|---|---|
SerialPort.GetPortNames() | 自动扫描,不用用户手打 COM 口号 |
| 5 个参数必须和下位机一致 | 波特率 / 校验 / 数据位 / 停止位,有一个不对就收不到数据 |
FormClosing里Close+Dispose | 防止程序异常退出后串口被锁,下次打不开 |
七、常见问题排查
| 现象 | 原因 | 解决 |
|---|---|---|
填了Text = "9600"没生效 | DropDownList模式只能显示列表里已有的项 | 用代码Items.Add+SelectedIndex = 0 |
找不到SelectedIndex属性 | VS 设计器有时不显示 | 不用找,代码里写 |
报ArgumentOutOfRange | Items还是空的就设了SelectedIndex = 0 | 先加选项,再设默认 |
| 打开时报"端口被占用" | 别的程序(如串口调试助手)没关 | 关掉其他串口工具 |
| 下次启动打不开串口 | 上次没Close/Dispose | 在FormClosing里释放 |
找不到SerialPort类 | .NET 8 需要装 NuGet 包 | 装System.IO.Ports |
八、明日预告
第 2 天:发送字符串 + 十六进制数据
- 加发送文本框 + 发送按钮
- 支持 ASCII 和 HEX 两种发送模式
- 用 VSPD 虚拟串口对(COM3↔COM4),用另一个串口工具连 COM4 接收验证