前言
今天分享一个用QT写的串口助手,关键代码会直接在文章的对应位置贴出,完整的工程文件(用的VS 2019)可以进入我的主页免费下载,也可以关注我的公众号“折途想要敲代码” 回复关键词“qt串口助手”免费获取。
如果是使用QTCreator的小伙伴在项目配置完毕后可以通过复制我提供的工程文件中的.cpp和.h文件来达到同样的效果。
要配置的就是在配置文件中加上串口对应的部分。
使用VS的需要再拓展插件中找到模块管理再加上串口的模块。
需要包含以下头文件。
#include <QtWidgets/QMainWindow>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QTimer>
#include <QMessageBox>#include <QSerialPortInfo>
#include <QSerialPort>#include <qdebug.h>
#include <qvector.h>
布局
写一个应用最重要的就是外观,也就是前端,因为用户是看不见你程序的逻辑的,因此我们首先需要做的就是先把串口助手的外观设计好。
说QT简单的一个很重要的因素就是QT自带一个图形化界面设计(QTDesign),不得不说使用图形化界面去设计界面确实又快又好。不过我个人不喜欢,因为“折途想要敲代码”。
从零开始写一个串口助手还是挺困难的,因此界面我参考的b站江科大自化协提供的串口助手。
从上图可以知道我们需要往串口助手里添加的组件就四种:两个可编辑内容展示内容的容器,5个按钮,7个用来配置串口通信的多选项以及对应描述功能的文字标签。
发送区和接收区的组件我们选用QPlainTextEdit。
按钮使用QPushButton。
多选项使用QComboBox。
文字标签自然使用的是QLabel。
有了布局的信息之后只需要将对应组件创建之后修改大小以及摆放位置即可。
接收区
接收区是接收串口通信传来的数据的,因此我们可以直接设为只读模式。
接收区还配一个按钮“清空接收区”,我们搭配一个信号槽,按下后直接调用QPlainTextEdit自带的clear函数来清空即可。
//接收区初始化
void Serial::ReceiveAeraInit(QWidget* parent) {receiveAera = new QPlainTextEdit(parent);receiveAera->setFixedSize(800,400);receiveAera->move(30,20);receiveAera->setReadOnly(true); //接收区改为只读QPushButton* clearReceive = new QPushButton(QString::fromLocal8Bit("清空接收区"),parent);clearReceive->setFixedSize(150,50);clearReceive->move(680,430);//为清空接收区设置信号槽QObject::connect(clearReceive, &QPushButton::clicked, [&]() {receiveAera->clear();});
}
串口设置
我们使用QComboBox多选项组件来配置串口的设置。
需要配置的是串口号,这个不是固定的,是根据用户连接到电脑的串口来决定的,因此需要动态地获取,我把这一块放在了定时器里,每1000ms扫描一次更新串口号选项。
我们使用QT自带的QSerialPortInfo来获取串口号,把可用的串口号取出,和之前串口号选项来作比较,如果可用的串口出现变化了再更新,如果一直更新的话后面插入的串口会选择不到。
如果获取不到可用串口的话就检查串口是否正确连接,并且检查是否安装了对应的驱动,如果电脑的设备管理器能够看到串口的话,这里也是可以获取的到的。
//刷新可用串口
void Serial::RefreshPort(void) {QVector<QString>temp;//获取当前可用串口号for (const QSerialPortInfo& info : QSerialPortInfo::availablePorts()) {temp.push_back(info.portName());}//排序现有的串口号,用于比较和原有的差距qSort(temp.begin(), temp.end());if (temp != this->ports) { //如果可用串口号有变化this->portNumber->clear(); //清除原有列表this->ports = temp; //更新串口列表for (auto& a : ports) { //更新新串口this->portNumber->addItem(a);}}
}
接下来是其他配置选项,其他配置都是固定选型,因此我们需要手动添加上去。
因为我主要是用来和stm32来使用的,所以选项是什么我需要参考stm32串口相关代码以及qt串口类支持的参数。
第一个是波特率。下图是QT提供的波特率,以及stm32关于波特率的参数注释。
综上,我决定给波特率三个选项,4800,9600,19200。
第二个是数据位。
QT支持5~8的数据位长度,而stm32支持8和9位,也就是没得选,只能是8位数据长度,这里可以写死也可以取消数据位长度的配置,不过为了后续功能的拓展,还是加上了数据位长度的多选项部分,只不过目前只有一个选项没得选。
第三个是停止位,qt支持1,1.5,2一共三种选项,stm32多一个0.5,因此我们使用它们的交集,也就是1,1.5,2。
最后一个是校验位,我们可以选择无校验,奇校验,偶校验。
另外两个是接收和发送的格式,可选HEX和文本。
//串口设置初始化
void Serial::SetupInit(QWidget* parent) {this->portNumber = new QComboBox(parent);this->baudRate = new QComboBox(parent);this->dataSize = new QComboBox(parent);this->stopSize = new QComboBox(parent);this->check = new QComboBox(parent);this->receiveMode = new QComboBox(parent);this->sendMode = new QComboBox(parent);this->baudRate->addItem("4800");this->baudRate->addItem("9600");this->baudRate->addItem("19200");this->dataSize->addItem("8");this->stopSize->addItem("1");this->stopSize->addItem("1.5");this->stopSize->addItem("2");this->check->addItem(QString::fromLocal8Bit("无"));this->check->addItem(QString::fromLocal8Bit("奇校验"));this->check->addItem(QString::fromLocal8Bit("偶校验"));this->receiveMode->addItem(QString::fromLocal8Bit("HEX"));this->receiveMode->addItem(QString::fromLocal8Bit("文本"));this->sendMode->addItem(QString::fromLocal8Bit("HEX"));this->sendMode->addItem(QString::fromLocal8Bit("文本"));QLabel* portLabel = new QLabel(QString::fromLocal8Bit("串口号"), parent);QLabel* baudLabel = new QLabel(QString::fromLocal8Bit("波特率"),parent);QLabel* dataLabel = new QLabel(QString::fromLocal8Bit("数据位"),parent);QLabel* stopLabel = new QLabel(QString::fromLocal8Bit("停止位"),parent);QLabel* checkLabel = new QLabel(QString::fromLocal8Bit("校验位"),parent);QLabel* receiveModeLabel = new QLabel(QString::fromLocal8Bit("接收格式"),parent);QLabel* sendModeLabel = new QLabel(QString::fromLocal8Bit("发送格式"),parent);QVector<QComboBox*>setups;setups.push_back(portNumber);setups.push_back(baudRate);setups.push_back(dataSize);setups.push_back(stopSize);setups.push_back(check);setups.push_back(receiveMode);setups.push_back(sendMode);QVector<QLabel*>labels;labels.push_back(portLabel);labels.push_back(baudLabel);labels.push_back(dataLabel);labels.push_back(stopLabel);labels.push_back(checkLabel);labels.push_back(receiveModeLabel);labels.push_back(sendModeLabel);for (int i = 0; i < setups.size(); ++i) {setups[i]->setFixedSize(200, 50);setups[i]->move(850, 20 + i * 80);labels[i]->move(1080,25+i*80);}}
串口核心代码
串口连接和断开我们分别需要两个按钮,江科大用的是同一个按钮,不过我用两个(不是做不到那样的效果,而是我觉得两个按钮表示两种功能比较合适)。
布局完之后就设置信号槽,也就是点击按钮之后的逻辑了。
首先一开始没有连接串口,所以先把断开连接的按钮失效,也就是点不了,并且无法发送数据,因此在发送区的发送按钮也需要失效。
在点击串口连接的按钮之后,判断是否有串口号,而且也只需要判断是否有串口号即可,因为其他配置都是肯定会有并且是合法的。
没有串口号的话那就没有任何反应。
如果有串口号的话就使串口连接的按钮失效,使断开连接按钮和发送区的发送按钮生效。
//串口连接
void Serial::BeginUSART(QWidget* parent) {startUSART = new QPushButton(QString::fromLocal8Bit("串口连接"),parent);endUSART = new QPushButton(QString::fromLocal8Bit("断开连接"),parent);endUSART->setFixedSize(150, 50);endUSART->move(1000, 600);startUSART->setFixedSize(150, 50);startUSART->move(850,600);endUSART->setDisabled(true); //一开始没有连接串口,因此关闭按钮初始化为无效//为关闭连接按钮配置信号槽QObject::connect(endUSART, &QPushButton::clicked, [&]() {endUSART->setDisabled(true); //使关闭连接按钮失效startUSART->setDisabled(false); //使连接按钮生效sendButton->setDisabled(true); //使发送按钮失效serialPort->close(); //断开串口连接});//为连接按钮配置信号槽QObject::connect(startUSART, &QPushButton::clicked, [&]() {QString port = portNumber->currentText();QString baud = baudRate->currentText();QString data = dataSize->currentText();QString stop = stopSize->currentText();QString ch = check->currentText();QString receive = receiveMode->currentText();QString send = sendMode->currentText();if (port != "") { //当串口号不为空,即有效时endUSART->setDisabled(false); //使关闭连接按钮生效sendButton->setDisabled(false); //使发送按钮生效startUSART->setDisabled(true); //使连接按钮失效USART(port,baud,data,stop,ch); //连接串口}});
}
接下来开始串口连接的逻辑。
因为比较多,写在lambda的话可读性比较差,因此我单开了一个函数。
在确认串口号有效之后,把配置选项的值传送进串口连接函数,其实不传参数,在函数里直接获取也是可以的,因为多选项的组件属于成员变量,是可以获取到的。
在串口连接函数的开始需要做的就是把选项值换成qt串口类支持的枚举类型,这里我用的if else语句,使用switch也是可以的。
转换完成之后对串口类进行配置然后连接即可。
接下来就是为串口配置信号槽,一旦有数据传来,我们就需要读取数据,并且通过选择的接收模式来对数据进行加工,如果是选了“HEX”,那么就把数据转换为16进制,如果选了“文本”,那么就转换为QString类型。
数据加工完毕之后加入接收区即可。
//串口通信核心
void Serial::USART(QString port, QString baud, QString data,QString stop,QString check) {QSerialPort::BaudRate Baud; //波特率QSerialPort::DataBits Data; //数据位QSerialPort::StopBits Stop; //停止位QSerialPort::Parity Check; //校验位if (baud == "4800") Baud = QSerialPort::Baud4800;else if (baud == "9600") Baud = QSerialPort::Baud9600;else if (baud == "19200") Baud = QSerialPort::Baud19200;if (data == "8") Data = QSerialPort::Data8;if (stop == "1") Stop = QSerialPort::OneStop;else if (stop == "1.5")Stop = QSerialPort::OneAndHalfStop;else if (stop == "2") Stop = QSerialPort::TwoStop;if (check == QString::fromLocal8Bit("无")) Check = QSerialPort::NoParity;else if (check == QString::fromLocal8Bit("奇校验")) Check = QSerialPort::OddParity;else if (check == QString::fromLocal8Bit("偶校验")) Check = QSerialPort::EvenParity;serialPort = new QSerialPort(this);//为串口设置配置serialPort->setBaudRate(Baud);serialPort->setPortName(port);serialPort->setDataBits(Data);serialPort->setParity(Check);serialPort->setStopBits(Stop);//打开串口if (serialPort->open(QSerialPort::ReadWrite)) {//配置信号槽,一旦收到数据则开始读取QObject::connect(serialPort, &QSerialPort::readyRead, [&]() {auto data = serialPort->readAll();if (receiveMode->currentText() == "HEX") { //字节模式QString hex = data.toHex(' ');receiveAera->appendPlainText(hex);}else { //文本模式QString str = QString(data);receiveAera->appendPlainText(str);}});}else {QMessageBox::critical(this, QString::fromLocal8Bit("串口打开失败"), QString::fromLocal8Bit("请确认串口是否正确连接"));}
}
发送区
最后剩个发送区,跟接收区类似,只不过多了一个发送的按钮。
发送按钮也需要配置信号槽。
当按下发送按钮的时候,我们就获取当前发送区中的数据,如果是发送格式选择了“HEX”,那我们就需要把发送区的数据按照两个数字一组的形式转换为16进制,完成的逻辑可以参考下面的代码。
如果选择的是“文本”,那么就直接把数据转成utf-8的数据格式即可。
转换完成之后就直接对串口类进行写操作就行了。
//发送区初始化
void Serial::SendAeraInit(QWidget* parent) {sendAera = new QPlainTextEdit(parent);sendAera->setFixedSize(800,100);sendAera->move(30,500);QPushButton* clearSend = new QPushButton(QString::fromLocal8Bit("清空发送区"), parent);clearSend->setFixedSize(150, 50);clearSend->move(680, 630);QObject::connect(clearSend, &QPushButton::clicked, [&]() {sendAera->clear();});sendButton = new QPushButton(QString::fromLocal8Bit("发送"), parent);sendButton->setFixedSize(150, 50);sendButton->move(500, 630);sendButton->setDisabled(true);QObject::connect(sendButton, &QPushButton::clicked, [&]() {QString data = sendAera->toPlainText();if (sendMode->currentText() == "HEX") {QByteArray arr;for (int i = 0; i < data.size(); i++){if (data[i] == ' ') continue;int num = data.mid(i, 2).toUInt(nullptr, 16); //将数据转为16进制i++;arr.append(num);}serialPort->write(arr);}else {serialPort->write(data.toLocal8Bit().data()); //转为utf-8格式字符串写入}});
}
.cpp完整代码&.h完整代码
#include "Serial.h"//串口通信核心
void Serial::USART(QString port, QString baud, QString data,QString stop,QString check) {QSerialPort::BaudRate Baud; //波特率QSerialPort::DataBits Data; //数据位QSerialPort::StopBits Stop; //停止位QSerialPort::Parity Check; //校验位if (baud == "4800") Baud = QSerialPort::Baud4800;else if (baud == "9600") Baud = QSerialPort::Baud9600;else if (baud == "19200") Baud = QSerialPort::Baud19200;if (data == "8") Data = QSerialPort::Data8;if (stop == "1") Stop = QSerialPort::OneStop;else if (stop == "1.5")Stop = QSerialPort::OneAndHalfStop;else if (stop == "2") Stop = QSerialPort::TwoStop;if (check == QString::fromLocal8Bit("无")) Check = QSerialPort::NoParity;else if (check == QString::fromLocal8Bit("奇校验")) Check = QSerialPort::OddParity;else if (check == QString::fromLocal8Bit("偶校验")) Check = QSerialPort::EvenParity;serialPort = new QSerialPort(this);//为串口设置配置serialPort->setBaudRate(Baud);serialPort->setPortName(port);serialPort->setDataBits(Data);serialPort->setParity(Check);serialPort->setStopBits(Stop);//打开串口if (serialPort->open(QSerialPort::ReadWrite)) {//配置信号槽,一旦收到数据则开始读取QObject::connect(serialPort, &QSerialPort::readyRead, [&]() {auto data = serialPort->readAll();if (receiveMode->currentText() == "HEX") { //字节模式QString hex = data.toHex(' ');receiveAera->appendPlainText(hex);}else { //文本模式QString str = QString(data);receiveAera->appendPlainText(str);}});}else {QMessageBox::critical(this, QString::fromLocal8Bit("串口打开失败"), QString::fromLocal8Bit("请确认串口是否正确连接"));}
}//刷新可用串口
void Serial::RefreshPort(void) {QVector<QString>temp;//获取当前可用串口号for (const QSerialPortInfo& info : QSerialPortInfo::availablePorts()) {temp.push_back(info.portName());}//排序现有的串口号,用于比较和原有的差距qSort(temp.begin(), temp.end());if (temp != this->ports) { //如果可用串口号有变化this->portNumber->clear(); //清除原有列表this->ports = temp; //更新串口列表for (auto& a : ports) { //更新新串口this->portNumber->addItem(a);}}
}//接收区初始化
void Serial::ReceiveAeraInit(QWidget* parent) {receiveAera = new QPlainTextEdit(parent);receiveAera->setFixedSize(800,400);receiveAera->move(30,20);receiveAera->setReadOnly(true); //接收区改为只读QPushButton* clearReceive = new QPushButton(QString::fromLocal8Bit("清空接收区"),parent);clearReceive->setFixedSize(150,50);clearReceive->move(680,430);//为清空接收区设置信号槽QObject::connect(clearReceive, &QPushButton::clicked, [&]() {receiveAera->clear();});
}//发送区初始化
void Serial::SendAeraInit(QWidget* parent) {sendAera = new QPlainTextEdit(parent);sendAera->setFixedSize(800,100);sendAera->move(30,500);QPushButton* clearSend = new QPushButton(QString::fromLocal8Bit("清空发送区"), parent);clearSend->setFixedSize(150, 50);clearSend->move(680, 630);QObject::connect(clearSend, &QPushButton::clicked, [&]() {sendAera->clear();});sendButton = new QPushButton(QString::fromLocal8Bit("发送"), parent);sendButton->setFixedSize(150, 50);sendButton->move(500, 630);sendButton->setDisabled(true);QObject::connect(sendButton, &QPushButton::clicked, [&]() {QString data = sendAera->toPlainText();if (sendMode->currentText() == "HEX") {QByteArray arr;for (int i = 0; i < data.size(); i++){if (data[i] == ' ') continue;int num = data.mid(i, 2).toUInt(nullptr, 16); //将数据转为16进制i++;arr.append(num);}serialPort->write(arr);}else {serialPort->write(data.toLocal8Bit().data()); //转为utf-8格式字符串写入}});
}//定时事件
void Serial::timerEvent(QTimerEvent* e) {RefreshPort(); //更新端口
}//串口设置初始化
void Serial::SetupInit(QWidget* parent) {this->portNumber = new QComboBox(parent);this->baudRate = new QComboBox(parent);this->dataSize = new QComboBox(parent);this->stopSize = new QComboBox(parent);this->check = new QComboBox(parent);this->receiveMode = new QComboBox(parent);this->sendMode = new QComboBox(parent);this->baudRate->addItem("4800");this->baudRate->addItem("9600");this->baudRate->addItem("19200");this->dataSize->addItem("8");this->stopSize->addItem("1");this->stopSize->addItem("1.5");this->stopSize->addItem("2");this->check->addItem(QString::fromLocal8Bit("无"));this->check->addItem(QString::fromLocal8Bit("奇校验"));this->check->addItem(QString::fromLocal8Bit("偶校验"));this->receiveMode->addItem(QString::fromLocal8Bit("HEX"));this->receiveMode->addItem(QString::fromLocal8Bit("文本"));this->sendMode->addItem(QString::fromLocal8Bit("HEX"));this->sendMode->addItem(QString::fromLocal8Bit("文本"));QLabel* portLabel = new QLabel(QString::fromLocal8Bit("串口号"), parent);QLabel* baudLabel = new QLabel(QString::fromLocal8Bit("波特率"),parent);QLabel* dataLabel = new QLabel(QString::fromLocal8Bit("数据位"),parent);QLabel* stopLabel = new QLabel(QString::fromLocal8Bit("停止位"),parent);QLabel* checkLabel = new QLabel(QString::fromLocal8Bit("校验位"),parent);QLabel* receiveModeLabel = new QLabel(QString::fromLocal8Bit("接收格式"),parent);QLabel* sendModeLabel = new QLabel(QString::fromLocal8Bit("发送格式"),parent);QVector<QComboBox*>setups;setups.push_back(portNumber);setups.push_back(baudRate);setups.push_back(dataSize);setups.push_back(stopSize);setups.push_back(check);setups.push_back(receiveMode);setups.push_back(sendMode);QVector<QLabel*>labels;labels.push_back(portLabel);labels.push_back(baudLabel);labels.push_back(dataLabel);labels.push_back(stopLabel);labels.push_back(checkLabel);labels.push_back(receiveModeLabel);labels.push_back(sendModeLabel);for (int i = 0; i < setups.size(); ++i) {setups[i]->setFixedSize(200, 50);setups[i]->move(850, 20 + i * 80);labels[i]->move(1080,25+i*80);}}//串口连接
void Serial::BeginUSART(QWidget* parent) {startUSART = new QPushButton(QString::fromLocal8Bit("串口连接"),parent);endUSART = new QPushButton(QString::fromLocal8Bit("断开连接"),parent);endUSART->setFixedSize(150, 50);endUSART->move(1000, 600);startUSART->setFixedSize(150, 50);startUSART->move(850,600);endUSART->setDisabled(true); //一开始没有连接串口,因此关闭按钮初始化为无效//为关闭连接按钮配置信号槽QObject::connect(endUSART, &QPushButton::clicked, [&]() {endUSART->setDisabled(true); //使关闭连接按钮失效startUSART->setDisabled(false); //使连接按钮生效sendButton->setDisabled(true); //使发送按钮失效serialPort->close(); //断开串口连接});//为连接按钮配置信号槽QObject::connect(startUSART, &QPushButton::clicked, [&]() {QString port = portNumber->currentText();QString baud = baudRate->currentText();QString data = dataSize->currentText();QString stop = stopSize->currentText();QString ch = check->currentText();QString receive = receiveMode->currentText();QString send = sendMode->currentText();if (port != "") { //当串口号不为空,即有效时endUSART->setDisabled(false); //使关闭连接按钮生效sendButton->setDisabled(false); //使发送按钮生效startUSART->setDisabled(true); //使连接按钮失效USART(port,baud,data,stop,ch); //连接串口}});
}Serial::Serial(QWidget *parent): QMainWindow(parent){this->setFixedSize(1200,750);this->setWindowTitle(QString::fromLocal8Bit("串口助手"));ReceiveAeraInit(this);SendAeraInit(this);SetupInit(this);BeginUSART(this);this->startTimer(1000); //开个1秒的定时器用来扫描可用串口
}Serial::~Serial(){}
#pragma once
#include <QtWidgets/QMainWindow>
#include <QPlainTextEdit>
#include <QPushButton>
#include <QComboBox>
#include <QLabel>
#include <QTimer>
#include <QMessageBox>#include <QSerialPortInfo>
#include <QSerialPort>#include <qdebug.h>
#include <qvector.h>class Serial : public QMainWindow{Q_OBJECT
public:Serial(QWidget *parent = nullptr);~Serial();void RefreshPort(void);void timerEvent(QTimerEvent* e);void ReceiveAeraInit(QWidget* parent);void SendAeraInit(QWidget* parent);void SetupInit(QWidget* parent);void BeginUSART(QWidget* parent);void USART(QString port, QString baud, QString data, QString stop, QString ch);
private:QPlainTextEdit* sendAera;QPlainTextEdit* receiveAera;QPushButton* sendButton;QComboBox* portNumber;QComboBox* baudRate;QComboBox* dataSize;QComboBox* stopSize;QComboBox* check;QComboBox* receiveMode;QComboBox* sendMode;QPushButton* startUSART;QPushButton* endUSART;QSerialPort* serialPort;QVector<QString>ports;
};