Qt6基础教程:多线程串口通信实战
Qt6基础教程:多线程串口通信实战
在实际 Qt 项目中,串口通信往往需要长时间运行、频繁收发数据。如果在 GUI 线程中直接操作 QSerialPort,则 readyRead 信号和 write 调用可能阻塞界面,造成卡顿。本文介绍一种线程安全、高内聚低耦合的设计模式:将串口操作封装到工作线程中,通过 SerialManager 对外提供简洁接口,实现串口通信与界面逻辑的完全分离。
一、创建串口工作类 SerialWorker
我们要设计的第一个类是 SerialWorker,它继承自 QObject,以便后续可以轻松移动到工作线程。
1.1 基本框架
头文件 serialworker.h 最开始只需要包含必要的头文件和类声明:
#ifndef SERIALWORKER_H#define SERIALWORKER_H
#include <QObject>#include <QSerialPort>
class SerialWorker : public QObject{ Q_OBJECT
};#endif接着添加构造函数和析构函数。注意构造时接收父对象指针,以便启用 Qt 的对象树内存管理。
class SerialWorker : public QObject{ Q_OBJECTpublic: explicit SerialWorker(QObject *parent = nullptr); ~SerialWorker();};对应的实现文件 serialworker. 暂时如下:
#include "serialworker.h"#include <QDebug>
SerialWorker::SerialWorker(QObject *parent) : QObject(parent){}
SerialWorker::~SerialWorker(){}1.2 加入串口对象成员
在 SerialWorker 中增加一个 QSerialPort 指针,并在构造函数中创建它,同时将其设置为 this 的子对象。这样,当 SerialWorker 销毁时,m_serial 会被 Qt 自动清理,无需手动 delete。
class SerialWorker : public QObject{ // ... 已有代码 ...private: QSerialPort *m_serial = nullptr;};SerialWorker::SerialWorker(QObject *parent) : QObject(parent) , m_serial(new QSerialPort(this)){}说明:
new QSerialPort(this)将m_serial作为SerialWorker的子对象,自动纳入 Qt 对象树。父对象析构时会递归删除所有子对象,因此我们不需要在析构函数中写delete m_serial。
1.3 串口的打开与关闭
为了能让外部控制串口,我们提供 open 和 close 两个槽函数。由于它们将来可能被跨线程调用,所以定义为 public slots。同时,为了便于传递串口参数,先定义一个配置结构体 SerialConfig。
struct SerialConfig { QString portName; qint32 baudRate = 115200; QSerialPort::DataBits dataBits = QSerialPort::Data8; QSerialPort::Parity parity = QSerialPort::NoParity; QSerialPort::StopBits stopBits = QSerialPort::OneStop; QSerialPort::FlowControl flowControl = QSerialPort::NoFlowControl;};
class SerialWorker : public QObject{ Q_OBJECTpublic: explicit SerialWorker(QObject *parent = nullptr); ~SerialWorker();public slots: void open(const SerialConfig &config); void close();private: QSerialPort *m_serial = nullptr; void applyConfig(const SerialConfig &config); bool m_isOpen = false;};对应的实现如下:
void SerialWorker::open(const SerialConfig &config){ if (m_isOpen) { close(); // 如果已打开,先关闭 } applyConfig(config); m_serial->open(QIODevice::ReadWrite); m_isOpen = true;}
void SerialWorker::close(){ if (m_serial->isOpen()) { m_serial->clear(); m_serial->close(); } m_isOpen = false;}
void SerialWorker::applyConfig(const SerialConfig &config){ m_serial->setPortName(config.portName); m_serial->setBaudRate(config.baudRate); m_serial->setDataBits(config.dataBits); m_serial->setParity(config.parity); m_serial->setStopBits(config.stopBits); m_serial->setFlowControl(config.flowControl);}为什么打开前要调用
close? 如果串口已经打开,直接修改端口名、波特率等参数可能不会立即生效,有些参数(如端口名)甚至要求串口处于关闭状态。先关闭再应用新配置,可以保证所有参数按照预期重新初始化。
1.4 接收数据:解决粘包与界面卡顿
串口数据是流式传输的,readyRead 信号可能在数据未完整时就被触发。如果每收到几个字节就立即通知上层,会导致频繁的信号发射和界面刷新,效率很低。更好的做法是:将数据暂存到缓冲区,等待一小段时间(例如 100 毫秒),如果这段时间内没有新数据到来,再将完整的缓冲区数据一次性发出。这既能避免粘包,又能减少界面刷新次数。
我们使用一个 QTimer 来实现延迟聚合。同时为了防止缓冲区无限增长,设定一个最大容量(如 10 MB),超出则强制发送。
修改类定义,添加必要的成员和槽函数:
class SerialWorker : public QObject{ Q_OBJECTpublic: // ... 已有 public 成员 ...signals: void dataReceived(const QByteArray &data);private slots: void onReadyRead(); void onTimeout();private: QSerialPort *m_serial = nullptr; QTimer *m_delayTimer = nullptr; QByteArray m_buffer; int m_receiveDelay = 100; // 默认100ms bool m_isOpen = false; void applyConfig(const SerialConfig &config); void flushBuffer();};构造函数中创建定时器并连接信号:
SerialWorker::SerialWorker(QObject *parent) : QObject(parent) , m_serial(new QSerialPort(this)) , m_delayTimer(new QTimer(this)){ m_delayTimer->setSingleShot(true); // 单次触发模式 connect(m_serial, &QSerialPort::readyRead, this, &SerialWorker::onReadyRead); connect(m_delayTimer, &QTimer::timeout, this, &SerialWorker::onTimeout);}onReadyRead 以及相关辅助函数的实现:
void SerialWorker::onReadyRead(){ QByteArray chunk = m_serial->readAll(); m_buffer.append(chunk);
// 防止缓冲区无限增长(例如 10MB 上限) constexpr int MAX_BUFFER_SIZE = 10 * 1024 * 1024; // 10 MiB if (m_buffer.size() > MAX_BUFFER_SIZE) { flushBuffer(); }
if (m_receiveDelay == 0) { flushBuffer(); // 立即发送 } else { m_delayTimer->start(m_receiveDelay); // 重启定时器 }}
void SerialWorker::onTimeout(){ flushBuffer();}
void SerialWorker::flushBuffer(){ if (m_buffer.isEmpty()) return; emit dataReceived(m_buffer); m_buffer.clear();}核心思想:每次
readyRead都会重置定时器,只有在延时期间没有新数据到达时,定时器才会超时并真正发出数据。这既能保证实时性,又能有效聚合小数据包。
1.5 发送数据:支持文本与十六进制
为了灵活发送数据,我们再实现一个 sendData 槽函数同时提供一个 setReceiveDelay 接口,用于动态调整接收聚合延时。
类定义中增加:
public slots: void sendData(const QByteArray &data); void setReceiveDelay(int ms);实现如下:
void SerialWorker::sendData(const QByteArray &data){ if (!m_isOpen) return; if (data.isEmpty()) return;
QByteArray outData = data; m_serial->write(outData);}
void SerialWorker::setReceiveDelay(int ms){ m_receiveDelay = ms;}说明:上述
sendData中仅完成了格式转换,实际写入操作m_serial->write(outData)请根据项目需要添加。同时,示例代码中onTimeout槽函数已在前面实现,但类声明中未显式声明,实际使用时请确保声明与定义一致。
1.6 完善错误处理与状态信号
为了便于上层感知串口状态变化和错误,我们增加一组信号:
signals: void opened(); // 打开成功 void closed(); // 关闭成功 void errorOccurred(const QString &errorString); void dataReceived(const QByteArray &data); void configApplied(const QString &configStr);同时完善 open 和 sendData 的实现,增加错误检查和信号发射。
完善后的 open 函数:
void SerialWorker::open(const SerialConfig &config){ if (m_isOpen) { close(); }
applyConfig(config); if (!m_serial->open(QIODevice::ReadWrite)) { emit errorOccurred(tr("打开串口失败: %1").arg(m_serial->errorString())); return; } m_isOpen = true; emit opened(); emit configApplied(config.toString()); // 需要 SerialConfig 实现 toString(),或自行构造字符串}完善后的 sendData 函数:
void SerialWorker::sendData(const QByteArray &data){ if (!m_isOpen) { emit errorOccurred(tr("串口未打开,无法发送数据")); return; } if (data.isEmpty()) return;
QByteArray outData = data;
qint64 written = m_serial->write(outData); if (written != outData.size()) { emit errorOccurred(tr("数据发送不完整")); }}设置接收延迟的槽函数:
void SerialWorker::setReceiveDelay(int ms){ m_receiveDelay = ms;}至此,SerialWorker 类已经具备了打开、关闭、发送、接收(带延迟聚合)以及完整错误通知的能力。
二、创建管理类 SerialManager
SerialWorker 已经实现了串口的所有底层操作,但它仍然运行在创建它的线程中。为了实现真正的多线程隔离,需要一个管理类,负责:
- 将
SerialWorker移动到一个独立的QThread中。 - 对外提供线程安全的调用接口(通过信号槽自动处理线程切换)。
- 将
SerialWorker发出的信号转发给上层,避免上层直接与工作对象耦合。
2.1 定义 SerialManager 类的基本框架
创建头文件 serialmanager.h,定义 SerialManager 类,继承自 QObject。需要包含一个 QThread 成员和一个 SerialWorker 指针。
#ifndef SERIALMANAGER_H#define SERIALMANAGER_H
#include <QObject>#include <QThread>#include "serialworker.h"
class SerialManager : public QObject{ Q_OBJECTpublic: explicit SerialManager(QObject *parent = nullptr); ~SerialManager();
private: QThread m_workerThread; SerialWorker *m_worker = nullptr;};
#endif2.2 添加对外公开接口
为了让 GUI 层能够控制串口,需要提供 open、close、sendData、setReceiveDelay 等函数。这些函数将被设计为 Q_INVOKABLE(方便 QML 调用,但不是必须),并且可以在任意线程中安全调用——它们内部只发射信号,不执行实际操作。
class SerialManager : public QObject{ Q_OBJECTpublic: explicit SerialManager(QObject *parent = nullptr); ~SerialManager();
// 对外公开接口(可从任意线程安全调用) Q_INVOKABLE void open(const SerialConfig &config); Q_INVOKABLE void close(); Q_INVOKABLE void sendData(const QByteArray &data); Q_INVOKABLE void setReceiveDelay(int ms);
// ... 其余成员};2.3 添加内部请求信号
公开接口本身不直接操作 SerialWorker,而是发射一个“请求信号”。这些信号会被连接到 SerialWorker 的对应槽函数。由于信号槽可以跨线程,SerialWorker 将在自己的线程中执行这些槽。
class SerialManager : public QObject{ // ...signals: // 请求信号(用于转发给 SerialWorker) void requestOpen(const SerialConfig &config); void requestClose(); void requestSendData(const QByteArray &data); void requestSetReceiveDelay(int ms);
// 转发信号(稍后添加)};2.4 添加转发信号
SerialWorker 会发出一些状态信号(如 opened、dataReceived 等)。上层只需要连接 SerialManager 的信号即可,因此需要定义一组相同的信号用于转发。
class SerialManager : public QObject{ // ...signals: // ... 请求信号
// 转发信号(来自 SerialWorker 的事件) void opened(); void closed(); void errorOccurred(const QString &errorString); void dataReceived(const QByteArray &data); void configApplied(const QString &configStr);};2.5 实现构造函数
构造函数需要完成以下工作:
- 创建
SerialWorker对象(此时它还在主线程)。 - 将工作对象移动到
m_workerThread。 - 连接请求信号到工作对象的槽。
- 连接工作对象的信号到转发信号。
- 启动工作线程。
// serialmanager.#include "serialmanager.h"
SerialManager::SerialManager(QObject *parent) : QObject(parent){ m_worker = new SerialWorker(); m_worker->moveToThread(&m_workerThread);
// 连接请求信号到工作对象的槽 connect(this, &SerialManager::requestOpen, m_worker, &SerialWorker::open); connect(this, &SerialManager::requestClose, m_worker, &SerialWorker::close); connect(this, &SerialManager::requestSendData, m_worker, &SerialWorker::sendData); connect(this, &SerialManager::requestSetReceiveDelay, m_worker, &SerialWorker::setReceiveDelay);
// 连接工作对象的信号到转发信号 connect(m_worker, &SerialWorker::opened, this, &SerialManager::opened); connect(m_worker, &SerialWorker::closed, this, &SerialManager::closed); connect(m_worker, &SerialWorker::errorOccurred, this, &SerialManager::errorOccurred); connect(m_worker, &SerialWorker::dataReceived, this, &SerialManager::dataReceived); connect(m_worker, &SerialWorker::configApplied, this, &SerialManager::configApplied);
m_workerThread.start();}2.6 实现析构函数
析构时需要安全地停止线程并释放工作对象。
SerialManager::~SerialManager(){ m_workerThread.quit(); m_workerThread.wait(); delete m_worker;}说明:
m_worker是在构造函数中用new创建的,并且被移到了工作线程。虽然它没有父对象,但我们在析构函数中手动delete是安全的,因为wait()保证了线程已经退出,不会再有槽函数被调用。
2.7 实现公开接口
每个公开接口只需要发射对应的请求信号。由于信号槽是异步的,调用者不会阻塞。
void SerialManager::open(const SerialConfig &config){ emit requestOpen(config);}
void SerialManager::close(){ emit requestClose();}
void SerialManager::sendData(const QByteArray &data){ emit requestSendData(data);}
void SerialManager::setReceiveDelay(int ms){ emit requestSetReceiveDelay(ms);}三、创建简易Demo
3.1 搭建基础界面与 SerialManager 集成
首先创建一个继承自 QWidget 的界面类,并加入必要的成员变量。
头文件 widget.h 初始结构:
#ifndef WIDGET_H#define WIDGET_H
#include <QWidget>#include "serial/serialmanager.h"
QT_BEGIN_NAMESPACEnamespace Ui { class Widget; }QT_END_NAMESPACE
class Widget : public QWidget{ Q_OBJECTpublic: explicit Widget(QWidget *parent = nullptr); ~Widget();
private: Ui::Widget *ui; SerialManager *manager = nullptr; // 串口管理器(多线程安全)};
#endif实现文件 widget.cpp 构造函数:
#include "widget.h"#include "ui_widget.h"
Widget::Widget(QWidget *parent) : QWidget(parent) , manager(new SerialManager(this)) // manager 作为子对象,自动释放 , ui(new Ui::Widget){ ui->setupUi(this);}
Widget::~Widget(){ delete ui;}说明:
SerialManager内部已经封装了工作线程,我们只需像普通QObject一样创建它,并设置父对象即可。所有串口操作都可以通过manager的公开接口安全调用。
3.2 初始化串口参数下拉框
串口调试助手需要让用户选择波特率、数据位等参数。我们在 Widget 中添加一个私有方法 initUIConfig(),在构造函数中调用它。
在 widget.h 中添加:
private: void initUIConfig();实现 initUIConfig():
void Widget::initUIConfig(){ // 波特率 ui->comBaudRate->addItems({"1200", "2400", "4800", "9600", "19200", "38400", "57600", "115200"}); ui->comBaudRate->setEditable(true); ui->comBaudRate->setValidator(new QIntValidator(0, 1000000, this)); ui->comBaudRate->setCurrentIndex(3); // 默认 9600
// 数据位 ui->comDataBits->addItems({"5", "6", "7", "8"}); ui->comDataBits->setCurrentIndex(3); // 默认 8
// 校验位 ui->comParity->addItems({"Even", "Mark", "None", "Odd", "Space"}); ui->comParity->setCurrentIndex(2); // 默认 None
// 停止位 ui->comStopBits->addItems({"1", "1.5", "2"}); ui->comStopBits->setCurrentIndex(0); // 默认 1}关键点:
- 使用
addItems一次性添加常用选项。- 波特率设置为可编辑 + 整数校验器,方便用户手动输入非常用波特率。
- 默认选择最常用的参数(9600/8/N/1)。
3.3 获取可用串口列表
动态扫描系统串口是调试助手的基本功能。我们添加 getportInfo() 方法,并在构造函数中调用。
声明与实现:
private: void getportInfo();
// widget.void Widget::getportInfo(){ ui->comPortName->clear(); QList<QSerialPortInfo> ports = QSerialPortInfo::availablePorts(); if (ports.isEmpty()) { ui->comPortName->addItem("无可用串口"); ui->OpenComBtn->setEnabled(false); return; } ui->OpenComBtn->setEnabled(true); for (const QSerialPortInfo &info : ports) ui->comPortName->addItem(info.portName());}说明:
- 使用
QSerialPortInfo::availablePorts()获取当前系统中的串口列表。- 若无可用串口,禁用“打开串口”按钮并显示提示项。
- 若有串口,启用按钮并填充端口名。
3.4 打开与关闭串口
这是与 SerialManager 交互的第一步。我们需要连接按钮的点击信号,并在槽函数中调用 manager->open(config) 或 manager->close()。同时,为了知道串口何时真正打开/关闭,需要连接 SerialManager 的状态信号。
在 Widget 中添加槽函数和状态处理:
private slots: void onOpenCloseButtonClicked(); void handleOpened(); void handleClosed(); void handleErrorOccurred(const QString &error);private: bool isSerialOpen = false;构造函数中连接信号:
// 在 Widget 构造函数中添加connect(ui->OpenComBtn, &QPushButton::clicked, this, &Widget::onOpenCloseButtonClicked);connect(manager, &SerialManager::opened, this, &Widget::handleOpened);connect(manager, &SerialManager::closed, this, &Widget::handleClosed);connect(manager, &SerialManager::errorOccurred, this, &Widget::handleErrorOccurred);实现打开/关闭逻辑:
void Widget::onOpenCloseButtonClicked(){ if (!isSerialOpen) { // 准备配置 SerialConfig config; config.portName = ui->comPortName->currentText(); config.baudRate = ui->comBaudRate->currentText().toInt();
// 数据位映射 switch (ui->comDataBits->currentIndex()) { case 0: config.dataBits = QSerialPort::Data5; break; case 1: config.dataBits = QSerialPort::Data6; break; case 2: config.dataBits = QSerialPort::Data7; break; default: config.dataBits = QSerialPort::Data8; break; } // 校验位映射 switch (ui->comParity->currentIndex()) { case 0: config.parity = QSerialPort::EvenParity; break; case 1: config.parity = QSerialPort::MarkParity; break; case 2: config.parity = QSerialPort::NoParity; break; case 3: config.parity = QSerialPort::OddParity; break; default: config.parity = QSerialPort::SpaceParity; break; } // 停止位映射 switch (ui->comStopBits->currentIndex()) { case 0: config.stopBits = QSerialPort::OneStop; break; case 1: config.stopBits = QSerialPort::OneAndHalfStop; break; default: config.stopBits = QSerialPort::TwoStop; break; }
manager->open(config); ui->OpenComBtn->setEnabled(false); ui->OpenComBtn->setText("正在打开..."); } else { manager->close(); }}
void Widget::handleOpened(){ isSerialOpen = true; ui->OpenComBtn->setEnabled(true); ui->OpenComBtn->setText("关闭串口");}
void Widget::handleClosed(){ isSerialOpen = false; ui->OpenComBtn->setEnabled(true); ui->OpenComBtn->setText("打开串口");}
void Widget::handleErrorOccurred(const QString &error){ QMessageBox::critical(this, "串口错误", error); isSerialOpen = false; ui->OpenComBtn->setEnabled(true); ui->OpenComBtn->setText("打开串口");}设计要点:
- 打开时先禁用按钮并显示“正在打开…”,防止重复点击。
- 打开成功/失败后,通过
handleOpened或handleErrorOccurred恢复按钮状态。- 关闭操作同样异步,最终由
handleClosed更新界面。
3.5 实现数据发送(支持纯文本与十六进制)
SerialManager 只接受 QByteArray 类型的原始字节数据。因此,我们需要根据界面上的“Hex发送”复选框,将用户输入的文本转换成对应的字节数组。
添加槽函数和辅助方法:
private slots: void onSendButtonClicked();private: void initCheckbox(); // 初始化 Hex 模式相关信号槽 void formatTxEditForHex(); // 实时格式化 Hex 输入 void convertTextToHex(); // 文本 -> Hex 字符串 void convertHexToText(); // Hex 字符串 -> 文本发送按钮槽函数实现:
void Widget::onSendButtonClicked(){ QString text = ui->TxEdit->toPlainText().trimmed(); if (text.isEmpty()) return;
bool hexFlag = ui->HexTxBox->isChecked(); QByteArray data;
if (hexFlag) { // 移除空格,校验偶数个字符 QString hexStr = text; hexStr.remove(' '); if (hexStr.length() % 2 != 0) { QMessageBox::warning(this, "错误", "十六进制数据必须为偶数个字符,请补零后重试"); return; } data = QByteArray::fromHex(hexStr.toUtf8()); if (data.isEmpty() && !hexStr.isEmpty()) { QMessageBox::warning(this, "错误", "无效的十六进制数据"); return; } } else { data = text.toUtf8(); // 文本模式使用 UTF-8 编码 } manager->sendData(data);}Hex 输入辅助功能(实时格式化和模式切换):
void Widget::initCheckbox(){ // 切换 Hex 发送模式时,转换当前文本框内容 connect(ui->HexTxBox, &QCheckBox::stateChanged, this, [=](int state) { if (state == Qt::Checked) convertTextToHex(); else convertHexToText(); });
// Hex 模式下,每次文本改变时自动添加空格、过滤非法字符 connect(ui->TxEdit, &QTextEdit::textChanged, this, [=]() { if (ui->HexTxBox->isChecked()) formatTxEditForHex(); });}
void Widget::formatTxEditForHex(){ // 实现细节参考最终源码:提取十六进制字符、每两个加空格、保持光标位置 // 此处省略完整代码}
void Widget::convertTextToHex(){ QString plainText = ui->TxEdit->toPlainText(); QByteArray bytes = plainText.toLocal8Bit(); QString hexStr = bytes.toHex(' ').toUpper(); ui->TxEdit->blockSignals(true); ui->TxEdit->setPlainText(hexStr); ui->TxEdit->blockSignals(false);}
void Widget::convertHexToText(){ QString hexText = ui->TxEdit->toPlainText(); QString pureHex = hexText; pureHex.remove(' '); QByteArray bytes = QByteArray::fromHex(pureHex.toUtf8()); if (bytes.isEmpty() && !pureHex.isEmpty()) { QMessageBox::warning(this, "错误", "无效的十六进制数据,无法转换为文本"); return; } QString plainText = QString::fromLocal8Bit(bytes); if (plainText.isEmpty() && !bytes.isEmpty()) plainText = QString::fromLatin1(bytes); ui->TxEdit->blockSignals(true); ui->TxEdit->setPlainText(plainText); ui->TxEdit->blockSignals(false);}关键说明:
- 界面层负责数据格式转换,
SerialManager保持简洁(只发送原始字节)。- Hex 模式下,提供实时格式化(自动插入空格、限制输入字符)和模式切换时的内容转换,提升用户体验。
3.6 实现数据接收与显示
接收数据通过连接 SerialManager::dataReceived 信号完成。我们可以在槽函数中根据用户设置(Hex显示、时间戳、自动换行)格式化数据并追加到接收文本框。
添加槽函数:
private slots: void handleDataReceived(const QByteArray &data);构造函数中连接信号:
connect(manager, &SerialManager::dataReceived, this, &Widget::handleDataReceived);实现接收处理:
void Widget::handleDataReceived(const QByteArray &data){ if (data.isEmpty()) return;
// 根据 Hex 复选框决定显示格式 QString display; if (ui->HexRxBox->isChecked()) { display = data.toHex(' ').toUpper(); if (!display.isEmpty()) display += ' '; } else { display = QString::fromUtf8(data); if (display.isEmpty()) display = QString::fromLatin1(data); }
ui->RxEdit->moveCursor(QTextCursor::End);
// 时间戳 if (ui->TimeBox->isChecked()) { QString timestamp = QDateTime::currentDateTime().toString("[yyyy/MM/dd hh:mm:ss] "); ui->RxEdit->insertPlainText(timestamp); }
ui->RxEdit->insertPlainText(display);
// 自动换行 if (ui->AWBox->isChecked()) ui->RxEdit->insertPlainText("\n");
ui->RxEdit->moveCursor(QTextCursor::End);}说明:
- 使用
data.toHex(' ')将字节数组转换为带空格的十六进制字符串,便于阅读。- 文本模式优先尝试 UTF-8 解码,失败则回退到 Latin1。
- 支持可选的时间戳和自动换行,所有选项实时生效。
3.7 实现定时发送
定时发送功能利用 QTimer 周期性调用发送按钮的槽函数。用户可通过复选框启用/禁用,并通过数值调节框改变间隔。
添加成员和槽函数:
private: QTimer *m_sendTimer = nullptr;private slots: void onTRBoxToggled(bool checked); void onTimeoutSend();构造函数中初始化定时器并连接信号:
// 在 Widget 构造函数中添加m_sendTimer = new QTimer(this);m_sendTimer->setSingleShot(false);connect(m_sendTimer, &QTimer::timeout, this, &Widget::onTimeoutSend);connect(ui->TRBox, &QCheckBox::toggled, this, &Widget::onTRBoxToggled);connect(ui->TVBox, QOverload<int>::of(&QSpinBox::valueChanged), this, [=](int value) { if (m_sendTimer->isActive()) m_sendTimer->setInterval(value);});实现槽函数:
void Widget::onTRBoxToggled(bool checked){ if (checked) { int interval = ui->TVBox->value(); if (interval <= 0) { QMessageBox::warning(this, "警告", "定时发送间隔必须大于0"); ui->TRBox->setChecked(false); return; } m_sendTimer->setInterval(interval); m_sendTimer->start(); } else { m_sendTimer->stop(); }}
void Widget::onTimeoutSend(){ onSendButtonClicked(); // 复用发送逻辑}设计说明:
- 定时器周期根据
ui->TVBox的值动态调整。- 启用定时发送前检查间隔合法性。
- 复用
onSendButtonClicked避免重复代码。
3.8 附加功能:清空发送/接收编辑框
为了界面友好,添加两个清空按钮非常简单,使用 Lambda 表达式直接连接。
在构造函数中添加:
connect(ui->ClearRxBtn, &QPushButton::clicked, this, [=]() { ui->RxEdit->clear(); });connect(ui->ClearTxBtn, &QPushButton::clicked, this, [=]() { ui->TxEdit->clear(); });四、 回顾与总结
通过本教程的三部分,我们完成了一个多线程串口通信模块:
SerialWorker:封装串口底层操作,利用延迟聚合优化接收效率,并提供打开、关闭、发送、错误处理等完整功能。SerialManager:将工作对象移至独立线程,通过信号槽提供线程安全的公开接口,并将工作对象的状态信号转发给上层,实现了界面与串口逻辑的完全解耦。- **
Widget**:展示了如何使用SerialManager构建一个串口调试助手,包括参数配置、串口扫描、数据收发(支持文本/十六进制、时间戳、自动换行)、定时发送等功能。