1 摘要
本文主要讲述如何使用QT从零开始实现一个串口助手的基本功能,功能如标题所示,文末附有源码供大家参考。文中若有纰漏,烦请读者斧正。
2 环境
- QT 5.14.1
- Window 11
3 功能
- 串口打开/关闭
- 启动软件时识别串口号
- 打开按键随串口打开状态而改变文字
- 打开失败弹窗提示
- 串口发送/接收
- 字符和十六进制
- 清除发送/接收
4 创建工程
- 下载QT
- 安装QT
- 创建工程
5 界面布局
5.1 用到的控件
- Buttons
- Push Button:即最常见的按键:打开串口、发送、清除接收、清除发送,都是用了这个控件
- Check Box:即用于打钩的小方框,16进制显示和16进制接收用到了此控件
- Input Widgets
- Combo Box:点击有下拉菜单的控件,串口、波特率、停止位、数据位、奇偶校验的选择用到此控件
- Text Edit:输入文本框,串口的发送内容的编辑用到此控件,串口接收内容的显示也用到此控件(此时该控件为只读)
- Display Widgets
- Label:串口、波特率、停止位、数据位、奇偶校验,这些文字的显示用到此控件
- Layouts
- Vertical Layout:垂直布局,在此布局框内的空间按垂直等间距排列
- Horizontal Layout:水平布局,在此布局框内的控件按水平等间距排列
- Form Layout:在此布局框内的控件按垂直两列等间距排列
5.2 摆放控件
将控件从左侧拖拽到窗口编辑处
5.3 控件编辑及布局
- 修改Label/Push Button/Check Box控件的文本内容(双击控件即可修改)
- 在Combo Box中添加菜单内容(双击控件即可添加)
- 串口:不加,后面通过串口号识别,在代码里添加
- 波特率:本文只添加9600/19200/38400/57600/115200
- 停止位:1/1.5/2
- 数据位:5/6/7/8
- 校验位:无/奇校验/偶校验
- 控件布局:调整控件位置,通过布局菜单对选中的控件进行布局
- 修改控件名:按控件的实际用途修改控件名
控件布局:
修改控件名:
6 添加库及头文件
本文的串口助手基于QT自带的QSerialPort类实现,故需要添加该类相关的宏和头文件,除此之外,本文用到的头文件也在此一并添加。
添加宏serialport:
添加头文件:
其中QSerialPort即QT自带的串口类,QSerialPortInfo用于获取串口号,QMessageBox用于实现弹窗提示,QDebug用于输出调试信息。
7 打开串口
7.1 槽函数
打开串口这个动作是在按下“打开串口”这个按键后进行的,因此必须建立按键跟动作之间的联系,在QT中,这种联系是通过“信号和槽”这样的机制来实现的,简单来说,“信号”就是按下按键这个事件,可以理解为一个标志,“槽”是指对这个事件所做的响应,可以理解为一个函数。
从控件转到槽函数(此处转到“按下时”的槽函数,即此函数是在按键按下时被调用):
选择clicked()后,QT将在cpp文件中生成槽函数(显然,函数里的内容是要程序员手写的,不是QT生成的):
槽函数所调用的子函数applySerialPortConfig(此函数实现获取combobox中的输入并设置到串口):
槽函数所调用的子函数setEnableSerialPortConfig(此函数实现combobox的屏蔽与打开):
“打开按键”的槽函数主要做这几件事情:
- 根据当前串口的打开状态,选择是要打开串口还是关闭串口
- 通过自定义成员变量mIsOpen实现
- 若要打开串口,则从combobox中获取配置并打开串口
- 先通过QComboBox的currentText方法获取当前输入,再把输入通过QSerialPort类的setXXX方法进行设置,再调open方法
- 若要关闭串口,则调用关闭串口函数
- 调QSerialPort类的close方法
- 串口打开失败时,弹窗提示
- 调QMessageBox类的warning方法
- 串口打开状态改变后,修改按键的文本内容
- 调QPushButton的setText方法
- 串口打开状态改变后,修改combobox的激活状态
- 调QComboBox的setText方法
注意:相关成员变量需先在头文件中定义好
7.2 串口列表的获取
前文中并没有在combobox中写死串口列表,是为了动态获取串口列表。本文只在软件打开的时候获取串口列表(更完善的做法是在点击combobox后更新,后面有时间会实现这个功能),只需在构造函数中添加以下代码。
foreach是QT中的一个关键字,其作用是对第二个参数中的对象进行遍历,把遍历过程中的每个对象依次赋给第一个参数,并执行花括号中的内容。在这里,就是把可获取的串口列表availablePorts()中的串口,逐个将其串口号添加到combobox中。
8 串口发送/接收
8.1 字符的收发
对于发送来说,其实现过程如下:
- 给发送按键创建槽函数
- 在槽函数中获取发送文本框中的数据
- 对获取到的数据进类型和格式的转换(如需)
- 发送数据到串口
代码实现:
void MainWindow::on_pushButtonSend_clicked()
{
if(mIsOpen == true) {
//mSerialPort.write(ui->textEditSend->toPlainText().toStdString().c_str()); //ENTER键:0A(即\n)
mSerialPort.write(ui->textEditSend->toPlainText().replace("\n", "\r\n").toStdString().c_str()); //ENTER键:0D 0A(即\r\n)
}
}
对于接收来说,由于不存在接收按键,其实现跟发送有些许不同,但本质还是一样的,都是QT中的信号和槽的机制:
- 通过connect方法,连接接收完成信号readyRead和自定义槽函数on_serialPort_readyRead(函数名字自定义)
- 在槽函数中读串口
- 对读到的串口数据进行类型和格式的转换(如需)
- 把数据显示在接收文本框中
代码实现:
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
, ui(new Ui::MainWindow)
{
ui->setupUi(this);
//此处省略中间内容....
//连接接收完成信号readyRead和自定义槽函数on_serialPort_readyRead
connect(&mSerialPort, SIGNAL(readyRead()), this, SLOT(on_serialPort_readyRead()));
}
void MainWindow::on_serialPort_readyRead()
{
if(mIsOpen == true) {
QByteArray rxData = mSerialPort.readAll();
ui->textEditReceive->insertPlainText(rxData); //用append会多一个换行
ui->textEditReceive->moveCursor(QTextCursor::End); //调整光标位置到最新接收的尾部,避免看不到最新接收的数据
}
}
8.2 十六进制收发
在实现了上面字符的收发基础上,再实现十六进制的收发,其实不难,无非多了显示方式的判断,以及相应格式和类型的转换罢了。这些格式和类型的转换通常都有现成的轮子,不需要再造轮子,这样节省了大量时间。
首先是checkbox控件的槽函数实现,无论是接收还是发送的16进制checkbox,其实现的都是以下两件事情:
- 根据当前checkbox状态,对文本框的数据进行字符和十六进制数之间的格式转换
- 记录当前checkbox状态(用于接收和发送函数的格式转换)
16进制接收显示checkbox的stateChanged槽函数:
void MainWindow::on_checkBoxHexDisplay_stateChanged(int arg1)
{
if(arg1 == Qt::Checked) {
QString *strHex = new QString;
*strHex = ui->textEditReceive->toPlainText().replace("\n", "\r\n"); //QT中ENTER键为:\n(即0A),将其替换为Windows中的\r\n(即0D 0A)
ui->textEditReceive->clear();
ui->textEditReceive->insertPlainText(strHex->toUtf8().toHex(' ').append(' ')); //QString转QByteArray,QByteArray中的字符转16进制并追加空格,toHex在每个16进制数后加空格,append在最后加空格
ui->textEditReceive->moveCursor(QTextCursor::End);
delete strHex;
mHexDisplay = true;
} else {
QString *strChar = new QString;
*strChar = ui->textEditReceive->toPlainText().remove(QRegExp("\\s")); //删除空格,空格的正则表达式为\s
ui->textEditReceive->clear();
ui->textEditReceive->insertPlainText(QByteArray::fromHex(strChar->toLatin1())); //toLatin1:按照ASCII编码把String转成ByteArray,fromHex:对ByteArray做16进制解码
ui->textEditReceive->moveCursor(QTextCursor::End);
delete strChar;
mHexDisplay = false;
}
}
16进制发送显示checkbox的stateChanged槽函数:
void MainWindow::on_checkBoxHexSend_stateChanged(int arg1)
{
if(arg1 == Qt::Checked) {
QString *strHex = new QString;
*strHex = ui->textEditSend->toPlainText().replace("\n", "\r\n");
ui->textEditSend->clear();
ui->textEditSend->insertPlainText(strHex->toUtf8().toHex(' ').append(' '));
ui->textEditSend->moveCursor(QTextCursor::End);
delete strHex;
mHexSend = true;
} else {
QString *strChar = new QString;
*strChar = ui->textEditSend->toPlainText().remove(QRegExp("\\s"));
ui->textEditSend->clear();
ui->textEditSend->insertPlainText(QByteArray::fromHex(strChar->toLatin1()));
ui->textEditSend->moveCursor(QTextCursor::End);
delete strChar;
mHexSend = false;
}
}
而收发槽函数中也要相应加入格式转换的逻辑。
接收槽函数:
void MainWindow::on_serialPort_readyRead()
{
if(mIsOpen == true) {
QByteArray rxData = mSerialPort.readAll();
if(ui->checkBoxHexDisplay->isChecked()) {
ui->textEditReceive->insertPlainText(rxData.toHex(' ').append(' ')); //把ByteArray按16进制编码,toHex在每个16进制数后加空格,append在最后加空格
} else {
ui->textEditReceive->insertPlainText(rxData);
}
ui->textEditReceive->moveCursor(QTextCursor::End); //调整光标位置到最新接收的尾部,避免看不到最新接收的数据
}
}
发送槽函数:
void MainWindow::on_pushButtonSend_clicked()
{
if(mIsOpen == true) {
if(ui->checkBoxHexSend->isChecked()) {
QByteArray* arrayTxData = new QByteArray;
*arrayTxData = ui->textEditSend->toPlainText().remove(QRegExp("\\s")).toUtf8();
mSerialPort.write(QByteArray::fromHex(*arrayTxData));
delete arrayTxData;
} else {
//mSerialPort.write(ui->textEditSend->toPlainText().replace("\n", "\r\n").toStdString().c_str());
mSerialPort.write(ui->textEditSend->toPlainText().replace("\n", "\r\n").toUtf8()); //QT中ENTER键为:\n(即0A),将其替换为Windows中的\r\n(即0D 0A)
}
}
}
也许有读者会对上述代码中那一连串的成员函数感到疑惑,不知其为何意,想要知道这些函数的作用,最好的办法是查阅QT的帮助文档。
比如要查fromHex这个函数的作用(对于QT中的类的搜索也是同理,查阅帮助文档和手册是学习QT乃至许多技术的必备技能):
9 清除发送/接收
这个非常简单,只需在槽函数中调用QTextEdit控件的clear方法即可。
10 后记
QT中的各种控件类都是经过层层继承而来,调用某个控件类中的方法,不一定是定义在该类里面,而是定义在其父类中。这种套娃模式极好地用代码描述了真实世界,是面向对象的精髓之一。
如果要实现串口列表的实时更新,习惯了面向过程开发的朋友可能第一反应是用定时器去周期更新,而在面向对象的世界中有一个方法是把控件的方法给改写,在其中加入获取串口列表的逻辑,这是两种开发思想差异的一个体现。
本文中的串口助手其实还是有很多不完善之处,比如还缺少以下功能:
- 16进制发送模式下,发送框的非法字符检测
- 发送接收字节数统计
- 自动发送
- 保存上一次的串口配置
- …
后续有时间将慢慢补上。
11 源码
懒得传git,先放某度云上
链接:Serial
提取码:hjq5
12 参考
- QT帮助文档
- QT中的foreach关键字
- QComboBox点击时自动更新列表(自动刷新QSerialPort)
- QT弹窗
- QTextEdit追加纯文本(无额外的换行)
- QString、QByteArray、ASCII码、16进制等类型转换和编码转换
- QT 十六进制字符串与原数据字符串互转
- QT QString去除空格
- QT 正则表达式
SZ
2023.9.3