PySide6 + QML - QSerialPort01 - 扫描操作系统上有效的串口
导言
在上位机开发中,串口通信模块几乎是所有调试类 GUI 程序的核心组成部分。它负责与下位机(MCU、驱动板或传感器)建立通信通道,实现数据上传、命令下发和参数同步等功能。
如果把上位机比作一个「大脑」,那么串口就是「神经通路」——没有它,图形界面上的所有按钮、参数和状态显示都失去了实际意义。
在 PySide6 这类跨平台 GUI 框架中,合理设计串口通信结构(如事件驱动的接收机制、多线程发送队列、实时刷新 UI)不仅能提升交互体验,还能让整个程序运行更稳定、更高效。
因此,构建一个稳定可靠的串口模块,往往是 GUI 应用开发中最关键的基础工作之一。
效果如下:

工程代码:
- github: https://github.com/q164129345/myPyside6_QML/tree/main/serial01_basic_scan
- gitee: https://gitee.com/wallace89/myPyside6_QML/tree/main/serial01_basic_scan
一、使用命令行方式扫描操作系统上有效的串口
PS D:\Coding\myPyside6_QML> python3
Python 3.10.11 (tags/v3.10.11:7d4cc5a, Apr 5 2023, 00:38:17) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
Ctrl click to launch VS Code Native REPL
>>> from PySide6.QtSerialPort import QSerialPortInfo
>>> QSerialPortInfo.availablePorts()
[<PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103980>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD1039C0>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103A00>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103A40>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103A80>]
>>> ports = QSerialPortInfo.availablePorts()
>>> ports
[<PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103B00>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103BC0>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103C00>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103C40>, <PySide6.QtSerialPort.QSerialPortInfo object at 0x0000024DAD103C80>]
>>> type(ports)
<class 'list'>
如上所示:
- 导入QSerialPortInfo后,在命令行输入QSerialPortInfo.availablePorts()就可以获取操作系统上有效的串口;
- 变量ports属于list变量,一共有5个元素:
- 0x0000024DAD103B00
- 0x0000024DAD103BC0
- 0x0000024DAD103C00
- 0x0000024DAD103C40
- 0x0000024DAD103C80

