Qt 串口编程
串口通信:串口按位(bit)发送和接收字节。
Modbus 是一种串行通信协议,是工业领域通信协议的业界标准,也是工业电子设备之间常用的连接方式。
目录
文章目录
- [Qt 串口编程](#Qt 串口编程)
-
- 目录
- 什么是串口
- 串口的使用场景
- 串口通讯的参数
- 如何查看串口
- 虚拟串口软件使用
- [serialport 类简介](#serialport 类简介)
-
- [QSerialPort 常用 API](#QSerialPort 常用 API)
- [QSerialPort 枚举类型](#QSerialPort 枚举类型)
- [在项目中使用 serialport 模块](#在项目中使用 serialport 模块)
- [初始化串口参数 UI](#初始化串口参数 UI)
- [界面样式(qss.txt 内容)](#界面样式(qss.txt 内容))
- 打开与关闭串口
- 串口读取数据
- 串口定时发送数据
- 串口接收大量数据导致页面卡顿
- 串口使用多线程
什么是串口
串口(Serial Port),也称串行通讯接口,通常指 COM 口。串口通信是指采用串行通信协议,在一条信号线上将数据一个比特一个比特地逐位进行传输的通信模式。其特点是通信线路简单,但传送速度较慢。
串行接口按电气标准及协议来分,主要包括:
- RS-232:标准串口,最常用的串行通讯接口,最大传送距离约 15 米,最高速率 20 kb/s。
- RS-422:最大传输距离 1219 米,最大传输速率 10 Mb/s。
- RS-485:在 RS-422 基础上发展而来,与 RS-422 相仿,最大传输距离 1219 米,最大传输速率 10 Mb/s。
串口的使用场景

串口通讯的参数
| 参数 | 含义 | 示例 |
|---|---|---|
| 端口 | 标识是哪个串口 | COM1、COM2 |
| 波特率 | 传输速率的参数,表示每秒钟传送符号的个数 | 4800、9600、14400、28800、115200 |
| 数据位 | 计算机发送一个信息包的实际位数。标准 ASCII 码是 0~127(7 位),扩展 ASCII 码是 0~255(8 位) | 5、6、7、8 |
| 停止位 | 表示单个包的最后一位,表示传输的结束,并提供计算机校正时钟同步的机会 | 1、1.5、2 |
| 奇偶校验 | 串口通信中一种简单的检错方式 | 无、偶、奇、1(Mark)、0(Space) |
| 流控制 | 当接收端处理不过来时,就发出"不再接收"的信号,发送端则停止发送,直到收到"可以继续发送"的信号再发送数据 | 无、硬件流控 RTS/CTS、软件流控 XON/XOFF |
如何查看串口


虚拟串口软件使用
Virtual Serial Port Driver(简称 VSPD)是一款虚拟串口工具,由著名软件公司 Eltima 开发。它支持快速调试代码、添加无限个虚拟串口、实时虚拟串口数据传输监控等多种功能,并且能够创建任何你想使用的端口号。
安装与配置步骤
- 软件主界面:

- 点击「下一步」:

- 接受协议:

- 设置安装地址:




- 安装完成后,会出现 14 天试用提示:


- 将
vspdctl.dll拷贝到已选择的安装路径,替换原先的vspdctl.dll:

- 再次打开软件,就不会再出现 14 天试用提示框了:

创建虚拟串口对
右键「我的电脑」→ 「属性」→「设备管理器」,打开 VSPD,点击 Add Pair,就会在设备管理器上出现两个串口(COM1 与 COM2)并相互连接:
COM1 -> COM2:从 COM1 发出去的数据会发送给 COM2;COM2 -> COM1:从 COM2 发出去的数据会发送给 COM1。


serialport 类简介
QSerialPort 是 Qt 框架中用于串行端口通信的核心类,自 Qt 5.1 起成为标准模块的一部分。它继承自 QIODevice,为访问硬件和虚拟串口提供了跨平台(Windows、Linux、macOS 等)的统一接口。
该类提供了完整的串口生命周期管理功能,包括设备的枚举、参数配置、打开与关闭、数据读写以及 RS-232 引脚信号的控制。QSerialPort 支持两种编程模式:
- 基于信号与槽的异步事件驱动模式(推荐用于 GUI 程序);
- 同步阻塞模式(仅适用于非 GUI 线程)。
串口默认以独占方式打开,确保同一时间只有一个进程或线程可以访问该端口。
QSerialPort 常用 API
生命周期与配置管理
| API | 说明 |
|---|---|
setPortName(const QString &name) / setPort(const QSerialPortInfo &info) |
设置要访问的串口设备,通常结合 QSerialPortInfo 使用 |
open(QIODevice::OpenMode mode) |
以只读、只写或读写模式打开串口 |
close() |
关闭串口并取消所有正在进行的 I/O 操作 |
isOpen() |
检查串口是否已成功打开 |
setBaudRate(qint32 baudRate) |
设置波特率(如 9600、115200) |
setDataBits(DataBits dataBits) |
设置数据位(5~8 位) |
setParity(Parity parity) |
设置奇偶校验位 |
setStopBits(StopBits stopBits) |
设置停止位(1、1.5 或 2 位) |
setFlowControl(FlowControl flowControl) |
设置流控制方式(无、硬件 RTS/CTS、软件 XON/XOFF) |
数据读写操作
| API | 说明 |
|---|---|
write(const QByteArray &data) |
向串口发送数据(非阻塞,数据写入内部缓冲区) |
readAll() / read(char *data, qint64 maxSize) |
从串口读取数据,通常配合 readyRead 信号使用 |
bytesAvailable() |
返回当前内部读缓冲区中可读取的字节数 |
bytesToWrite() |
返回尚未写入硬件的字节数,常用于高频发送时的流控 |
信号与控制
| API | 说明 |
|---|---|
readyRead |
当有新数据到达并可供读取时触发 |
bytesWritten(qint64 bytes) |
当数据成功写入硬件时触发 |
errorOccurred(SerialPortError error) |
当发生通信错误时触发 |
waitForReadyRead(int msecs) / waitForBytesWritten(int msecs) |
同步阻塞函数,挂起当前线程直到数据可读或写入完成(严禁在 GUI 主线程使用) |
QSerialPort 枚举类型
BaudRate(波特率)
定义数据传输速率,常用枚举值包括 Baud1200、Baud9600、Baud19200、Baud38400、Baud57600、Baud115200 等。此外,setBaudRate() 也接受 qint32 整数以支持非标波特率。
DataBits(数据位)
定义每个字符的数据位数:Data5、Data6、Data7、Data8(最常用)。
Parity(奇偶校验)
定义错误检测机制:NoParity(无校验,最常用)、EvenParity(偶校验)、OddParity(奇校验)、SpaceParity、MarkParity。
StopBits(停止位)
表示数据包的结束并提供时钟同步:OneStop(1 位,最常用)、OneAndHalfStop(1.5 位)、TwoStop(2 位)。
FlowControl(流控制)
防止接收端处理过慢导致数据丢失:NoFlowControl(无流控)、HardwareControl(硬件流控 RTS/CTS)、SoftwareControl(软件流控 XON/XOFF)。
Direction(传输方向)
用于指定参数生效的方向:Input(输入)、Output(输出)、AllDirections(双向,默认)。
PinoutSignal(引脚信号)
用于查询 RS-232 硬件握手引脚状态:NoSignal、DataTerminalReadySignal(DTR)、DataCarrierDetectSignal(DCD)、DataSetReadySignal(DSR)、RingIndicatorSignal(RI)、RequestToSendSignal(RTS)、ClearToSendSignal(CTS)等。
SerialPortError(错误状态)
表示串口操作发生的错误类型:NoError、DeviceNotFoundError(设备未找到)、PermissionError(权限不足)、OpenError、NotOpenError、ReadError、WriteError、ResourceError(资源错误,如设备被拔出)、UnknownError 等。
在项目中使用 serialport 模块
Qt5 提供了 serialport 模块,方便快速开发串口应用程序。QSerialPort 类是 Qt5 封装的串口类,实现了串口通信的功能。要使用该模块,需要:
- 在
.pro文件中加入:QT += serialport - 在
.h文件中加入:#include <QSerialPort>




初始化串口参数 UI
cpp
void MainWindow::initUI()
{
// 端口名称,预制 10 个
for (int i = 1; i <= 10; i++) {
ui->comboBox_portName->addItem(QString("COM%1").arg(i));
}
// 波特率
ui->comboBox_baudRate->addItem(QString("Baud1200"), QSerialPort::Baud1200);
ui->comboBox_baudRate->addItem(QString("Baud2400"), QSerialPort::Baud2400);
ui->comboBox_baudRate->addItem(QString("Baud4800"), QSerialPort::Baud4800);
ui->comboBox_baudRate->addItem(QString("Baud9600"), QSerialPort::Baud9600);
ui->comboBox_baudRate->addItem(QString("Baud19200"), QSerialPort::Baud19200);
ui->comboBox_baudRate->addItem(QString("Baud38400"), QSerialPort::Baud38400);
ui->comboBox_baudRate->addItem(QString("Baud57600"), QSerialPort::Baud57600);
ui->comboBox_baudRate->addItem(QString("Baud115200"), QSerialPort::Baud115200);
// 数据位
ui->comboBox_dataBit->addItem("8", QSerialPort::Data8);
ui->comboBox_dataBit->addItem("7", QSerialPort::Data7);
ui->comboBox_dataBit->addItem("6", QSerialPort::Data6);
ui->comboBox_dataBit->addItem("5", QSerialPort::Data5);
// 奇偶校验
ui->comboBox_parity->addItem("None", QSerialPort::NoParity);
ui->comboBox_parity->addItem("Even", QSerialPort::EvenParity);
ui->comboBox_parity->addItem("Odd", QSerialPort::OddParity);
ui->comboBox_parity->addItem("Mark", QSerialPort::MarkParity);
ui->comboBox_parity->addItem("Space", QSerialPort::SpaceParity);
// 停止位
ui->comboBox_stopBit->addItem("1", QSerialPort::OneStop);
ui->comboBox_stopBit->addItem("1.5", QSerialPort::OneAndHalfStop);
ui->comboBox_stopBit->addItem("2", QSerialPort::TwoStop);
// 流控制
ui->comboBox_flowControl->addItem("None", QSerialPort::NoFlowControl);
ui->comboBox_flowControl->addItem("RTS/CTS", QSerialPort::HardwareControl);
ui->comboBox_flowControl->addItem("XON/XOFF", QSerialPort::SoftwareControl);
}

界面样式(qss.txt 内容)
css
/* ============================================================
串口调试工具 - 科技风样式表
主题:深空暗色 + 霓虹青蓝 + 发光效果
============================================================ */
/* ---------- 全局 ---------- */
QMainWindow, QWidget#centralwidget, QWidget {
background-color: #0a0e17;
color: #c8d6e5;
font-family: "Microsoft YaHei", "Consolas", "Segoe UI";
font-size: 13px;
}
QMainWindow::separator {
background: #1b2432;
width: 1px;
height: 1px;
}
/* ---------- 菜单栏 ---------- */
QMenuBar {
background-color: #0d1320;
border-bottom: 1px solid #1e2a3a;
padding: 2px;
}
QMenuBar::item {
background: transparent;
padding: 4px 12px;
color: #9fb3c8;
}
QMenuBar::item:selected {
background: #16283c;
color: #00e5ff;
border-radius: 4px;
}
QMenu {
background-color: #0f1624;
border: 1px solid #1f2d42;
padding: 4px;
}
QMenu::item {
padding: 6px 24px;
border-radius: 4px;
}
QMenu::item:selected {
background: #13263d;
color: #00e5ff;
}
/* ---------- 状态栏 ---------- */
QStatusBar {
background-color: #0d1320;
border-top: 1px solid #1e2a3a;
color: #7f96ab;
}
/* ---------- 分组框 ---------- */
QGroupBox {
background-color: #0d1420;
border: 1px solid #1e2d42;
border-radius: 8px;
margin-top: 14px;
padding: 12px 8px 8px 8px;
color: #00e5ff;
font-weight: bold;
}
QGroupBox::title {
subcontrol-origin: margin;
subcontrol-position: top left;
left: 14px;
padding: 0 6px;
color: #00e5ff;
background-color: #0a0e17;
}
/* ---------- 标签 ---------- */
QLabel {
color: #9fb3c8;
background: transparent;
}
/* ---------- 下拉框 ---------- */
QComboBox {
background-color: #101828;
border: 1px solid #22344c;
border-radius: 6px;
padding: 4px 30px 4px 10px;
color: #d6e4f0;
min-height: 18px;
selection-background-color: #13263d;
}
QComboBox:hover {
border-color: #00b8d4;
}
QComboBox:focus {
border-color: #00e5ff;
}
QComboBox::drop-down {
subcontrol-origin: padding;
subcontrol-position: top right;
width: 24px;
border-left: 1px solid #22344c;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
background-color: #0e1522;
}
QComboBox::down-arrow {
width: 0;
height: 0;
border-left: 5px solid transparent;
border-right: 5px solid transparent;
border-top: 6px solid #00e5ff;
margin-right: 4px;
}
QComboBox QAbstractItemView {
background-color: #0f1624;
border: 1px solid #1f2d42;
border-radius: 4px;
color: #d6e4f0;
selection-background-color: #13263d;
selection-color: #00e5ff;
outline: none;
}
/* ---------- 文本框(接收/发送) ---------- */
QPlainTextEdit {
background-color: #0a1220;
border: 1px solid #00a0b8;
border-radius: 6px;
padding: 6px;
color: #00e6bc;
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
selection-background-color: #0f3b4d;
selection-color: #ffffff;
}
QPlainTextEdit:hover {
border-color: #00b8d4;
background-color: #0c1626;
}
QPlainTextEdit:focus {
border-color: #00e5ff;
background-color: #0e192c;
}
/* ---------- 单行输入框 ---------- */
QLineEdit {
background-color: #101828;
border: 1px solid #22344c;
border-radius: 6px;
padding: 4px 8px;
color: #d6e4f0;
selection-background-color: #13263d;
}
QLineEdit:hover {
border-color: #00b8d4;
}
QLineEdit:focus {
border-color: #00e5ff;
}
/* ---------- 按钮 ---------- */
QPushButton {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #0e2238, stop:1 #0b1828);
color: #00e5ff;
border: 1px solid #1f4a63;
border-radius: 6px;
padding: 7px 18px;
font-weight: bold;
min-height: 20px;
}
QPushButton:hover {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #123553, stop:1 #0e2a45);
border-color: #00e5ff;
color: #ffffff;
}
QPushButton:pressed {
background-color: #082033;
border-color: #00b8d4;
padding-top: 8px;
padding-bottom: 6px;
}
QPushButton:disabled {
background-color: #10151f;
color: #4a5a6a;
border-color: #1a2533;
}
/* 打开串口按钮 - 绿色高亮 */
QPushButton#pushButton_open {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #0a2e2a, stop:1 #08221f);
border-color: #1f6a52;
color: #00ffa3;
}
QPushButton#pushButton_open:hover {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #0e453d, stop:1 #0b362f);
border-color: #00ffa3;
color: #ffffff;
}
/* 发送按钮 - 蓝紫高亮 */
QPushButton#pushButton_send {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #16204a, stop:1 #101736);
border-color: #3a4f9e;
color: #7c9bff;
}
QPushButton#pushButton_send:hover {
background-color: qlineargradient(x1:0, y1:0, x2:0, y2:1,
stop:0 #1e2c66, stop:1 #17204c);
border-color: #7c9bff;
color: #ffffff;
}
/* ---------- 复选框 ---------- */
QCheckBox {
color: #9fb3c8;
spacing: 8px;
background: transparent;
}
QCheckBox::indicator {
width: 18px;
height: 18px;
border: 1px solid #22344c;
border-radius: 4px;
background-color: #101828;
}
QCheckBox::indicator:hover {
border-color: #00b8d4;
}
QCheckBox::indicator:checked {
background-color: #0e2a45;
border-color: #00e5ff;
image: none;
}
QCheckBox::indicator:checked {
background-color: #00e5ff;
border-color: #00e5ff;
}
/* ---------- 滚动条 ---------- */
QScrollBar:vertical {
background: #0a0e17;
width: 10px;
margin: 0;
border-radius: 5px;
}
QScrollBar::handle:vertical {
background: #22344c;
min-height: 30px;
border-radius: 5px;
}
QScrollBar::handle:vertical:hover {
background: #00b8d4;
}
QScrollBar::add-line:vertical,
QScrollBar::sub-line:vertical {
height: 0;
background: none;
}
QScrollBar::add-page:vertical,
QScrollBar::sub-page:vertical {
background: none;
}
QScrollBar:horizontal {
background: #0a0e17;
height: 10px;
margin: 0;
border-radius: 5px;
}
QScrollBar::handle:horizontal {
background: #22344c;
min-width: 30px;
border-radius: 5px;
}
QScrollBar::handle:horizontal:hover {
background: #00b8d4;
}
QScrollBar::add-line:horizontal,
QScrollBar::sub-line:horizontal {
width: 0;
background: none;
}
QScrollBar::add-page:horizontal,
QScrollBar::sub-page:horizontal {
background: none;
}
/* ---------- 工具提示 ---------- */
QToolTip {
background-color: #0f1624;
color: #d6e4f0;
border: 1px solid #00b8d4;
padding: 4px 8px;
border-radius: 4px;
}


打开与关闭串口
cpp
void MainWindow::on_pushButton_open_clicked()
{
QString text = ui->pushButton_open->text();
if (text == QStringLiteral("打开串口")) {
// 设置串口参数
m_serial.setPortName(ui->comboBox_portName->currentText());
m_serial.setBaudRate(ui->comboBox_baudRate->currentData().toInt());
m_serial.setParity((QSerialPort::Parity)ui->comboBox_parity->currentData().toInt());
m_serial.setDataBits((QSerialPort::DataBits)ui->comboBox_dataBit->currentData().toInt());
m_serial.setStopBits((QSerialPort::StopBits)ui->comboBox_stopBit->currentData().toInt());
m_serial.setFlowControl((QSerialPort::FlowControl)ui->comboBox_flowControl->currentData().toInt());
// 打开串口
bool ret = m_serial.open(QIODevice::ReadWrite);
if (ret) {
// 打开后禁止更改参数
ui->groupBox->setEnabled(false);
// 切换按钮文本
ui->pushButton_open->setText(QStringLiteral("关闭串口"));
} else {
// 打开失败,状态栏显示错误信息
ui->statusbar->showMessage(m_serial.errorString() + QString::number(m_serial.error()), 5000);
return;
}
} else {
// 关闭串口
m_serial.close();
// 切换按钮文本
ui->pushButton_open->setText(QStringLiteral("打开串口"));
}
}
串口读取数据
相关读写方法
| 方法 | 说明 |
|---|---|
read(char *data, qint64 maxSize) |
最多从设备读取 maxSize 个字节到 data,并返回读取的字节数 |
readData(char *data, qint64 maxSize) |
从设备读取最多 maxSize 个字节到 data,并返回读取的字节数;如果发生错误返回 -1 |
readAll() |
从设备读取所有剩余数据,并将其作为字节数组返回 |
write(const char *data, qint64 maxSize) |
最多将 maxSize 个字节的数据从 data 写入设备,返回实际写入字节数;如果发生错误返回 -1 |
writeData(const char *data, qint64 maxSize) |
从数据向设备写入最多 maxSize 个字节,返回写入的字节数;如果发生错误返回 -1 |
相关信号
| 信号 | 说明 |
|---|---|
readyRead |
每次有新数据可用于从设备的当前读取通道读取时,都会发出一次该信号 |
bytesWritten |
每次将数据负载写入设备的当前写入通道时,都会发出此信号 |
连接信号和槽
cpp
// 连接信号与槽:有数据到来
connect(&m_serial, &QSerialPort::readyRead, this, &MainWindow::serialReadData);
// 连接数据发送后的信号槽
connect(&m_serial, &QSerialPort::bytesWritten, this, &MainWindow::bytesWriteData);
读取与发送数据实现
cpp
void MainWindow::serialReadData()
{
// 读取串口的数据,从接收缓冲区中读取数据
QByteArray buffer = m_serial.readAll();
// 数据转换为字符串
QString strText = QString(buffer);
// 加上时间
QDateTime current_data_time = QDateTime::currentDateTime();
QString t = current_data_time.toString("yyyy-MM-dd hh:mm:ss.zzz:");
// 追加到末尾
ui->plainTextEdit_recvText->appendPlainText(t + strText + "\n");
}
void MainWindow::on_pushButton_send_clicked()
{
QByteArray data = ui->plainTextEdit_sendText->toPlainText().toUtf8();
// 向串口写入数据
m_serial.write(data);
}
void MainWindow::bytesWriteData(qint64 bytes)
{
ui->statusbar->showMessage(QStringLiteral("发送了%1字节").arg(bytes), 5000);
}

串口定时发送数据
QTimer 类提供了重复和单次触发信号的定时器。
cpp
// 定时器
QTimer m_timer;
// 启动定时器,单位为毫秒
m_timer.start(msec);
// 定时器触发信号槽
connect(&m_timer, SIGNAL(timeout()), this, SLOT(timeout()));
案例实现
cpp
void MainWindow::on_pushButton_send_clicked()
{
QByteArray data = ui->plainTextEdit_sendText->toPlainText().toUtf8();
// 向串口写入数据
m_serial.write(data);
}
// 连接定时器信号和槽
connect(&m_timer, &QTimer::timeout, this, &MainWindow::timeUp);
// 勾选「定时发送」复选框
void MainWindow::on_checkBox_timeCheckBox_stateChanged(int arg1)
{
if (arg1) { // 勾选了
m_timer.start(ui->lineEdit_time->text().toUInt());
} else { // 取消勾选
m_timer.stop();
}
}
// 定时器触发的槽函数
void MainWindow::timeUp()
{
// 发送数据
on_pushButton_send_clicked();
}

串口接收大量数据导致页面卡顿
接收数据的函数在主线程执行。当主线程处理太多高频串口数据时,会来不及响应 UI 交互,从而导致主界面卡顿。
如下图可以看到,接收消息和 UI 都在同一个主线程上:

cpp
void MainWindow::serialReadData()
{
// 读取串口的数据,从接收缓冲区中读取数据
QByteArray buffer = m_serial.readAll();
// 数据转换为字符串
QString strText = QString(buffer);
// 加上时间
QDateTime current_data_time = QDateTime::currentDateTime();
QString t = current_data_time.toString("yyyy-MM-dd hh:mm:ss.zzz:");
// 追加到末尾
ui->plainTextEdit_recvText->appendPlainText(t + strText + "\n");
// 输出线程 ID
qDebug() << QThread::currentThreadId();
// 耗时工作(用于演示卡顿)
int sum = 0;
for (int i = 0; i < 10000000; ++i) {
sum += i;
}
qDebug() << sum;
}

串口使用多线程
Qt 使用多线程的步骤
- 新建一个
xxx类,公有继承自QObject,将线程要执行的内容写在xxx类的某些槽函数中。 - 创建一个
QThread对象t,调用start()方法。 - 将
xxx类的对象x通过x.moveToThread(t)移动到线程。 - 使用信号槽机制,触发
xxx类的槽函数,即线程要执行的内容。
目录结构
cpp
MySerialToolPlus/
├── main.cpp # 程序入口
├── mainwindow.h # 主窗口类声明
├── mainwindow.cpp # 主窗口类实现(界面 + 线程管理 + 信号槽)
├── mainwindow.ui # 界面文件(科技风样式表)
├── myserialport.h # 串口封装类声明
├── myserialport.cpp # 串口封装类实现
├── MySerialToolPlus.pro # qmake 工程文件
├── ui_mainwindow.h # 由 .ui 自动生成的界面头文件(勿手改)
└── MySerialToolPlus.pro.user # Qt Creator 用户配置(含 kit、运行配置)
多线程模型
cpp
┌─────────────────────────────────────────────────────────┐
│ 主线程(UI 线程) │
│ │
│ MainWindow │
│ ├── 界面控件(参数下拉框、收发文本框、按钮) │
│ ├── 信号:sigStart / sigStop / sigSend │
│ └── 槽:started / stoped / recieved(刷新界面) │
│ │
└───────────────┬─────────────────────────┬───────────────┘
│ QueuedConnection │ QueuedConnection
▼ ▼
┌─────────────────────────────────────────────────────────┐
│ 子线程(工作线程) │
│ │
│ QThread m_thread(运行事件循环) │
│ └── MySerialPort m_serial(被 moveToThread 移入) │
│ ├── 槽:Start / Stop / Send(串口操作) │
│ └── 信号:sigStarted / sigStoped / sigReceived │
│ │
└─────────────────────────────────────────────────────────┘
m_serial.moveToThread(&m_thread) 把串口对象的线程亲和性改为子线程;之后主线程发信号 → 子线程槽函数执行(自动变成 QueuedConnection 队列连接);串口收到数据 → 子线程发信号 → 主线程槽函数刷新界面。
信号槽通信关系
| 方向 | 信号 | 槽 | 作用 |
|---|---|---|---|
| 主 → 子 | MainWindow::sigStart |
MySerialPort::Start |
请求打开串口 |
| 主 → 子 | MainWindow::sigStop |
MySerialPort::Stop |
请求关闭串口 |
| 主 → 子 | MainWindow::sigSend |
MySerialPort::Send |
请求发送数据 |
| 子 → 主 | MySerialPort::sigStarted |
MainWindow::started |
通知打开成功 |
| 子 → 主 | MySerialPort::sigStoped |
MainWindow::stoped |
通知关闭(或失败) |
| 子 → 主 | MySerialPort::sigReceived |
MainWindow::recieved |
通知收到数据 |
文件结构:

整体代码
myserialport.h
cpp
#ifndef MYSERIALPORT_H
#define MYSERIALPORT_H
#include <QSerialPort>
/**
* @brief 串口封装类
*
* 继承自 QSerialPort,是对串口功能的核心封装。
*
* 该类的对象会在运行时被移动到子线程中(见 MainWindow::initCOM),
* 因此所有串口相关操作(打开、关闭、读写)都在子线程中执行,
* 避免阻塞 UI 主线程。
*/
class MySerialPort : public QSerialPort
{
Q_OBJECT
public:
MySerialPort();
/**
* @brief 串口参数结构体
*
* 用于一次性打包打开串口所需的全部参数,
* 通过信号 sigStart 跨线程传递给子线程中的 Start() 槽函数。
*/
struct Settings{
QString name; // 端口名称,如 "COM1"、"COM2"
BaudRate baudRate; // 波特率,如 Baud9600
DataBits dataBits; // 数据位,如 Data8
Parity parity; // 奇偶校验,如 NoParity
StopBits stopBits; // 停止位,如 OneStop
FlowControl flowControl; // 流控制,如 NoFlowControl
};
public slots:
// 以下槽函数将在子线程中执行
/// 打开串口
/// @param sets 串口参数
void Start(Settings sets);
/// 关闭串口
void Stop();
/// 向串口写入数据
/// @param data 待发送的数据
void Send(QByteArray data);
signals:
// 以下信号用于向外部(主线程)传递串口状态与数据
void sigStarted(); // 串口打开成功
void sigStoped(int status); // 串口停止(0=正常停止,1=打开失败)
void sigReceived(QByteArray data); // 收到串口数据
};
#endif // MYSERIALPORT_H
myserialport.cpp
cpp
#include "myserialport.h"
/**
* @brief 构造函数
*
* 连接 QSerialPort 的 readyRead 信号:
* 当串口接收缓冲区有数据到达时触发,一次性读取全部数据并转发给外部。
*/
MySerialPort::MySerialPort()
{
connect(this,&QSerialPort::readyRead,[this]{
// 收到串口数据:一次性读取缓冲区中的全部数据
QByteArray arr = readAll();
// 通过信号把数据转发给主线程去刷新界面
emit sigReceived(arr);
});
}
/**
* @brief 打开串口(在子线程中执行)
* @param sets 串口参数
*
* 依次设置端口名称、奇偶校验、波特率、数据位、停止位、流控制,
* 然后以读写模式打开串口。
* - 成功:发出 sigStarted() 信号
* - 失败:发出 sigStoped(1) 信号(1 表示打开失败)
*/
void MySerialPort::Start(Settings sets){
// 设置串口参数
QSerialPort::setPortName(sets.name);
QSerialPort::setParity(sets.parity);
QSerialPort::setBaudRate(sets.baudRate);
QSerialPort::setDataBits(sets.dataBits);
QSerialPort::setStopBits(sets.stopBits);
QSerialPort::setFlowControl(sets.flowControl);
// 打开串口(读写模式)
if(QSerialPort::open(QIODevice::ReadWrite)){
emit sigStarted();
}else{
// 打开失败传 1,正常停止传 0
emit sigStoped(1);
return;
}
}
/**
* @brief 关闭串口(在子线程中执行)
*
* 若串口处于打开状态则先关闭,然后发出 sigStoped(0) 信号(0 表示正常停止)。
*/
void MySerialPort::Stop(){
// 关闭串口
if(QSerialPort::isOpen()){
QSerialPort::close();
}
// 打开失败传 1,正常停止传 0
emit sigStoped(0);
}
/**
* @brief 向串口写数据(在子线程中执行)
* @param data 待发送的数据
*
* 仅当串口已打开时才写入,避免向未打开的串口写入导致异常。
*/
void MySerialPort::Send(QByteArray data){
if(QSerialPort::isOpen()){
QSerialPort::write(data);
}
}
mainwindow.h
cpp
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QMainWindow>
#include <QSerialPort>
#include <QDateTime>
#include <QTimer>
#include <QDebug>
#include <QThread>
#include "myserialport.h"
QT_BEGIN_NAMESPACE
namespace Ui { class MainWindow; }
QT_END_NAMESPACE
/**
* @brief 主窗口类
*
* 职责:
* 1. 构建串口工具界面(左侧参数设置区 + 右侧数据收发区);
* 2. 管理串口对象 MySerialPort 与工作线程 QThread 的生命周期;
* 3. 作为 UI 主线程与串口工作线程之间的桥梁,通过信号槽进行通信。
*
* 多线程架构:
* - 串口对象 m_serial 通过 moveToThread 被移动到子线程 m_thread 中;
* - UI 操作(点击按钮)在主线程产生信号,跨线程投递到子线程执行串口读写;
* - 串口的异步通知(数据到达 / 打开 / 关闭)再以信号形式回传主线程刷新界面。
*/
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = nullptr);
~MainWindow();
// 初始化界面控件内容(填充下拉框选项、设置默认值等)
void initUI();
// 初始化串口对象(移动到线程、启动线程、建立信号槽连接)
void initCOM();
private slots:
// 点击"打开串口 / 关闭串口"按钮的槽函数(由 ui 命名约定自动连接)
void on_pushButton_open_clicked();
// 点击"发送"按钮的槽函数
void on_pushButton_send_clicked();
void timeUp();
void on_checkBox_timeCheckBox_stateChanged(int arg1);
signals:
// 发送给串口工作线程的"打开串口"信号,携带串口参数
void sigStart(MySerialPort::Settings s);
// 发送给串口工作线程的"关闭串口"信号
void sigStop();
// 发送给串口工作线程的"发送数据"信号
void sigSend(QByteArray data);
public slots:
// 串口打开成功后的界面回调
void started();
// 串口关闭(或打开失败)后的界面回调
// @param status 0=正常停止,1=打开失败
void stoped(int status);
// 收到串口数据后的界面回调
void recieved(QByteArray data);
private:
Ui::MainWindow *ui;
// 串口对象(运行时被移动到子线程)
MySerialPort m_serial;
// 串口工作线程
QThread m_thread;
// 定时器对象
QTimer m_timer;
};
#endif // MAINWINDOW_H
mainwindow.cpp
cpp
#include "mainwindow.h"
#include "ui_mainwindow.h"
/**
* @brief 构造函数
*
* 完成界面构建、自定义类型注册、界面与串口模块的初始化。
*/
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
// 注册自定义类型,使 MySerialPort::Settings 能够通过信号槽跨线程传递。
// 跨线程的信号槽默认使用 QueuedConnection(队列连接),参数需要在事件队列中
// 投递,因此自定义类型必须先注册到 Qt 元对象系统,否则运行时会报错。
qRegisterMetaType<MySerialPort::Settings>("MySerialPort::Settings");
initUI(); // 初始化界面控件
initCOM(); // 初始化串口对象与工作线程
}
/**
* @brief 析构函数
*
* 在销毁对象前,先停止串口工作线程:
* 1. quit() 请求线程退出事件循环;
* 2. wait() 阻塞等待线程真正结束。
* 确保线程不会在对象销毁后仍然运行(否则会触发
* "QThread: Destroyed while thread is still running" 警告,甚至崩溃)。
*/
MainWindow::~MainWindow()
{
// 停止线程:先退出事件循环,再等待线程结束
m_thread.quit();
m_thread.wait();
delete ui;
}
/**
* @brief 初始化界面控件内容
*
* 填充各个下拉框的可选项,并设置默认值。
*
* 注意:下拉框每一项使用 addItem(显示文本, 关联数据) 的形式,
* - 第一个参数是显示给用户看的文本(如 "Baud9600");
* - 第二个参数是真正存储的枚举值(如 QSerialPort::Baud9600)。
* 这样在读取参数时用 currentData() 即可拿到枚举值,而不是去解析文本。
*/
void MainWindow::initUI(){
// 端口名称,预制 10 个(实际使用建议改为动态枚举真实存在的串口)
for(int i=1;i<=10;i++){
ui->comboBox_portName->addItem(QString("COM%1").arg(i));
}
// 波特率:显示文本 + QSerialPort::BaudRate 枚举值
ui->comboBox_baudRate->addItem(QString("Baud1200"),QSerialPort::Baud1200);
ui->comboBox_baudRate->addItem(QString("Baud2400"),QSerialPort::Baud2400);
ui->comboBox_baudRate->addItem(QString("Baud4800"),QSerialPort::Baud4800);
ui->comboBox_baudRate->addItem(QString("Baud9600"),QSerialPort::Baud9600);
ui->comboBox_baudRate->addItem(QString("Baud19200"),QSerialPort::Baud19200);
ui->comboBox_baudRate->addItem(QString("Baud38400"),QSerialPort::Baud38400);
ui->comboBox_baudRate->addItem(QString("Baud57600"),QSerialPort::Baud57600);
ui->comboBox_baudRate->addItem(QString("Baud115200"),QSerialPort::Baud115200);
// 数据位:显示文本 + QSerialPort::DataBits 枚举值
ui->comboBox_dataBit->addItem("8",QSerialPort::Data8);
ui->comboBox_dataBit->addItem("7",QSerialPort::Data7);
ui->comboBox_dataBit->addItem("6",QSerialPort::Data6);
ui->comboBox_dataBit->addItem("5",QSerialPort::Data5);
// 奇偶校验:显示文本 + QSerialPort::Parity 枚举值
ui->comboBox_parity->addItem("None",QSerialPort::NoParity);
ui->comboBox_parity->addItem("Even",QSerialPort::EvenParity);
ui->comboBox_parity->addItem("Odd",QSerialPort::OddParity);
ui->comboBox_parity->addItem("Mark",QSerialPort::MarkParity);
ui->comboBox_parity->addItem("Space",QSerialPort::SpaceParity);
// 停止位:显示文本 + QSerialPort::StopBits 枚举值
ui->comboBox_stopBit->addItem("1",QSerialPort::OneStop);
ui->comboBox_stopBit->addItem("1.5",QSerialPort::OneAndHalfStop);
ui->comboBox_stopBit->addItem("2",QSerialPort::TwoStop);
// 流控制:显示文本 + QSerialPort::FlowControl 枚举值
ui->comboBox_flowControl->addItem("None",QSerialPort::NoFlowControl);
ui->comboBox_flowControl->addItem("RTS/CTS",QSerialPort::HardwareControl);
ui->comboBox_flowControl->addItem("XON/XOFF",QSerialPort::SoftwareControl);
// 定时发送的默认时间间隔(单位:毫秒)
ui->lineEdit_time->setText("1000");
// 连接定时器信号和槽
connect(&m_timer,&QTimer::timeout,this,&MainWindow::timeUp);
}
// 定时器触发的槽函数
void MainWindow::timeUp(){
// 发送数据
on_pushButton_send_clicked();
}
/**
* @brief 点击"打开串口 / 关闭串口"按钮的槽函数
*
* 根据按钮当前文字判断要执行的动作:
* - 文字为"打开串口":读取界面上的串口参数,打包后发 sigStart(s) 信号,
* 请求子线程打开串口;
* - 文字为"关闭串口":发 sigStop() 信号,请求子线程关闭串口。
*/
void MainWindow::on_pushButton_open_clicked()
{
QString text = ui->pushButton_open->text();
if(text==QStringLiteral("打开串口")){
// 打包串口参数
MySerialPort::Settings s;
s.name = ui->comboBox_portName->currentText(); // 端口名是纯文本
s.baudRate = (QSerialPort::BaudRate)ui->comboBox_baudRate->currentData().toInt(); // 其余参数取下拉框关联的枚举值
s.dataBits = (QSerialPort::DataBits)ui->comboBox_dataBit->currentData().toInt();
s.stopBits = (QSerialPort::StopBits)ui->comboBox_stopBit->currentData().toInt();
s.parity = (QSerialPort::Parity)ui->comboBox_parity->currentData().toInt();
s.flowControl = (QSerialPort::FlowControl)ui->comboBox_flowControl->currentData().toInt();
emit sigStart(s); // 发信号请求子线程打开串口
}else{
emit sigStop(); // 发信号请求子线程关闭串口
}
}
/**
* @brief 点击"发送"按钮的槽函数
*
* 读取发送输入框中的文本,转换为 UTF-8 字节流,
* 通过 sigSend 信号交给子线程写入串口。
*/
void MainWindow::on_pushButton_send_clicked()
{
QString strSend = ui->plainTextEdit_sendText->toPlainText(); // 读取发送框文本
QByteArray arr = strSend.toUtf8(); // 转为 UTF-8 字节流
emit sigSend(arr); // 发信号请求子线程发送
}
/**
* @brief 初始化串口对象与工作线程
*
* 1. 将串口对象 m_serial 移动到子线程 m_thread(改变线程亲和性);
* 2. 启动子线程,其内部运行事件循环;
* 3. 建立主线程与子线程之间的信号槽连接。
*
* 由于 m_serial 位于子线程,连接类型会自动变为 QueuedConnection,
* 主线程发出的信号会被投递到子线程的事件队列中,由子线程依次执行。
*/
void MainWindow::initCOM(){
m_serial.moveToThread(&m_thread); // 串口对象移动到子线程(须在线程启动前完成)
m_thread.start(); // 启动线程
// 主动发送信号:主线程 -> 子线程(UI 操作驱动串口动作)
connect(this,&MainWindow::sigStart,&m_serial,&MySerialPort::Start);
connect(this,&MainWindow::sigStop,&m_serial,&MySerialPort::Stop);
connect(this,&MainWindow::sigSend,&m_serial,&MySerialPort::Send);
// 被动接受信号:子线程 -> 主线程(串口状态与数据回传刷新界面)
connect(&m_serial,&MySerialPort::sigStarted,this,&MainWindow::started);
connect(&m_serial,&MySerialPort::sigStoped,this,&MainWindow::stoped);
connect(&m_serial,&MySerialPort::sigReceived,this,&MainWindow::recieved);
}
/**
* @brief 串口打开成功后的界面回调
*
* 更新按钮文字为"关闭串口",并禁用参数设置区(打开状态下不允许修改参数)。
*/
void MainWindow::started(){
ui->pushButton_open->setText(QStringLiteral("关闭串口"));
ui->groupBox->setEnabled(false);
}
/**
* @brief 串口关闭(或打开失败)后的界面回调
* @param status 0=正常停止,1=打开失败
*
* 恢复按钮文字为"打开串口",并启用参数设置区。
*/
void MainWindow::stoped(int status){
ui->pushButton_open->setText(QStringLiteral("打开串口"));
ui->groupBox->setEnabled(true);
}
/**
* @brief 收到串口数据后的界面回调
* @param data 从串口读取到的数据
*
* 将数据转为字符串,加上当前时间戳后追加显示到接收框。
*/
void MainWindow::recieved(QByteArray data){
// 读取串口的数据,从接收缓冲区中读取数据
// QByteArray buffer = m_serial.readAll();
// 数据转换为字符串
QString strText = QString(data);
// 加上时间
QDateTime current_data_time = QDateTime::currentDateTime();
QString t = current_data_time.toString("yyyy-MM-dd hh:mm:ss.zzz:");
// 追加到末尾
ui->plainTextEdit_recvText->appendPlainText(t+strText+"\n");
}
void MainWindow::on_checkBox_timeCheckBox_stateChanged(int arg1)
{
if(arg1){ // 勾选了
m_timer.start(ui->lineEdit_time->text().toUInt());
}else{ // 取消勾选
m_timer.stop();
}
}
关键知识点总结
| 知识点 | 说明 |
|---|---|
moveToThread |
改变 QObject 的线程亲和性,使其槽函数在目标线程执行 |
QThread::quit() / wait() |
优雅退出线程:退出事件循环 + 等待线程结束 |
qRegisterMetaType |
跨线程信号槽传自定义类型前必须注册 |
QueuedConnection |
跨线程信号槽的默认连接方式,参数经事件队列投递 |
currentData() vs currentText() |
下拉框关联数据 vs 显示文本 |
readyRead 信号 |
串口缓冲区有数据到达时触发 |
多线程串口效果
