☰
第01天_串口扫描与开关_教程
2026/10/27 10:20:48 网站建设 项目流程

第 1 天 · 串口扫描与开关

技术栈:.NET 8 + WinForms +System.IO.Ports
目标:程序启动时自动扫描并列出所有可用 COM 口,点击按钮能打开 / 关闭串口
预计耗时:1 小时

运行结果

一、今日目标

做出一个能"发现串口、打开串口、关闭串口"的小程序,界面长这样:

┌─────────────────────────────────────────────────────┐ │ 串口号:[COM3 ▼] 波特率:[9600 ▼] [刷新] │ │ │ │ [打开串口] [关闭串口] │ │ │ │ 状态:已打开 COM3,9600 bps │ └─────────────────────────────────────────────────────┘

学完今天,你要能回答三个问题:

  1. SerialPort.GetPortNames()是干什么用的?
  2. 串口那 5 个参数为什么必须和下位机一致?
  3. 为什么要在FormClosing里Close+Dispose?

二、新建项目步骤

  1. 打开Visual Studio 2022
  2. 新建项目 → 搜索“Windows Forms App”→ 选择.NET 8.0→ 命名为Day01_SerialPort
  3. 安装 NuGet 包:右键项目 → 管理 NuGet 程序包 → 搜索System.IO.Ports→ 安装
  4. 按下面的「控件清单」拖好 6 个控件
  5. 用本文档里的代码替换Form1.cs和Form1.Designer.cs

注意:.NET 8 中SerialPort不在框架里,必须装System.IO.Ports这个 NuGet 包。.NET Framework 4.8 是自带的,这是两者最大的区别。


三、控件清单

一共 6 个控件,照着拖:

控件类型Name作用关键属性
下拉框 ComboBoxcmbPortName选串口号DropDownStyle = DropDownList
下拉框 ComboBoxcmbBaudRate选波特率DropDownStyle = DropDownList,默认 9600
按钮 ButtonbtnRefresh重新扫描 COM 口Text = “刷新”
按钮 ButtonbtnOpen打开串口Text = “打开串口”
按钮 ButtonbtnClose关闭串口Text = “关闭串口”,初始禁用
标签 LabellblStatus显示当前状态放底部,显示状态文字

关于下拉框(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设默认。反过来会报错或空白。

设计器里"能做的"只有这些

  1. 选中cmbBaudRate
  2. 属性窗口点Items→...→ 一行一个填9600/19200/38400/57600/115200
  3. DropDownStyle设DropDownList
  4. 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 设计器有时不显示不用找,代码里写
报ArgumentOutOfRangeItems还是空的就设了SelectedIndex = 0先加选项,再设默认
打开时报"端口被占用"别的程序(如串口调试助手)没关关掉其他串口工具
下次启动打不开串口上次没Close/Dispose在FormClosing里释放
找不到SerialPort类.NET 8 需要装 NuGet 包装System.IO.Ports

八、明日预告

第 2 天:发送字符串 + 十六进制数据

  • 加发送文本框 + 发送按钮
  • 支持 ASCII 和 HEX 两种发送模式
  • 用 VSPD 虚拟串口对(COM3↔COM4),用另一个串口工具连 COM4 接收验证

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询