QT5串口多线程--派生类加moveToThread
- Chapter1 QT5串口多线程--派生类加moveToThread
- 前言
- 新建工程
- 源码
- serialobject.h
- serialobject.cpp
- manager.h
- manager.cpp
- widget.h
- widget.cpp
- 测试
- Chapter2 QT在PC开发中多串口通信有哪些方法
- 方法
- 实现
- 方案一:
- 需要注意的是:
- 方案二:
- 方案三:
- 方案四:
- Chapter3 Qt串口助手开发:基于多线程moveToThread方法串口通信工具
- 1. 项目背景与设计思路
- 2. 串口助手的主要功能
- 3. SerialWorker类:串口操作的后台处理
- 4.MainWindow 类:用户界面的交互逻辑
- Chapter4 QT学习笔记QserialPort类学习(二):QSerialPort的成员函数
Chapter1 QT5串口多线程–派生类加moveToThread
原文链接:https://blog.csdn.net/weixin_42968757/article/details/109530944
前言
之前讲过继承QThread,在虚函数run()中实现线程里的工作。这是qt4.6之前的方法,目前官方推荐的方法,是继承QObject类,再用方法moveToThread来将QObject派生类移动到新线程中去,这样QObject派生类定义的信号和槽的事件响应行为,都会发生在新的线程中。
这个方法非常简单。下面还是以串口通讯作为例子,这次不用窗口,用控制台的方式。
新建工程
打开QTcreator,新建一个Qt Console Application工程,起好工程名,选用qmake作为编译工具,选择Desktop MinGW 64-bit作为编译器。
ps:选择编译器时,会看到各种各样的编译器,如MSVC,MinGW等,这取决于在安装Qt时你的选择。在选择编译器时还会看到,Desktop和UWP两种平台的选择,Desktop很好解释,就是桌面程序,UWP全称是Universal Windows Platform,即Windows通用应用平台,只支持Window10以上系统,如Windows应用商店上的应用都是UWP平台的。
源码
使用QSerialPort首先得在pro文件中,增加serialport模块
QT +=serialport
在serialobject头文件中,我们定义了serialobject类,继承QObject类,这个serialobject类后面我们要通过moveToThread方法放进线程里,在QObject类new的对象和定义的信号和槽都将运行在新的线程中。
在使用moveToThread方法后,所有与信号关联的槽的运行都在新的线程中,但没有通过信号与槽方式的函数,都属于调用他的线程,比如构造函数。
我们的QserialPort对象是我们用来实际操作和设置串口的对象,设置为private类型,这样将所有的串口操作都放在了serialobject类里,多个串口就new多个serialobject类,可以方便创建多个线程操作。
serialobject.h
#ifndef SERIALOBJECT_H
#define SERIALOBJECT_H#include <QObject>
#include <QThread>
#include <QSerialPort> //提供访问串口的功能
#include <QSerialPortInfo> //提供系统中存在的串口的信息
#include <QTimer>
#include <QDebug>class serialobject : public QObject
{Q_OBJECT
public:explicit serialobject(QObject *parent = nullptr);~serialobject();public:void senddata(QByteArray &msg);private:QSerialPort* myQSerialPort = nullptr;QTimer* timer = nullptr; //定时器QByteArray ba_received;QTimer* pTimerRecv = nullptr; //接收延时定时器,10k数据(115200bps),20ms完整接收没有问题/* 一、问题描述在串口通信时,会经常遇到,在接收一帧数据时,QSerialPort::readyRead()会触发多次,即接收一包数据需要多次接收才能完整得到数据帧。二、解决思路延迟接收: QSerialPort::readyRead()触发时不要立刻去接收数据,而是等待readyRead的最后一次触发时读数据。如何等待最后一次readyRead: 用一个单触发定时器,readyRead触发时启动定时器,当定时器timeout(),可以认为是最后一次readyRead()。*/public slots:
//串口初始化、单独打开、关闭等操作槽函数void slot_init(); //在init中设置了波特率等信息后,打开串口,串口打开成功的话,将数据readyRead信号和自定义的槽函数receivedata函数关联起来void slot_openSerialPort(QString portname, int baudrate); //打开串口void slot_closeSerialPort(); //关闭串口
//串口发送和接收相关槽函数,通过信号与槽机制,运行在独立的线程中void slot_receivedata(); //串口接收到数据触发的槽函数,定时器重新启动,直到该函数不再触发(超过20ms),定时器触发;10K数据(115200bps条件下),20ms的延迟接收完全没问题void slot_cyclesend(); //周期性发送槽函数,通过timer定时器的槽函数绑定void slot_serialport_delay_recv_timeout(); //延迟接收: QSerialPort::readyRead()触发时不要立刻去接收数据,而是等待readyRead的最后一次触发时读数据。signals:void sig_datareceived(QByteArray msg);
};#endif // SERIALOBJECT_H
注意:增加一个延时接收定时器(单触发定时器), QTimer pTimerRecv = nullptr; //接收延时定时器,10k数据(115200bps),20ms完整接收没有问题。*
/* 一、问题描述
在串口通信时,会经常遇到,在接收一帧数据时,QSerialPort::readyRead()会触发多次,即接收一包数据需要多次接收才能完整得到数据帧。
二、解决思路
延迟接收: QSerialPort::readyRead()触发时不要立刻去接收数据,而是等待readyRead的最后一次触发时读数据。
如何等待最后一次readyRead: 用一个单触发定时器,readyRead触发时启动定时器,当定时器timeout(),可以认为是最后一次readyRead()。*///! 串口模式-数据延迟接收-保证数据完整
pTimerRecv = new QTimer(this); //接收延时定时器,10k数据(115200bps),20ms完整接收没有问题
pTimerRecv->setTimerType(Qt::PreciseTimer);
pTimerRecv->setSingleShot(true); //只触发一次
connect(pTimerRecv, &QTimer::timeout, this, &serialobject::slot_serialport_delay_recv_timeout);
为了避免跨线程创建对象的错误,我们在槽函数init中实例化了QSerialPort类,后面会将init与Qthread的started信号关联起来。
如果在构造函数实例化QSerialPort类,会导致在调用write函数时,报
QObject: Cannot create children for a parent that is in a different thread的错误。
因为write会创建对象,而QserialPort是在构造函数上创建的,属于主线程,不属于子线程。
在init中设置了波特率等信息后,打开串口,串口打开成功的话,将数据readyRead信号和自定义的槽函数receivedata函数关联起来,这样只要串口有数据发来,槽函数就会将其打印到控制窗口上来。
serialobject.cpp
#include "serialobject.h"serialobject::serialobject(QObject *parent) : QObject(parent)
{qDebug()<<"serialobject created"<<QThread::currentThread(); //直接打印当前线程名称和ID
}serialobject::~serialobject()
{}void serialobject::senddata(QByteArray &msg)
{myQSerialPort->write(msg);myQSerialPort->flush();
}void serialobject::slot_receivedata()
{//! 定时器重新启动,直到该函数不再触发(超过50ms),定时器触发pTimerRecv->start(20); //10K数据(115200bps条件下),20ms的延迟接收完全没问题
}void serialobject::slot_serialport_delay_recv_timeout()
{QByteArray Recv = myQSerialPort->readAll();emit sig_datareceived(Recv);
}void serialobject::slot_cyclesend()
{QByteArray msg = "serial send: abcdefg";senddata(msg);
}/******** 为了避免跨线程创建对象的错误,我们在槽函数init中实例化了QSerialPort类,后面会将init与Qthread的started信号关联起来。* 如果在构造函数实例化QSerialPort类,会导致在调用write函数时,报
QObject: Cannot create children for a parent that is in a different thread的错误。
因为write会创建对象,而QserialPort是在构造函数上创建的,属于主线程,不属于子线程。在init中设置了波特率等信息后,打开串口,串口打开成功的话,将数据readyRead信号和自定义的槽函数receivedata函数关联起来,
这样只要串口有数据发来,槽函数就会将其打印到控制窗口上来。* ****/
void serialobject::slot_init()
{qDebug()<<"serialobject slot_init"<<QThread::currentThread(); //直接打印当前线程名称和IDmyQSerialPort = new QSerialPort();myQSerialPort->setParity(QSerialPort::NoParity); 设置奇偶校验位为0myQSerialPort->setDataBits(QSerialPort::Data8);//设置数据位为8bitmyQSerialPort->setFlowControl(QSerialPort::NoFlowControl);//设置流控制为OFFmyQSerialPort->setStopBits(QSerialPort::OneStop);//设置停止位为1myQSerialPort->setBaudRate(115200);myQSerialPort->setPortName("COM20");if(myQSerialPort->isOpen()) //如果串口已经打开了 先给他关闭了{myQSerialPort->clear();myQSerialPort->close();}if(!myQSerialPort->open(QIODevice::ReadWrite)) //用ReadWrite 的模式尝试打开串口{qDebug()<<myQSerialPort->portName()<<QString::fromLocal8Bit("串口打开失败");return;}else {timer = new QTimer(this);connect(timer, &QTimer::timeout, this, &serialobject::slot_cyclesend);timer->start(1000);connect(myQSerialPort, &QSerialPort::readyRead, this, &serialobject::slot_receivedata);qDebug()<<myQSerialPort->portName()<<QString::fromLocal8Bit("串口打开成功,波特率:")<<myQSerialPort->baudRate()<<QString::fromLocal8Bit("发送周期ms:")<<1000;//! 串口模式-数据延迟接收-保证数据完整/* 一、问题描述在串口通信时,会经常遇到,在接收一帧数据时,QSerialPort::readyRead()会触发多次,即接收一包数据需要多次接收才能完整得到数据帧。二、解决思路延迟接收: QSerialPort::readyRead()触发时不要立刻去接收数据,而是等待readyRead的最后一次触发时读数据。如何等待最后一次readyRead: 用一个单触发定时器,readyRead触发时启动定时器,当定时器timeout(),可以认为是最后一次readyRead()。*/pTimerRecv = new QTimer(this);pTimerRecv->setTimerType(Qt::PreciseTimer);pTimerRecv->setSingleShot(true); //只触发一次connect(pTimerRecv, &QTimer::timeout, this, &serialobject::slot_serialport_delay_recv_timeout);}}void serialobject::slot_openSerialPort(QString portname, int baudrate)
{myQSerialPort->setBaudRate(baudrate);myQSerialPort->setPortName(portname.toLocal8Bit());if(myQSerialPort->isOpen()) //如果串口已经打开了,先给他关闭了{myQSerialPort->clear();myQSerialPort->close();}if(!myQSerialPort->open(QIODevice::ReadWrite)) //用ReadWrite 的模式尝试打开串口{qDebug()<<myQSerialPort->portName()<<QString::fromLocal8Bit("串口打开失败");return;}timer->start(1000);}void serialobject::slot_closeSerialPort()
{if(myQSerialPort->isOpen()) //如果串口已经打开了,先给他关闭了{myQSerialPort->clear();myQSerialPort->close();timer->stop();}
}
为了方便管理多线程,我们新建了一个manager类,来控制多线程。也来控制线程间通讯的信号和槽的关系。MoveToThread方法也在这里调用。
这里的manager,我们将QThread的started的信号与serialobject的init关联起来,来实现在子线程中实例化QSerialPort类。
manager.h
#ifndef MANAGER_H
#define MANAGER_H#include <QObject>
#include "serialobject.h"/**
为了方便管理多线程,我们新建了一个manager类,来控制多线程。也来控制线程间通讯的信号和槽的关系。MoveToThread方法也在这里调用。
这里的manager,我们将QThread的started的信号与serialobject的init关联起来,来实现在子线程中实例化QSerialPort类。**/
class manager : public QObject
{Q_OBJECT
public:explicit manager(QObject *parent = nullptr);~manager();void thread_init(void);void openSerialport(QString portname, int baudrate); //主线程调用打开串口操作,用于触发【void sig_openSerialPort(QString portname, int baudrate); //发送串口参数及打开串口的信号】serialobject* SObject = nullptr;QThread* SerialThread = nullptr;signals:void sig_ReceiveData(QByteArray msg);void sig_openSerialPort(QString portname, int baudrate); //发送串口参数及打开串口的信号};#endif // MANAGER_H
manager.cpp
#include "manager.h"manager::manager(QObject *parent) : QObject(parent)
{}manager::~manager()
{SerialThread->quit(); // 退出工作线程SerialThread->wait(); // 等待线程完全退出SerialThread->deleteLater();delete SObject; // 删除串口工作类对象
}void manager::thread_init()
{//serial receive&send threadSObject = new serialobject();SerialThread = new QThread();SObject->moveToThread(SerialThread);connect(SerialThread, &QThread::started, SObject, &serialobject::slot_init);connect(SObject, &serialobject::sig_datareceived, this, &manager::sig_ReceiveData);connect(this, &manager::sig_openSerialPort, SObject, &serialobject::slot_openSerialPort);SerialThread->start();
}void manager::openSerialport(QString portname, int baudrate)
{emit sig_openSerialPort(portname, baudrate);
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H#include <QWidget>
#include <QDateTime>
#include "manager.h"QT_BEGIN_NAMESPACE
namespace Ui { class Widget; }
QT_END_NAMESPACEclass Widget : public QWidget
{Q_OBJECTpublic:Widget(QWidget *parent = nullptr);~Widget();void findSerialPorts();
private slots:void on_pBtn_Start_clicked();void slot_serialDataReceived(QByteArray msg);void on_pBtn_openSerial_clicked();void on_pBtn_closeSerial_clicked();private:Ui::Widget *ui;manager* my_manager = nullptr;signals:};
#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"Widget::Widget(QWidget *parent): QWidget(parent), ui(new Ui::Widget)
{ui->setupUi(this);qDebug()<<"Widget:"<<QThread::currentThread();findSerialPorts();my_manager = new manager();connect(my_manager, &manager::sig_ReceiveData, this, &Widget::slot_serialDataReceived);ui->pBtn_openSerial->setEnabled(false);ui->pBtn_closeSerial->setEnabled(false);
}Widget::~Widget()
{delete ui;
}void Widget::findSerialPorts()
{//读取串口信息foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()){qDebug()<<"Name:"<<info.portName();qDebug()<<"Description:"<<info.description();qDebug()<<"Manufacturer:"<<info.manufacturer();//这里相当于自动识别串口号之后添加到了cmb,如果要手动选择可以用下面列表的方式添加进去QSerialPort serial;serial.setPort(info);if(serial.open(QIODevice::ReadWrite)){//将串口号添加到cmbui->cmbPortName->addItem(info.portName());//关闭串口等待人为(打开串口按钮)打开serial.close();}}QStringList baudList;//波特率baudList<<"9600"<<"19200"<<"38400"<<"57600"<<"115200";ui->cmbPortBaudrate->addItems(baudList);
}void Widget::on_pBtn_Start_clicked()
{my_manager->thread_init();ui->pBtn_openSerial->setEnabled(true);
}void Widget::slot_serialDataReceived(QByteArray msg)
{ui->textEdit->clear();QString date = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
// date += ": " + msg;ui->textEdit->append(msg);ui->textEdit->append(date);
}void Widget::on_pBtn_openSerial_clicked()
{QString portname = ui->cmbPortName->currentText().toLocal8Bit();int baudrate = ui->cmbPortBaudrate->currentText().toInt();my_manager->openSerialport(portname, baudrate);
}void Widget::on_pBtn_closeSerial_clicked()
{}
测试
我们已经在main中,serialobject构建函数和receivedata槽函数,放了打印当前线程的方法QThread::currentThread()。
可以看到,serialobject的构造函数和主线程属于同一个线程,串口初始化和串口接受都在同一个子线程进行。这种方法要比继承QThread的方法要简单和清晰很多,也不容易出现跨线程创建对象的问题。
Chapter2 QT在PC开发中多串口通信有哪些方法
原文链接:https://blog.csdn.net/ren365880/article/details/140384988
方法
方案一: 在单独的线程中创建并管理QSerialPort对象,但不要让该对象成为任何其他对象的子对象。在该线程中启动一个事件循环,并使用信号和槽来跨线程通信。如果您的应用程序涉及到复杂的串口通信,并且需要处理大量的数据或保持UI的响应性,您可能需要考虑将串口通信放在单独的线程中。
方案二: 使用Qt的并发框架(如QtConcurrent)来处理与串口通信相关的数据处理任务,但让QSerialPort对象保持在主线程或专用的IO线程中。
方案三(不推荐,但可行): 为每个串口创建一个独立的线程,在每个线程中处理相应串口的通信任务。这种方法可以避免多个串口同时操作时可能出现的阻塞问题,确保每个串口的通信能够及时响应。示例代码可参考:利用 Qt 多线程机制实现双路串口数据流的接收和发送(附工程源代码) ,如果你确实需要将QSerialPort对象移动到另一个线程,请确保你完全理解Qt的线程模型,并且你能够正确地管理对象的生命周期和线程间的同步。这通常是不必要的,也是复杂的,并且容易出错。
方案四:(类似方案一,比较简单,推荐) 在主线程中持有多个QSerialPort的连接对象,QT 中的 QSerialPort 类提供了对串口操作的支持。可以创建多个 QSerialPort 对象来分别处理不同的串口。在使用时,需设置串口的参数(如波特率、数据位、停止位等),然后通过信号与槽机制来实现数据的接收和发送。例如,连接 readyRead 信号来接收数据,调用 write 函数发送数据。在Qt中,信号和槽的连接是自动的,只要对象没有被销毁,并且信号在对象生命周期内被正确发出,槽函数就会被调用。但是,如果槽函数是跨线程的,那么您需要确保信号和槽的参数是线程安全的,或者使用Qt的元对象系统(meta-object system)来进行线程间的通信。方案四的串口对象在同一个线程中,因此不需要担心这个问题。
实现
方案一:
首先,你需要一个类来封装QSerialPort,这个类将负责串口的配置(如波特率、数据位、停止位等)、打开串口、关闭串口、发送数据和接收数据。
#include <QSerialPort>
#include <QObject> class SerialPortManager : public QObject { Q_OBJECT public: SerialPortManager(QObject *parent = nullptr) : QObject(parent), serial(new QSerialPort(this)) { connect(serial, &QSerialPort::readyRead, this, &SerialPortManager::readData); } void openSerialPort(const QString &portName, int baudRate) { serial->setPortName(portName); serial->setBaudRate(baudRate); // 设置其他参数,如数据位、停止位等 if (serial->open(QIODevice::ReadWrite)) { qDebug() << "Serial port opened successfully"; } else { qDebug() << "Failed to open serial port"; } } void closeSerialPort() { if (serial->isOpen()) { serial->close(); qDebug() << "Serial port closed"; } } void writeData(const QByteArray &data) { if (serial->isOpen()) { serial->write(data); } } private slots: void readData() { QByteArray data = serial->readAll(); // 处理接收到的数据 qDebug() << "Received data:" << data; } private: QSerialPort *serial;
};
然后,你需要一个或多个线程来运行串口管理类。这可以通过继承QThread来实现,但更好的做法是使用QThread的run方法来启动一个事件循环,并在其中运行你的串口管理类。
#include <QThread> class SerialPortThread : public QThread { Q_OBJECT public: SerialPortThread(QObject *parent = nullptr) : QThread(parent), manager(new SerialPortManager(this)) {} void run() override { exec(); // 启动事件循环 } void openPort(const QString &portName, int baudRate) { manager->openSerialPort(portName, baudRate); } // 其他必要的方法... private: SerialPortManager *manager;
};
最后,在你的主程序或主窗口中,你可以创建多个SerialPortThread实例,并为每个实例配置不同的串口。
#include <QCoreApplication>
#include "SerialPortThread.h" int main(int argc, char *argv[]) { QCoreApplication a(argc, argv); SerialPortThread *thread1 = new SerialPortThread(); thread1->openPort("COM1", 9600); thread1->start(); // 实际上在SerialPortThread中已经隐式启动事件循环 // 对于额外的串口,创建更多的SerialPortThread实例 return a.exec();
}
需要注意的是:
确保在适当的时机关闭串口和线程。
考虑使用Qt的信号和槽机制来处理线程间的通信,避免直接调用线程内部的对象方法。
线程安全:确保在多个线程中访问共享资源时采取适当的同步措施。
错误处理:在实际应用中,应该添加更多的错误处理和异常捕获机制。
方案二:
没有实际的使用过
方案三:
可以查看博客中的代码,为什么这个方案不推荐:
在Qt中,将QSerialPort对象作为某个类的子对象(即使用new QSerialPort()),并且随后尝试通过moveToThread()将其移动到另一个线程中,这通常是不推荐的,也是不安全的。原因如下:
对象生命周期和所有权: 当你使用new QSerialPort()时,你正在创建一个QSerialPort对象,并将其父对象设置为当前对象。在Qt中,子对象的生命周期是由其父对象管理的。如果父对象被销毁,那么它的所有子对象也会被销毁。然而,当你尝试将子对象移动到另一个线程时,这可能会破坏Qt的对象层次结构和生命周期管理机制。
线程亲和性: QSerialPort(以及大多数Qt的IO类)被设计为在其被创建的线程中执行IO操作。尽管你可以通过moveToThread()改变对象的线程亲和性(即它的事件处理在哪个线程中发生),但这并不意味着你可以安全地在另一个线程中执行IO操作。对于QSerialPort来说,这可能会导致未定义的行为,包括程序崩溃。
Qt的线程模型: Qt的线程模型是基于事件循环的。当你将一个对象移动到另一个线程时,你实际上是在告诉Qt将该对象的事件(如信号和槽的调用)发送到该线程的事件循环中。但是,这并不意味着对象本身可以在该线程中执行非事件驱动的IO操作。
方案四:
首先新建一个串口连接类:
#include "serialcontroller.h"SerialController::SerialController(QObject *parent) : QObject(parent){}/*** 析构函数* @brief SerialController::~SerialController*/
SerialController::~SerialController(){if (m_serialPort->isOpen()) {m_serialPort->close();delete m_serialPort;}
}/*** 初始化串口连接* @brief SerialController::initSerialPort* @param id 对象ID* @param portName 串口名*/
void SerialController::initSerialPort(int id,QString portName){m_portName = portName;m_id = id;m_serialPort = new QSerialPort(this);m_serialPort->setPortName(m_portName);m_serialPort->setBaudRate(QSerialPort::Baud9600,QSerialPort::AllDirections);m_serialPort->setDataBits(QSerialPort::Data8);//数据位为8位m_serialPort->setFlowControl(QSerialPort::NoFlowControl);//无流控制m_serialPort->setParity(QSerialPort::NoParity); //无校验位m_serialPort->setStopBits(QSerialPort::OneStop); //一位停止位if(!m_serialPort->open(QIODevice::ReadWrite)){ //用ReadWrite 的模式尝试打开串口emit sendUpdateUI(20,0,m_portName+"打开失败");//发送更新UI的信号return;}// 连接信号和槽connect(m_serialPort,&QSerialPort::readyRead,this,&SerialController::readData);}/*** 读取串口信息的槽并发送信号* @brief MainWindow::readData*/
void SerialController::readData(){emit sendUpdateUI(20,2,"读取串口数据");QByteArray buf;buf = m_serialPort->readAll();QString hexStr;if(!buf.isEmpty()){// 将接收到的字节转换为16进制字符串for (char byte : buf) {hexStr += QString("%1").arg(byte&0xff, 2, 16, QChar('0'));}}buf.clear();emit sendDataToMain(hexStr);
}/*** 发送数据* @brief SerialController::sendData* @param data 要发送的字节数组*/
void SerialController::sendData(const QByteArray &data){m_serialPort->write(data);
}
在MainWindow中新建方法和槽
/*** 初始化串口* @brief MainWindow::initSerialPort*/
void MainWindow::initSerialPort(){QStringList ports = port.split(" "); //连接指定的1到多个串口for (int i=0;i<ports.length();i++) {SerialController *con = new SerialController(this);connect(con, &SerialController::sendDataToMain, this, &MainWindow::receiveData);con->initSerialPort(i,ports[i]);serialControllers.append(con);//把对象放到数组中}
}/*** 接收串口发来信息的槽* @brief MainWindow::receiveData* @param data 串口信息*/
void MainWindow::receiveData(const QString &data){//接收到串口发来的信息后进行业务上的处理
}
在对串口发送信息时,可从数组中循环出来,可以群发,也可以根据自定的ID判断对指定的接口发
foreach(SerialController* con,serialControllers){con->sendData(QByteArray::fromHex(data));
}
Chapter3 Qt串口助手开发:基于多线程moveToThread方法串口通信工具
原文链接:https://blog.csdn.net/chenai886/article/details/142335803
介绍了一个基于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); // 显示错误信息
}
Chapter4 QT学习笔记QserialPort类学习(二):QSerialPort的成员函数
原文链接
本文主要参考的是官方手册,力争写一个可信的,详尽可查的QserialPort类学习手册。
bool QSerialPort::flush();
这个函数的功能是将内部写缓存中的数据写入到尽可能多地写入到串口中去,而且是无阻塞的。如果写入了任何数据,那么函数返回true,否则就返回false。
调用这个函数就立刻把缓存的数据发到串口。成功写入到串口的数据量取决于操作系统。在大多数情况下,这个函数不需要被调用,因为QSerialPort类会在控件返回到事件循环的时候自动开始发送数据。在非事件循环情况下,可以调用waitForBytesWritten()函数来代替。
注意:使用flush()函数的时候,串口必须打开,否则会报错。
void QSerialPort::setReadBufferSize(qint64 size);
设置QSerialPort的内部读缓存大小为参数“size”的大小。
如果这个缓存的大小被限制在了某个确定值上,QSerialPort就会严格执行,不会缓存超过其大小的数据。有一种特殊情况,缓存大小是0(默认值就是0),这就意味着读缓存大小没有限制,并且所有进来的数据都会被缓存下来。
这个函数在以下情况下是非常有用的:
1、数据只是在某个特定的时间点来读取(比如:在实时流应用上);
2、保护串口,防止接收过多的数据,因为某些情况下这可能会导致应用超出内存。
[virtual] qint64 QSerialPort::bytesAvailable() const;
这个函数是QIODevice::bytesAvailable()的继承和重载。这个函数会返回串口收到的那些等待读取的数据字节数。