4560 字
23 分钟

Qt6基础教程:多线程串口通信实战

🤖AI 摘要
AI

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_OBJECT
public:
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 串口的打开与关闭#

为了能让外部控制串口,我们提供 openclose 两个槽函数。由于它们将来可能被跨线程调用,所以定义为 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_OBJECT
public:
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_OBJECT
public:
// ... 已有 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);

同时完善 opensendData 的实现,增加错误检查和信号发射。

完善后的 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 已经实现了串口的所有底层操作,但它仍然运行在创建它的线程中。为了实现真正的多线程隔离,需要一个管理类,负责:

  1. SerialWorker 移动到一个独立的 QThread 中。
  2. 对外提供线程安全的调用接口(通过信号槽自动处理线程切换)。
  3. SerialWorker 发出的信号转发给上层,避免上层直接与工作对象耦合。

2.1 定义 SerialManager 类的基本框架#

创建头文件 serialmanager.h,定义 SerialManager 类,继承自 QObject。需要包含一个 QThread 成员和一个 SerialWorker 指针。

serialmanager.h
#ifndef SERIALMANAGER_H
#define SERIALMANAGER_H
#include <QObject>
#include <QThread>
#include "serialworker.h"
class SerialManager : public QObject
{
Q_OBJECT
public:
explicit SerialManager(QObject *parent = nullptr);
~SerialManager();
private:
QThread m_workerThread;
SerialWorker *m_worker = nullptr;
};
#endif

2.2 添加对外公开接口#

为了让 GUI 层能够控制串口,需要提供 openclosesendDatasetReceiveDelay 等函数。这些函数将被设计为 Q_INVOKABLE(方便 QML 调用,但不是必须),并且可以在任意线程中安全调用——它们内部只发射信号,不执行实际操作。

class SerialManager : public QObject
{
Q_OBJECT
public:
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 会发出一些状态信号(如 openeddataReceived 等)。上层只需要连接 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_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACE
class Widget : public QWidget
{
Q_OBJECT
public:
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() 方法,并在构造函数中调用。

声明与实现:

widget.h
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 中添加槽函数和状态处理:

widget.h
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("打开串口");
}

设计要点

  • 打开时先禁用按钮并显示“正在打开…”,防止重复点击。
  • 打开成功/失败后,通过 handleOpenedhandleErrorOccurred 恢复按钮状态。
  • 关闭操作同样异步,最终由 handleClosed 更新界面。

3.5 实现数据发送(支持纯文本与十六进制)#

SerialManager 只接受 QByteArray 类型的原始字节数据。因此,我们需要根据界面上的“Hex发送”复选框,将用户输入的文本转换成对应的字节数组。

添加槽函数和辅助方法:

widget.h
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显示、时间戳、自动换行)格式化数据并追加到接收文本框。

添加槽函数:

widget.h
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 周期性调用发送按钮的槽函数。用户可通过复选框启用/禁用,并通过数值调节框改变间隔。

添加成员和槽函数:

widget.h
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 构建一个串口调试助手,包括参数配置、串口扫描、数据收发(支持文本/十六进制、时间戳、自动换行)、定时发送等功能。

完整工程地址

daitcl
/
Qt-demo
Waiting for api.github.com...
00K
0K
0K
Waiting...
Qt6基础教程:多线程串口通信实战
https://www.daitcc.top/posts/13725773/
作者
Dait
发布于
2026-04-02
许可协议
CC BY-NC-SA 4.0
如果这篇文章对你有帮助或启发,可以请作者喝杯咖啡 ☕️