介绍了一个基于Qt框架开发的简易串口助手,满足粉丝的需求。该项目展示了如何利用Qt的moveToThread
方法实现多线程串口通信,确保数据接收和发送功能的流畅性。项目中的核心类包括SerialWorker
类和MainWindow
类,分别负责串口操作和用户界面交互。
1. 项目背景与设计思路
Qt 是一个跨平台的 C++ 开发框架,具有强大的 GUI 开发能力和对硬件接口(如串口)的支持。串口通信通常涉及长时间的读写操作,因此为了避免阻塞用户界面线程,需要将串口操作放入后台线程处理。本项目通过 moveToThread
方法实现了这一需求,即将串口操作移至一个独立的工作线程中,保证界面在数据处理过程中仍然保持响应。
2. 串口助手的主要功能
该串口助手工具具备以下主要功能:
- 串口的自动检测与配置。
- 数据的十六进制格式与文本格式发送和接收。
- 数据的发送、接收、及错误处理。
- 多线程处理,确保串口操作不会阻塞主界面。
3. SerialWorker类:串口操作的后台处理
SerialWorker
类是串口助手的核心,专门用于处理串口的开启、关闭、数据收发等操作。该类继承自QObject
,其设计遵循Qt的信号与槽机制,以实现异步通信。
-
构造与析构:串口对象
serial
在构造时初始化为nullptr
,并在析构时安全关闭串口,释放资源。 -
startSerialPort方法:该方法通过Q_INVOKABLE修饰,可以在其他线程中被调用。它用于设置串口的端口名和波特率,并打开串口,准备进行读写操作。
-
handleReadyRead槽函数:这是处理串口数据接收的关键函数,当串口接收到数据时,它会被触发,读取数据并发射
dataReceived
信号。 -
handleWriteData槽函数:该函数用于向串口发送数据,在串口打开时调用
serial->write()
方法发送数据,确保数据通过串口传输出去。4.MainWindow 类:用户界面的交互逻辑
MainWindow
类是串口助手的主界面,负责用户操作与后台串口通信的交互。 -
UI初始化与线程处理:在构造函数中,将
SerialWorker
对象移到独立线程workerThread
中,确保串口操作在后台线程执行,不阻塞界面。通过QMetaObject::invokeMethod
实现主线程对工作线程的安全调用。 -
串口检测:
populateSerialPorts
方法自动检测可用串口,并将其填充到下拉菜单供用户选择。 -
串口启动与关闭:点击启动按钮后,获取串口名和波特率,通过
invokeMethod
调用SerialWorker
的startSerialPort
方法开启串口;点击停止按钮则关闭串口。 -
数据发送:应用支持文本和十六进制两种格式的发送,用户选择格式后,数据会被处理并通过
handleWriteData
方法发送至串口。 -
数据接收与显示:接收到串口数据后,通过信号将数据传给
MainWindow
,并实时显示,支持文本与十六进制格式的切换。 -
错误处理:当出现错误时,
SerialWorker
通过信号将错误信息传递给MainWindow
,主窗口会通过弹窗通知用户。 -
#ifndef SERIALWORKER_H #define SERIALWORKER_H#include <QObject> #include <QSerialPort> #include <QThread>#define tc(a) QString::fromLocal8Bit(a)class SerialWorker : public QObject {Q_OBJECT public:explicit SerialWorker(QObject *parent = nullptr); // 构造函数~SerialWorker(); // 析构函数Q_INVOKABLE void startSerialPort(const QString &portName, int baudRate); // 启动串口,Q_INVOKABLE使其可被invokeMethod调用Q_INVOKABLE void stopSerialPort(); // 关闭串口,Q_INVOKABLE使其可被invokeMethod调用signals:void dataReceived(const QByteArray &data); // 数据接收信号void errorOccurred(const QString &error); // 错误信号void writeData(const QByteArray &data); // 写数据信号public slots:void handleWriteData(const QByteArray &data); // 写数据槽函数private slots:void handleReadyRead(); // 处理串口接收数据槽函数private:QSerialPort *serial; // QSerialPort 对象指针 };#endif // SERIALWORKER_H
#include "serialworker.h" #include <QDebug>SerialWorker::SerialWorker(QObject *parent) : QObject(parent) {serial = nullptr; // 初始化时serial为空,稍后在startSerialPort中创建 }SerialWorker::~SerialWorker() {if (serial) {if (serial->isOpen()) {serial->close(); // 如果串口打开,关闭串口}delete serial; // 删除serial对象} }// 启动串口,Q_INVOKABLE 使其可被跨线程调用 void SerialWorker::startSerialPort(const QString &portName, int baudRate) {if (!serial) {serial = new QSerialPort(); // 创建串口对象}serial->setPortName(portName); // 设置串口名称serial->setBaudRate(baudRate); // 设置波特率// 连接串口的 readyRead 信号到 handleReadyRead 槽函数connect(serial, &QSerialPort::readyRead, this, &SerialWorker::handleReadyRead);// 打开串口,读写模式if (serial->open(QIODevice::ReadWrite)) {qDebug() << tc("串口成功打开:") << portName;} else {emit errorOccurred(serial->errorString()); // 发送错误信号} }// 停止串口操作 void SerialWorker::stopSerialPort() {if (serial && serial->isOpen()) {serial->close(); // 关闭串口qDebug() << tc("串口已关闭");} }// 写入数据到串口 void SerialWorker::handleWriteData(const QByteArray &data) {if (serial && serial->isOpen()) {serial->write(data); // 写数据到串口} else {emit errorOccurred(tc("串口未打开")); // 如果串口未打开,发送错误信号} }// 处理串口接收到的数据 void SerialWorker::handleReadyRead() {QByteArray data = serial->readAll(); // 读取所有数据emit dataReceived(data); // 发出数据接收信号 }
#ifndef MAINWINDOW_H #define MAINWINDOW_H#include <QMainWindow> #include <QSerialPortInfo> #include "serialworker.h"#define tc(a) QString::fromLocal8Bit(a)QT_BEGIN_NAMESPACE namespace Ui { class MainWindow; } QT_END_NAMESPACEclass MainWindow : public QMainWindow {Q_OBJECTpublic:MainWindow(QWidget *parent = nullptr);~MainWindow();protected:void closeEvent(QCloseEvent *e)override;void initStyle();private slots:void on_startButton_clicked(); // 点击启动按钮槽函数void on_sendButton_clicked(); // 点击发送按钮槽函数void on_stopButton_clicked(); // 点击停止按钮槽函数void handleDataReceived(const QByteArray &data); // 处理接收到的数据槽函数void handleError(const QString &error); // 处理错误槽函数void populateSerialPorts(); // 自动检索可用串口并填充到下拉列表private:Ui::MainWindow *ui;SerialWorker *serialWorker; // 串口工作类对象QThread *workerThread; // 工作线程对象 };#endif // MAINWINDOW_H
#include "mainwindow.h" #include "ui_mainwindow.h" #include <QMessageBox> #include <QSerialPortInfo> #include <QDebug> #include <QFile> #include <QDateTime> MainWindow::MainWindow(QWidget *parent): QMainWindow(parent), ui(new Ui::MainWindow), serialWorker(new SerialWorker), workerThread(new QThread(this)) {ui->setupUi(this);// 将 SerialWorker 移动到 workerThreadserialWorker->moveToThread(workerThread);// 启动工作线程workerThread->start();// 连接信号和槽connect(serialWorker, &SerialWorker::dataReceived, this, &MainWindow::handleDataReceived);connect(serialWorker, &SerialWorker::errorOccurred, this, &MainWindow::handleError);// 预填充常用波特率ui->baudRateComboBox->addItems({"9600", "115200", "38400", "19200", "57600"});// 自动检索串口并填充到下拉列表populateSerialPorts();initStyle(); }MainWindow::~MainWindow() {workerThread->quit(); // 退出工作线程workerThread->wait(); // 等待线程完全退出delete serialWorker; // 删除串口工作类对象delete ui; } void MainWindow::initStyle() {//加载样式表QString qss;QFile file(":/qss/psblack.css");if (file.open(QFile::ReadOnly)) { #if 1//用QTextStream读取样式文件不用区分文件编码 带bom也行QStringList list;QTextStream in(&file);//in.setCodec("utf-8");while (!in.atEnd()) {QString line;in >> line;list << line;}qss = list.join("\n"); #else//用readAll读取默认支持的是ANSI格式,如果不小心用creator打开编辑过了很可能打不开qss = QLatin1String(file.readAll()); #endifQString paletteColor = qss.mid(20, 7);qApp->setPalette(QPalette(paletteColor));qApp->setStyleSheet(qss);file.close();}} void MainWindow::closeEvent(QCloseEvent *e) {on_stopButton_clicked();QMainWindow::closeEvent(e); }// 自动检索系统可用的串口并填充到ComboBox中 void MainWindow::populateSerialPorts() {ui->portNameComboBox->clear(); // 清空现有的串口列表// 获取可用串口列表并添加到ComboBox中const QList<QSerialPortInfo> serialPorts = QSerialPortInfo::availablePorts();for (const QSerialPortInfo &info : serialPorts) {ui->portNameComboBox->addItem(info.portName());}// 如果没有可用串口,提示警告if (ui->portNameComboBox->count() == 0) {QMessageBox::warning(this, tc("警告"), tc("未检测到可用的串口"));} }// 启动串口操作 void MainWindow::on_startButton_clicked() {QString portName = ui->portNameComboBox->currentText(); // 从 ComboBox 中获取串口名int baudRate = ui->baudRateComboBox->currentText().toInt(); // 从 ComboBox 中获取波特率// 使用 QMetaObject::invokeMethod 来确保在工作线程中启动串口QMetaObject::invokeMethod(serialWorker, "startSerialPort", Qt::QueuedConnection,Q_ARG(QString, portName), Q_ARG(int, baudRate)); }// 发送数据到串口 void MainWindow::on_sendButton_clicked() {QByteArray data;if(ui->radioSendHEX->isChecked()){QString input = ui->sendDataEdit->text().remove(QRegExp("\\s")); // Remove all spacesdata = QByteArray::fromHex(input.toLocal8Bit()); // Convert the cleaned string to QByteArray}else{data=(ui->sendDataEdit->text().toLocal8Bit());}// 使用 QMetaObject::invokeMethod 来确保在工作线程中发送数据QMetaObject::invokeMethod(serialWorker, "handleWriteData", Qt::QueuedConnection,Q_ARG(QByteArray, data));QString msg;msg.append(QDateTime::currentDateTime().toString("hh:mm:ss.(zzz) "));msg.append(tc("发送 "));msg.append(ui->radioSendHEX->isChecked()? data.toHex(' ').toUpper():QString::fromLocal8Bit(data));ui->receiveDataEdit->append(msg);}// 停止串口操作 void MainWindow::on_stopButton_clicked() {// 使用 QMetaObject::invokeMethod 来确保在工作线程中关闭串口QMetaObject::invokeMethod(serialWorker, "stopSerialPort", Qt::QueuedConnection); }// 处理串口接收到的数据 void MainWindow::handleDataReceived(const QByteArray &data) {QString msg;msg.append(QDateTime::currentDateTime().toString("hh:mm:ss.(zzz) "));msg.append(tc("接收 "));msg.append(ui->radioRecvHex->isChecked()? data.toHex(' ').toUpper():QString::fromLocal8Bit(data));ui->receiveDataEdit->append(msg);}// 处理串口错误 void MainWindow::handleError(const QString &error) {QMessageBox::critical(this, tc("错误"), error); // 显示错误信息 }