项目简介
本项目通过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转换成QString QString 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