项目简介
本项目通过QT框架设计一款可以在Windows、Linux等平台的跨平台串口助手,串口功能能够满足基本的调试需求。
本项目采用的版本为:QT5.14 + visual studio 2022 进行开发。
项目源码:https://github.com/say-Hai/MyCOMDemo
项目页面:
一、创建开发环境
打开vs新建工程,选择创建Qt Widgets Application
项目,选择保存路径后,配置QT的SerialPort模块。
二、配置ui界面
打开工程的ui文件,设置本项目的ui页面(可直接从本项目的ui文件中copy到自己的项目中;但是注意:需要暂时把
comboBoxNo_2
降级成普通QComboBox
)
三、编写串口扫描代码
通过
QSerialPortInfo::availablePorts
生成可用串口列表,(目前暂定在MyCOM.h的构造函数中编写串口列表函数)
MyCOM::MyCOM(QWidget* parent): QMainWindow(parent)
{ui.setupUi(this);//创建串口列表QStringList comPort;foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts()){comPort << info.portName();}ui.comboBoxNo_2->addItems(comPort);
}
四、“打开串口”按钮设计
vs中无法使用Qt Creator的“转到槽”功能,因此需要开发者自己绑定槽函数;具体操作步骤为:https://www.cnblogs.com/ybqjymy/p/17999513
注:解决vs + qt 导致的乱码问题:出现中文的文件首行加上#pragma execution_character_set("utf-8")
当我们绑定好槽函数on_pushButtonOpen_clicked()
,接下来就是实现串口打开逻辑:以下为具体代码
//Map定义代码查看源文件
void MyCOM::on_pushButtonOpen_clicked()
{QSerialPort::BaudRate CombaudRate;QSerialPort::DataBits ComdataBits;QSerialPort::StopBits ComstopBits;QSerialPort::Parity ComParity;QString selectedBaudRate = ui.comboBoxComBaud_2->currentText();std::cout << selectedBaudRate.toStdString() << "\n";if (baudRateMap.contains(selectedBaudRate)) {CombaudRate = baudRateMap[selectedBaudRate];}else {// 如果用户选择了一个未知的波特率,可以设置默认值或提示错误CombaudRate = QSerialPort::Baud9600; // 默认值qWarning("Invalid baud rate selected. Defaulting to 9600.");}
//具体代码查看源文件// 根据用户选择设置数据位// 根据用户选择设置停止位// 根据用户选择设置校验方式//初始化串口MyCom.setBaudRate(CombaudRate);MyCom.setDataBits(ComdataBits);MyCom.setStopBits(ComstopBits);MyCom.setParity(ComParity);MyCom.setPortName(spTxt);//打开串口if (ui.pushButtonOpen_2->text() == "打开串口"){bool ComFlag;ComFlag = MyCom.open(QIODevice::ReadWrite);if (ComFlag == true)//串口打开成功{//串口下拉框设置为不可选ui.comboBoxCheck_2->setEnabled(false);//具体代码查看源文件//使能相应按钮等ui.pushButtonSend_2->setEnabled(true);//具体代码查看源文件ui.pushButtonOpen_2->setText(" 关闭串口 ");}else{QMessageBox::critical(this, "错误提示", "串口打开失败,该端口可能被占用或不存在!rnLinux系统可能为当前用户无串口访问权限!");}}else{MyCom.close();ui.pushButtonOpen_2->setText(" 打开串口 ");//具体代码查看源文件//使相应的按钮不可用ui.pushButtonSend_2->setEnabled(false);具体代码查看源文件}
}
五、串口数据发送与接收
通过信号槽机制,在发送区发送数据,通过
&QIODevice::readyRead
信号来通知接收区函数&MyCOM::MyComRevSlot
打印串口发送的数据
代码逻辑:
-
信号槽逻辑:当串口有数据可以读取时,自动响应
MyComRevSlot
函数。connect(&MyCom, &QIODevice::readyRead, this, &MyCOM::MyComRevSlot);
-
发送区代码逻辑:通过第四步中的“转到槽”机制,在发送按钮上绑定槽函数
on_pushButtonSend_clicked()
,再槽函数中接收发送区字符并通过MyCom.write(comSendData)
发送到串口。- 其中16进制发送需要将字符串格式化成16进制
QByteArray::fromHex(SendTemp.toUtf8()).data();
//精简版,少了一些单选框的逻辑判断 void MyCOM::on_pushButtonSend_clicked() {QByteArray comSendData;QString SendTemp;int temp;//读取发送窗口数据SendTemp = ui.TextSend_2->toPlainText();//判断发送格式,并格式化数据if (ui.checkBoxSendHex_2->checkState() != false)//16进制发送{comSendData = QByteArray::fromHex(SendTemp.toUtf8()).data();//获取字符串}temp = MyCom.write(comSendData); }
- 其中16进制发送需要将字符串格式化成16进制
-
接收区代码逻辑:通过信号槽机制来调用
MyComRevSlot
函数,利用MyCom.readAll()
读取串口的数据,最后显示到文本框内。//精简版 void MyCOM::MyComRevSlot() {QByteArray MyComRevBUff;//接收数据缓存QString StrTemp, StrTimeDate, StrTemp1;//读取串口接收到的数据,并格式化数据MyComRevBUff = MyCom.readAll();StrTemp = QString::fromLocal8Bit(MyComRevBUff);curDateTime = QDateTime::currentDateTime();StrTimeDate = curDateTime.toString("[yyyy-MM-dd hh:mm:ss.zzz]");StrTemp = MyComRevBUff.toHex().toUpper();//转换为16进制数,并大写for (int i = 0; i < StrTemp.length(); i += 2)//整理字符串,即添加空格{StrTemp1 += StrTemp.mid(i, 2);StrTemp1 += " ";}//添加时间头StrTemp1.prepend(StrTimeDate);StrTemp1.append("\r\n");//后面添加换行ui.TextRev_2->insertPlainText(StrTemp1);//显示数据ui.TextRev_2->moveCursor(QTextCursor::End);//光标移动到文本末尾 }
六、周期循环发送指令
通过定时器,实现周期性指令发送功能
-
创建定时器
QTimer* PriecSendTimer;
-
在构造函数中注册定时器超时connect函数,调用
on_pushButtonSend_clicked()
connect(PriecSendTimer, &QTimer::timeout, this, [=]() {on_pushButtonSend_clicked(); });
-
通过信号槽机制,绑定选择框状态变化信号处理函数
-
编写选择框变化处理函数
void MyCOM::on_checkBoxPeriodicSend_stateChanged(int arg1) {if (arg1 == false){PriecSendTimer->stop();ui.lineEditTime->setEnabled(true);}else{PriecSendTimer->start(ui.lineEditTime->text().toInt());ui.lineEditTime->setEnabled(false);} }
七、接收流量统计及状态栏设计
通过设计状态栏来实时展示QLabel的相关数据
-
自定义变量
//添加自定义变量long ComSendSum, ComRevSum;//发送和接收流量统计变量QLabel* qlbSendSum, * qlbRevSum;//发送接收流量label对象QLabel* myLink, * MySource;
-
变量绑定状态栏
//创建底部状态栏及其相关部件 QStatusBar* STABar = statusBar();qlbSendSum = new QLabel(this); qlbRevSum = new QLabel(this); myLink = new QLabel(this); MySource = new QLabel(this); myLink->setMinimumSize(90, 20);// 设置标签最小大小 MySource->setMinimumSize(90, 20); qlbSendSum->setMinimumSize(100, 20); qlbRevSum->setMinimumSize(100, 20); ComSendSum = 0; ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum); setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);STABar->addPermanentWidget(qlbSendSum);// 从右往左依次添加 STABar->addPermanentWidget(qlbRevSum); STABar->addWidget(myLink);// 从左往右依次添加 STABar->addWidget(MySource);myLink->setOpenExternalLinks(true);//状态栏显示官网、源码链接 myLink->setText("<style> a {text-decoration: none} </style> <a href=\"http://8.134.156.7/\">--个人博客--"); MySource->setOpenExternalLinks(true); MySource->setText("<style> a {text-decoration: none} </style> <a href=\"https://github.com/say-Hai/MyCOMDemo\">--源代码--");
-
自定义函数来更改自定义变量
void MyCOM::setNumOnLabel(QLabel* lbl, QString strS, long num) {QString strN = QString("%1").arg(num);QString str = strS + strN;lbl->setText(str); }
-
在发送/接收函数中调用自定义函数
//发送 temp = MyCom.write(comSendData); ComSendSum++; setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);//接收 MyComRevBUff = MyCom.readAll(); StrTemp = QString::fromLocal8Bit(MyComRevBUff); ComRevSum++; setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
八、数据区清空功能
void MyCOM::on_pushButtonClearRev_clicked()
{ui.TextRev_2->clear();ComSendSum = 0;ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
}void MyCOM::on_pushButtonClearSend_clicked()
{ui.TextSend_2->clear();ComSendSum = 0;ComRevSum = 0;setNumOnLabel(qlbSendSum, "Tx: ", ComSendSum);setNumOnLabel(qlbRevSum, "Rx: ", ComRevSum);
}
九、文件保存与读取功能
通过文件的读取快速实现对串口发送数据,通过写入文件的方式保存串口的输出。
-
读取文件:通过
QFile aFile(aFileName);QByteArray text = aFile.readAll();
来获取文本数据,并写入到文本框中。//首先创建on_pushButtonRdFile_clicked信号槽机制打开文件夹选择文件路径 void MyCOM::on_pushButtonRdFile_clicked() {QString curPath = QDir::currentPath();QString dlgTitle = "打开一个文件"; //对话框标题QString filter = "文本文件(*.txt);;所有文件(*.*)"; //文件过滤器QString aFileName = QFileDialog::getOpenFileName(this, dlgTitle, curPath, filter);if (aFileName.isEmpty())return;openTextByIODevice(aFileName); } //通过openTextByIODevice来读取文件 bool MyCOM::openTextByIODevice(const QString& aFileName) {QFile aFile(aFileName);if (!aFile.exists()) //文件不存在return false;if (!aFile.open(QIODevice::ReadOnly | QIODevice::Text))return false;QByteArray text = aFile.readAll();QString strText = byteArrayToUnicode(text);//编码格式转换,防止GBK中文乱码ui.TextSend_2->setPlainText(strText);aFile.close();return true; } //其中防止编码格式问题,通过byteArrayToUnicode进行编码格式转换 QString MyCOM::byteArrayToUnicode(const QByteArray& array) {QTextCodec::ConverterState state;// 先尝试使用utf-8的方式把QByteArray转换成QStringQString text = QTextCodec::codecForName("UTF-8")->toUnicode(array.constData(), array.size(), &state);// 如果转换时无效字符数量大于0,说明编码格式不对if (state.invalidChars > 0){// 再尝试使用GBK的方式进行转换,一般就能转换正确(当然也可能是其它格式,但比较少见了)text = QTextCodec::codecForName("GBK")->toUnicode(array);}return text; }
-
写入文件:选择文件路径->调用
aFile.write(strBytes, strBytes.length());
写入文件void MyCOM::on_pushButtonSaveRev_clicked() {QString curFile = QDir::currentPath();QString dlgTitle = " 另存为一个文件 "; //对话框标题QString filter = " 文本文件(*.txt);;所有文件(*.*);;h文件(*.h);;c++文件(*.cpp) "; //文件过滤器QString aFileName = QFileDialog::getSaveFileName(this, dlgTitle, curFile, filter);if (aFileName.isEmpty())return;saveTextByIODevice(aFileName); } bool MyCOM::saveTextByIODevice(const QString& aFileName) {QFile aFile(aFileName);if (!aFile.open(QIODevice::WriteOnly | QIODevice::Text))return false;QString str = ui.TextRev_2->toPlainText();//整个内容作为字符串QByteArray strBytes = str.toUtf8();//转换为字节数组aFile.write(strBytes, strBytes.length()); //写入文件aFile.close();return true; }
十、多行发送功能
通过信号槽机制和定时器功能,实现对多行数据选择的循环发送
具体逻辑:根据选择框的状态确定定时器状态->通过定时器超时函数唤醒发送事件->在发送事件中确定此次需要发送的行数据->调用对应发送按钮函数
-
通过选择框的状态变化来打开/关闭定时器发送
void MyCOM::on_checkBoxMuti_stateChanged(int arg) {if (!arg){PriecSendTimer->stop();//关闭定时器ui.lineEditTime->setEnabled(true);//使能对话框编辑}else{LastSend = 0;//从第一行开始发送ui.checkBoxPeriodicSend->setChecked(false);PriecSendTimer->start(ui.lineEditTime->text().toInt());ui.lineEditTime->setEnabled(false);//关闭对话框编辑} }
-
重构定时器超时响应函数,适配多行重复发送功能
connect(PriecSendTimer, &QTimer::timeout, this, [=]() {Pre_on_pushButtonSend_clicked(); });void MyCOM::Pre_on_pushButtonSend_clicked() {if (ui.checkBoxPeriodicMutiSend_2->isChecked() == true){while (LastSend < 10){if (checkBoxes[LastSend]->isChecked()){//发送对应行的数据on_pushButtonMuti_clicked(++LastSend);break;}LastSend++;}if (LastSend == 10){LastSend = 0;}}else{//普通发送on_pushButtonSend_clicked();} }
-
通过行索引触发对应的点击事件
void MyCOM::on_pushButtonMuti_clicked(int lineEditIndex) {QString Strtemp;switch (lineEditIndex) {case 1:Strtemp = ui.lineEditMuti1_2->text();break;case 2:Strtemp = ui.lineEditMuti2_2->text();break;//...后面对应的操作default:return; // 默认情况下不做任何操作}ui.TextSend_2->clear();ui.TextSend_2->insertPlainText(Strtemp);ui.TextSend_2->moveCursor(QTextCursor::End);MyCOM::on_pushButtonSend_clicked(); }
十一:自动刷新串口下拉框
实现方法:新建一个类继承
QComboBox
类,重写鼠标点击事件使其调用扫描端口函数 -
新建
mycombobox
类,继承QComBox#include <QComboBox> #include <QMouseEvent> #include <QSerialPort> #include <QSerialPortInfo>class mycombobox : public QComboBox {Q_OBJECT public:explicit mycombobox(QWidget* parent = nullptr);void mousePressEvent(QMouseEvent* event) override; signals: private:void scanActivatePort(); };
-
重写扫描函数和鼠标点击函数
mycombobox::mycombobox(QWidget* parent) : QComboBox(parent) {scanActivatePort(); }void mycombobox::mousePressEvent(QMouseEvent* event) {if (event->button() == Qt::LeftButton){scanActivatePort();showPopup();} }void mycombobox::scanActivatePort() {clear();//创建串口列表QStringList comPort;foreach(const QSerialPortInfo & info, QSerialPortInfo::availablePorts()){QString serialPortInfo = info.portName() + ": " + info.description();// 串口设备信息,芯片/驱动名称comPort << serialPortInfo;}this->addItems(comPort); }
-
最后将
comboBoxNo_2
组件提升为mycombobox
类
到此整个软件设计完毕
END:信号槽绑定图
参考文献:
[1] https://rymcu.com/portfolio/40