如上图所示,QSerialPortInfo类提供一些方法让我们获取串口的信息。
>>> ports = QSerialPortInfo.availablePorts()
>>> for port in ports:
... print(f"串口: {port.portName()}")
...
串口: COM13
串口: COM7
串口: COM6
串口: CNCA0
串口: CNCB0
使用for ... in ...:遍历各个串口的portName。
>>> ports = QSerialPortInfo.availablePorts()
>>> for port in ports:
... print(f"串口:{port.portName()}, 设备描述:{port.description()}")
...
串口:COM13, 设备描述:USB Serial Port
串口:COM7, 设备描述:com0com - serial port emulator
串口:COM6, 设备描述:com0com - serial port emulator
串口:CNCA0, 设备描述:com0com - serial port emulator
串口:CNCB0, 设备描述:com0com - serial port emulator
可以将串口的portName()与description()一起打印出来。
二、main.py
# python3.10.11 - PySide6==6.9
"""
serial01_basic_scan - 串口扫描与基本信息
核心概念:
1. QSerialPortInfo - 获取系统可用串口信息
2. 扫描并显示串口详细信息(端口名、描述)
3. 信号机制更新QML界面
"""
import sysfrom PySide6.QtCore import QObject, Signal, Slot
from PySide6.QtGui import QGuiApplication
from PySide6.QtQml import QQmlApplicationEngine
from PySide6.QtSerialPort import QSerialPortInfoclass SerialBackend(QObject):"""串口后端类 - 负责串口扫描和信息管理"""# 定义信号:当串口列表更新时发射portsListChanged = Signal(list) # 发送串口信息列表statusChanged = Signal(str) # 发送状态消息def __init__(self):super().__init__()self._ports_list = [] # 内部存储串口列表@Slot()def scanPorts(self):"""扫描系统中所有可用的串口"""print("[SerialBackend] 开始扫描串口...")# 获取所有可用串口信息available_ports = QSerialPortInfo.availablePorts()if not available_ports:print("[SerialBackend] 未检测到可用串口")self.statusChanged.emit("未检测到可用串口")self.portsListChanged.emit([])return# 清空之前的列表self._ports_list = []# 遍历每个串口,提取详细信息for port in available_ports:port_name = port.portName()# 只处理COM口(Windows系统)if not port_name.startswith("COM"):print(f"[SerialBackend] 跳过非COM口: {port_name}")continueport_info = {"portName": port_name, # 端口名称(如 COM1, COM3)"description": port.description(), # 设备描述}self._ports_list.append(port_info)# 打印到控制台(调试用)print(f"[SerialBackend] 发现COM口: {port_info['portName']} - {port_info['description']}")# 发射信号,通知QML更新界面self.statusChanged.emit(f"找到 {len(self._ports_list)} 个串口")self.portsListChanged.emit(self._ports_list)print(f"[SerialBackend] 扫描完成,共找到 {len(self._ports_list)} 个串口")if __name__ == "__main__":# 创建应用程序和引擎app = QGuiApplication(sys.argv)engine = QQmlApplicationEngine()# 创建串口后端对象backend = SerialBackend()# 注册到QML环境engine.rootContext().setContextProperty("backend", backend)# 加载QML文件engine.addImportPath(sys.path[0]) # 当前项目路径engine.loadFromModule("Example", "Main") # 模块(Example) + QML文件名(Main.qml)if not engine.rootObjects():sys.exit(-1)sys.exit(app.exec())
关键点知识
- QSerialPortInfo串口信息类
from PySide6.QtSerialPort import QSerialPortInfo# 核心API
available_ports = QSerialPortInfo.availablePorts() # 获取所有可用串口列表
- Python后端类设计模式
class SerialBackend(QObject):# 信号定义(用于通知QML界面)portsListChanged = Signal(list) # 发送列表数据statusChanged = Signal(str) # 发送状态消息@Slot() # QML可调用的槽函数def scanPorts(self):# 扫描逻辑available_ports = QSerialPortInfo.availablePorts()# 发射信号更新QMLself.portsListChanged.emit(self._ports_list)
- 继承 QObject 实现信号槽机制
- 使用 @Slot() 装饰器暴露方法给QML
- 使用 Signal() 定义信号推送数据到QML
- Python与QML连接
# main.py
backend = SerialBackend()
engine.rootContext().setContextProperty("backend", backend)
说明: 通过 setContextProperty 将 Python 对象注册到QML环境,QML中直接使用 backend 名称访问。
三、Main.qml
// 导入QtQuick模块,提供基本的QML元素
import QtQuick
// 导入QtQuick.Controls模块,提供UI控件
import QtQuick.Controls
// 导入QtQuick.Layouts模块,提供布局管理
import QtQuick.Layouts// 定义主窗口
Window {width: 700height: 500visible: truetitle: "serial01 - 串口扫描与基本信息"// 主布局容器ColumnLayout {anchors.fill: parentanchors.margins: 20spacing: 15// ===== 标题区域 =====Text {text: "串口扫描工具"font.pixelSize: 24font.bold: trueLayout.alignment: Qt.AlignHCenter}// ===== 操作按钮区域 =====RowLayout {Layout.alignment: Qt.AlignHCenterspacing: 15Button {text: "🔍 扫描串口"font.pixelSize: 14onClicked: {statusText.text = "正在扫描..."statusText.color = "#2196F3" // 蓝色backend.scanPorts()}}Button {text: "🗑️ 清空列表"font.pixelSize: 14onClicked: {portListModel.clear()statusText.text = "列表已清空"statusText.color = "#9E9E9E" // 灰色}}}// ===== 状态信息 =====Text {id: statusTexttext: "点击 '扫描串口' 开始"font.pixelSize: 14color: "#666666"Layout.alignment: Qt.AlignHCenter}// ===== 分隔线 =====Rectangle {Layout.fillWidth: trueheight: 1color: "#E0E0E0"}// ===== 串口列表标题 =====Text {text: "检测到的串口设备:"font.pixelSize: 16font.bold: true}// ===== 串口列表视图 =====ListView {id: portListViewLayout.fillWidth: trueLayout.fillHeight: trueclip: truespacing: 10// 列表模型model: ListModel {id: portListModel}// 列表项委托delegate: Rectangle {width: portListView.widthheight: 80border.color: "#BDBDBD"border.width: 1radius: 8RowLayout {anchors.fill: parentanchors.margins: 15spacing: 15// 图标Text {text: "📌"font.pixelSize: 20}// 串口信息ColumnLayout {Layout.fillWidth: truespacing: 5// 端口名称Text {text: model.portNamefont.pixelSize: 18font.bold: truecolor: "#1976D2"}// 描述信息Text {text: model.description || "无描述"font.pixelSize: 13color: "#757575"Layout.fillWidth: truewrapMode: Text.WordWrap}}// 可用标签Rectangle {width: 60height: 25color: "#4CAF50"radius: 12Text {anchors.centerIn: parenttext: "可用"color: "white"font.pixelSize: 12}}}}// 空状态提示Text {visible: portListModel.count === 0anchors.centerIn: parenttext: "暂无串口设备\n\n💡 请连接串口设备后点击 '扫描串口'"font.pixelSize: 14color: "#9E9E9E"horizontalAlignment: Text.AlignHCenter}// 滚动条ScrollBar.vertical: ScrollBar {policy: ScrollBar.AsNeeded}}}// ===== 信号连接 =====Connections {target: backend// 当串口列表更新时function onPortsListChanged(portsList) {// 清空现有列表portListModel.clear()// 添加新的串口信息for (var i = 0; i < portsList.length; i++) {portListModel.append(portsList[i])}}// 当状态更新时function onStatusChanged(status) {statusText.text = status// 根据状态设置颜色if (status.includes("找到")) {statusText.color = "#4CAF50" // 绿色 - 成功} else if (status.includes("未检测到")) {statusText.color = "#FF9800" // 橙色 - 警告}}}
}
关键点知识
- QML 中的 Connections 机制
Connections {target: backend // 连接到Python对象// 信号处理函数(on + 信号名首字母大写)function onPortsListChanged(portsList) {portListModel.clear()for (var i = 0; i < portsList.length; i++) {portListModel.append(portsList[i])}}function onStatusChanged(status) {statusText.text = status}
}
- target 指定监听对象
- 信号处理函数命名规则:on + 信号名(首字母大写)
- 自动接收 Python 发射的信号
- QML Button 调用 Python 方法
Button {text: "🔍扫描串口"onClicked: {backend.scanPorts() // 直接调用Python的@Slot方法}
}
- ListModel 和 ListView 数据绑定
ListModel {id: portListModel
}ListView {model: portListModeldelegate: Rectangle {// model.portName 访问列表项数据Text { text: model.portName }Text { text: model.description }}
}
动态更新列表:
portListModel.clear() // 清空
portListModel.append({...}) // 添加项
