【C++/QT】串口调试助手项目(附完整代码和可执行文件)
本项目基于 QT 框架实现了一款功能完善的串口调试助手,涵盖串口搜索、参数配置、数据收发等核心功能,还支持十六进制转换、定时发送、多指令循环发送等扩展功能。
一、 项目概述
本项目旨在基于 QT 框架逆向安信可的串口调试助手,帮助大家回顾QT项目的开发流程,以及如何使用帮助文档来进行QT新控件的学习。该助手具备跨平台特性,可在 Windows、Linux、macOS 等操作系统上稳定运行,满足不同开发环境下的使用需求。
从功能层面来看,这款串口调试助手涵盖了串口通信的核心需求与实用扩展功能。核心功能包括:
- 自动搜索并显示可用串口
- 支持对波特率、数据位、停止位、校验位等关键参数进行灵活配置
- 实现数据的实时发送与接收
扩展功能包括: - 数据格式的自由转换(可在十六进制与 ASCII 码之间切换)
- 涉及、定时发送功能的精准控制
- 接收数据的本地记录与导出
- 自定义数据的快速发送
二、开发环境与工具
在本次 串口调试项目开发过程中,所使用的开发环境与工具如下:
- 操作系统:Windows 10
- QT 版本:QT 5.8.0
- 编译器:MinGW_32bit
- 开发工具:QT Creator 4.2.1(Community)
三、界面设计与布局
1.总体界面概览

如上图所示,整体上将界面分为三个部分,分别是:
- 上方的显示区(接收组、历史记录组、多文本组)
- 中间的用户交互区(参数配置区、控制区、发送区)
- 下方的状态栏显示区(状态、接收字节、发送字节、时间)
2.页面布局
(1)显示区


如图所示,显示区由horizontalLayoutUp配置为水平布局,内部承载多个功能分组与控件,整体结构可拆为 3 大核心分组(groupBoxRev、groupBoxSend、groupBoxTexts ),逐层展开如下:
- 最外层:horizontalLayoutUp(QHBoxLayout)
作用:作为水平容器,将界面横向切分为 左、中、右 3 个功能区,让
groupBoxRev(接收区)、groupBoxSend(发送区)、groupBoxTexts(文本配置区)水平排列,互不干扰。
- 第一层子控件:3 个 QGroupBox 分组
| 分组名称 | 类型(QGroupBox) | 功能概括 | 子控件/子布局展开 |
|---|---|---|---|
groupBoxRev |
QGroupBox |
数据接收显示区 | 包含 1 个 textEditRev(QTextEdit) |
groupBoxSend |
QGroupBox |
历史记录显示区 | 包含 1 个 textEditRecord(QTextEdit) |
groupBoxTexts |
QGroupBox |
自定义文本参数配置区 | 嵌套多层布局,包含按钮、标签、子布局等 |
以上三个分组中,groupBoxRev和groupBoxSend结构简单,仅包含一个文本编辑区QTextEdit。而groupBoxTexts分组较为复杂,下面我们在UI界面从上到下详细介绍这一分组:
- horizontalLayout(QHBoxLayout)
由水平排列的3个QLabel组成:label、label_2、label_3(均为 QLabel):用于 显示说明文字(HEX、字符串、发送),无交互功能,纯 UI 引导。
- verticalLayoutRealText(QVBoxLayout)
垂直排列多个 水平子布局(horizontalLayout_1 ~ horizontalLayout_9 ),每个子布局承载一组
“复选框 + 输入框 + 按钮”,用于配置每组字符串编辑发送。其中每个horizontalLayout_N(N=1~9)都是 水平子布局,结构类似,以 horizontalLayout_1 为例:
checkBox_1:启用/复用HEX发送lineEdit_1:快速发送的指令配置pushButton_1:发送指令按钮
- horizontalLayout_10(QHBoxLayout)
- checkBox_Send(QCheckBox) : 是否启用上述指令集的循环发送(勾选后,子布局的发送逻辑生效 )
2.label_4(QLabel):说明文字(循环发送)
3.spinBox(QSpinBox):设置循环发送间隔参数
- horizontalLayout_11(QHBoxLayout)
btnInit(QPushButton):初始化参数(重置上述指令配置)btnLoad(QPushButton):加载预设参数btnSave(QPushButton):保存当前配置为文本文件
(2)用户交互区


如图所示,用户交互区由gridLayoutDownAll配置为网格布局,横向整合 “串口参数配置”、“串口控制与数据收发辅助功能” 两大板块,为串口调试的参数设置、功能操作提供交互入口 ,下面逐个介绍各个分组:
- 左侧串口参数配置区(垂直排列)
该部分为串口的相关配置,包括波特率、数据位、校验位、停止位和流控。其中每组由QLabel和QComboBox组成,由QHBoxLayout 水平布局。下面以串口控件为例:
- 布局容器:horizontalLayout_serialnum(QHBoxLayout )
- 控件
combo_box_serialnum(MyComboBox ,自定义组合框 ):下拉选择可用串口号,如 COM1/COM2 等,是串口连接的基础配置 。
-label_5(QLabel ):作为串口号选择的文本标识,显示 “串口” ,提示用户该控件功能 。
- 右上功能操作区——串口连接与基础操作(
groupBox_DownRightUp相关)
- 布局:隐含水平排列,整合串口连接、数据清理功能按钮
- 控件:
btnCloseOrOpenSerial(QPushButton ,界面显示 “打开串口” ):点击触发串口的连接 / 断开操作,是串口通信的核心控制入口 。btnClearRev(对应界面 “清空接收” ,QPushButton ):一键清空接收区显示的串口数据,方便重新监测 。btnSaveRev(对应界面 “保存接收” ,QPushButton ):将接收区数据保存到本地文件,用于数据记录与分析 。checkBoxRevTime(QCheckBox ,界面 “接收时间” ):勾选后,接收数据时附加时间戳,便于定位数据收发时序 。checkBoxHexDisplay(QCheckBox ,界面 “HEX 显示” ):切换接收数据的显示格式,开启后以十六进制展示,适合查看二进制数据 。btnHidePanel(QPushButton ,界面 “隐藏面板” ):点击隐藏 / 显示多文本界面区域,优化操作空间 。checkBoxAutoNewLine(QCheckBox ,界面 “自动换行” ):控制接收数据是否自动换行显示,提升长数据阅读体验 。btnHideHistory(QPushButton ,界面 “隐藏历史” ):隐藏 / 显示历史数据记录区域,聚焦当前操作 。
- 右下串口发送区——数据发送控制(groupBox_DownRightLow 相关)
- 布局:网格布局,整合发送相关功能
- 控件:
checkBoxSendInTime(QCheckBox ,界面 “定时发送” ):勾选后启用定时发送功能,配合lineEditTimeEach实现周期性数据发送 。lineEditTimeEach(QLineEdit ,界面 “1000 ms / 次” ):输入定时发送的时间间隔(毫秒 ),决定定时发送频率 。checkBoxSendNewLine(QCheckBox ,界面 “发送换行” ):设置发送数据时是否自动附加换行符,适配接收端的数据解析需求 。checkBoxHexSend(QCheckBox ,界面 “HEX 发送” ):切换发送数据的格式,开启后以十六进制发送,用于调试二进制指令 。btnSendContext(QPushButton ,界面 “发送” ):点击发送lineEditSendContext中输入的内容,是数据发送的直接操作按钮 。lineEditSendContext(QLineEdit ,界面输入框 ):用户输入待发送的文本 / 指令,支持手动输入、粘贴,作为串口发送的数据来源 。
(3)状态显示区


由图可知,状态显示区由widgetStatus 及子标签组成,用于展示串口通信的状态信息,使用水平布局,以下为各个控件:
labelCurrentTime(QLabel ):显示当前系统时间,或配合功能展示数据收发时刻 。labelRevCnt(QLabel ):统计并显示接收数据的字节数,反馈串口通信的活跃度 。labelSendCnt(QLabel ):统计并显示发送数据的字节数,辅助监测发送操作 。labelSendStatus(QLabel ):展示发送操作的状态(如 “发送成功”/“发送失败” “串口未连接” ),提供反馈 。
四、核心功能实现
1.串口通信核心类QSerialPort与QSerialPortInfo
(1)QSerialPort
作用:QT 串口通信的核心类,封装了串口设备的打开、关闭、参数配置、数据读写等底层操作,是实现串口通信的基础。
关键用法:
- 设置串口参数:setPortName()(串口号)、setBaudRate()(波特率)、setDataBits()(数据位)等。
- 数据收发:write() 发送数据,readAll() 读取接收缓冲区数据。
- 状态控制:open(QIODevice::ReadWrite) 打开串口,close() 关闭串口,isOpen() 判断是否连接。
(2)QSerialPortInfo
作用:提供串口设备的信息查询功能,用于枚举系统中可用的串口,获取串口名称、厂商信息等。
关键用法:
availablePorts():返回系统中所有可用串口的列表(QList)。portName():获取串口名称(如 “COM1”“/dev/ttyUSB0”)。
2.串口查找与打开/关闭串口
(1)查找串口
如下为串口查找对应的槽函数:
void Widget::on_comboBox_serialnum_clicked()
{
ui->comboBox_serialnum->clear();//清空串口comboBox中的所有串口名(防止重复添加)
QList<QSerialPortInfo> serialList = QSerialPortInfo::availablePorts();//查找当前电脑所连接的所有串口,返回所有已查到的串口信息列表
for(QSerialPortInfo serialport : serialList)//遍历所有串口
{
ui->comboBox_serialnum->addItem(serialport.portName());//在串口comboBox中增加串口名作为一项
qDebug()<<serialport.portName();
}
ui->labelSendStatus->setText("COM Refreshed!");//更新状态栏信息:串口查找刷新成功
}
槽函数调用时机:
- 主窗口刚打开时调用:因此需要在构造函数中调用该函数
Widget::Widget(QWidget *parent) :
QWidget(parent),
ui(new Ui::Widget)
{
ui->setupUi(this);
on_comboBox_serialnum_clicked();//更新电脑的当前串口
}
- 用户点击串口的comboBox时调用:信号与槽
实现方法:由于comboBox没有被点击的信号可供我们直接使用,因此需要提前捕获鼠标点击事件,因此创建子类MyComboBox继承QComboBox,并重写父类虚函数在虚函数中触发更新串口信号。
#ifndef MYCOMBOBOX_H
#define MYCOMBOBOX_H
#include <QWidget>
#include <QComboBox>
#include <QMouseEvent>
class MyComboBox : public QComboBox
{
Q_OBJECT
public:
MyComboBox(QWidget *parent);
protected:
virtual void mousePressEvent(QMouseEvent *e)//重写父类虚函数
{
if(e->button() == Qt::LeftButton){//只有当是鼠标左键点击事件时才触发
emit refresh();
}
QComboBox::mousePressEvent(e);//其他事件交给父类函数处理(不妨碍其他comboBox功能)
}
signals:
void refresh();
};
#endif // MYCOMBOBOX_H
这样只需绑定信号(refresh)与槽(on_comboBox_serialnum_clicked)即可:
connect(ui->comboBox_serialnum,&MyComboBox::refresh,this,/
&Widget::on_comboBox_serialnum_clicked);
(2)打开/关闭串口(on_btnCloseOrOpenSerial_clicked)
触发机制:信号与槽
功 能:该按钮有两种状态,分别为打开串口和关闭串口。
打开串口:设置serialPort的各自属性(如:串口名称、波特率、数据位、校验位等),调用open打开串口。
关闭串口:调用close关闭串口。
其中不同的状态对其他按钮的enable特性也有限制,具体见如下两张图片

如图可知,当未打开串口时,与发送特性有关的按钮无效,而像波特率、校验位等与串口有关的属性可以被选择;当串口已经打开时则相反。
// 串口打开/关闭按钮点击事件处理函数
void Widget::on_btnCloseOrOpenSerial_clicked()
{
// 判断当前串口是否处于关闭状态(serialStatus为false表示未打开)
if(!serialStatus)
{
// 1. 配置串口参数
// 设置串口号(从下拉框选择当前选中的串口号)
serialPort->setPortName(ui->comboBox_serialnum->currentText());
// 设置波特率(将下拉框文本转为整数,如"9600"→9600)
serialPort->setBaudRate(ui->comboBox_boautrate->currentText().toInt());
// 设置数据位(将下拉框文本转为整数,如"8"→8位数据位,通过QSerialPort::DataBits枚举封装)
serialPort->setDataBits(QSerialPort::DataBits(ui->comboBox_databit->currentText().toInt()));
// 设置校验位(根据下拉框选中的索引配置不同校验方式)
switch (ui->comboBox_jiaoyan->currentIndex()) {
case 0: // 无校验
serialPort->setParity(QSerialPort::NoParity);
break;
case 1: // 偶校验
serialPort->setParity(QSerialPort::EvenParity);
break;
case 2: // 标记校验
serialPort->setParity(QSerialPort::MarkParity);
break;
case 3: // 奇校验
serialPort->setParity(QSerialPort::OddParity);
break;
case 4: // 空格校验
serialPort->setParity(QSerialPort::SpaceParity);
break;
default: // 未知校验(默认情况,实际很少触发)
serialPort->setParity(QSerialPort::UnknownParity);
break;
}
// 设置停止位(根据下拉框选中的索引配置不同停止位)
switch(ui->comboBox_stopbit->currentIndex()){
case 0: // 1位停止位
serialPort->setStopBits(QSerialPort::OneStop);
break;
case 1: // 1.5位停止位(较少使用,部分硬件支持)
serialPort->setStopBits(QSerialPort::OneAndHalfStop);
break;
case 2: // 2位停止位
serialPort->setStopBits(QSerialPort::TwoStop);
break;
case 3: // 未知停止位(默认情况)
serialPort->setStopBits(QSerialPort::UnknownStopBits);
break;
default:
break;
}
// 设置流控制(此处简化处理,仅支持"无流控",可根据需求扩展)
if(ui->comboBox_filecon->currentText() == "None")
serialPort->setFlowControl(QSerialPort::NoFlowControl);
// 2. 尝试打开串口(以读写模式)
if(serialPort->open(QIODevice::ReadWrite))
{
// 打开成功:更新日志、UI状态和串口状态标记
qDebug()<<"Serial open successful!"; // 调试日志
// 禁用串口参数配置控件(避免连接状态下修改参数导致冲突)
ui->comboBox_boautrate->setEnabled(false);
ui->comboBox_databit->setEnabled(false);
ui->comboBox_filecon->setEnabled(false);
ui->comboBox_jiaoyan->setEnabled(false);
ui->comboBox_serialnum->setEnabled(false);
ui->comboBox_stopbit->setEnabled(false);
// 更新按钮文本为"关闭串口"
ui->btnCloseOrOpenSerial->setText(tr("关闭串口"));
// 启用发送相关功能控件
ui->btnSendContext->setEnabled(true); // 发送按钮
ui->checkBoxSendInTime->setEnabled(true); // 定时发送复选框
ui->checkBoxHexSend->setEnabled(true); // 十六进制发送复选框
ui->checkBoxSendNewLine->setEnabled(true); // 发送换行复选框
// 更新状态标签,提示用户串口已成功打开
ui->labelSendStatus->setText(ui->comboBox_serialnum->currentText() + " Open Successed!");
// 将串口状态标记设为"已打开"
serialStatus = true;
}
else{
// 打开失败:输出日志并提示用户(可能因串口被占用、不存在或权限问题)
qDebug()<<"Serial open failed!";
QMessageBox::critical(this,tr("串口打开失败"),tr("打开失败,串口可能被占用或拔出!"),QMessageBox::Ok);
}
}
else{
// 3. 若串口已打开,则执行关闭操作
serialPort->close();
qDebug()<<"Serial close successful!"; // 调试日志
// 更新串口状态标记为"已关闭"
serialStatus = false;
// 恢复串口参数配置控件的可用性
ui->comboBox_boautrate->setEnabled(true);
ui->comboBox_databit->setEnabled(true);
ui->comboBox_filecon->setEnabled(true);
ui->comboBox_jiaoyan->setEnabled(true);
ui->comboBox_serialnum->setEnabled(true);
ui->comboBox_stopbit->setEnabled(true);
// 更新按钮文本为"打开串口"
ui->btnCloseOrOpenSerial->setText(tr("打开串口"));
// 禁用发送相关功能控件
ui->btnSendContext->setEnabled(false);
ui->checkBoxSendInTime->setEnabled(false);
ui->checkBoxSendInTime->setCheckState(Qt::Unchecked); // 取消定时发送勾选
ui->lineEditTimeEach->setEnabled(true); // 恢复定时间隔输入框
ui->lineEditSendContext->setEnabled(true); // 恢复发送内容输入框
ui->checkBoxHexSend->setEnabled(false);
ui->checkBoxSendNewLine->setEnabled(false);
// 更新状态标签,提示用户串口已成功关闭
ui->labelSendStatus->setText(ui->comboBox_serialnum->currentText() + " Close Successed!");
// 停止定时发送定时器(避免关闭后仍触发发送)
timer->stop();
}
}
3.数据发送(on_btnSendContext_clicked)
- 核心逻辑:发送按照是否勾选HEX发送复选框来决定是16进制发送和文本发送。其中对于16进制发送会进行输入文本校验,即长度为偶数、字符合法,避免无效数据发送。
- 数据转换:
QByteArray::fromHex()(十六进制字符串转字节流);toLocal8Bit()(文本转本地编码字节)。 - 发送新行:若勾选了发送新行,则会在发送内容的最后加上"\r\n"
// 发送按钮点击事件处理函数:负责将用户输入的数据通过串口发送
void Widget::on_btnSendContext_clicked()
{
int sendCnt = 0; // 记录实际发送的字节数
// 将发送框中的文本转换为本地编码的字节流(用于后续文本发送)
const char* sendMsg = ui->lineEditSendContext->text().toLocal8Bit().constData();
// 分支1:如果勾选了"十六进制发送"(HEX发送)
if(ui->checkBoxHexSend->isChecked())
{
// 将输入的文本转换为字节数组(如"AA55"转为对应的字节序列)
QByteArray tmp = ui->lineEditSendContext->text().toLocal8Bit();
// 校验1:十六进制输入必须为偶数长度(每2个字符代表1个字节)
if(tmp.size() % 2 != 0)
{
ui->labelSendStatus->setText("Error Input!"); // 显示输入错误
return; // 终止发送流程
}
// 校验2:检查输入是否为合法的十六进制字符(0-9、A-F、a-f)
for(char c : tmp)
{
if(!isxdigit(c)){ // isxdigit()判断字符是否为十六进制有效字符
ui->labelSendStatus->setText("Error Input!");
return;
}
}
// 如果勾选了"发送换行",附加回车换行符(\r\n)
if(ui->checkBoxSendNewLine->isChecked())
{
tmp.append("\r\n");
}
// 将十六进制字符串转换为对应的字节数组(核心转换逻辑)
QByteArray sendArry = QByteArray::fromHex(tmp);
// 通过串口发送字节数组,返回实际发送的字节数
sendCnt = serialPort->write(sendArry);
}
// 分支2:文本发送模式(默认模式)
else{
// 如果勾选了"发送换行",在发送内容后附加回车换行符
if(ui->checkBoxSendNewLine->isChecked())
{
// 构建包含换行符的发送数据
QByteArray arrySendData(sendMsg,strlen(sendMsg));
arrySendData.append("\r\n");
sendCnt = serialPort->write(arrySendData);
}
// 不附加换行符,直接发送原始内容
else{
sendCnt = serialPort->write(sendMsg);
}
}
// 发送结果处理:判断发送是否成功
if(sendCnt != -1) // sendCnt为-1表示发送失败
{
qDebug()<<"Send MSG:"<<sendMsg; // 调试日志输出发送内容
writeCntTotal += sendCnt; // 累加总发送字节数
// 更新发送计数标签(显示累计发送字节数)
ui->labelSendCnt->setText("Sent:" + QString::number(writeCntTotal));
ui->labelSendStatus->setText("Send OK!"); // 显示发送成功状态
// 如果当前发送内容与上一次不同,则记录到历史发送区
if(strcmp(sendMsg,sendBak.toStdString().c_str()) != 0)
{//sendBak为窗口类的私有成员变量,QString sendBak;
ui->textEditRecord->append(sendMsg); // 添加到历史记录
sendBak = QString::fromUtf8(sendMsg); // 保存当前内容作为下次对比基准
}
}
else{
// 发送失败:更新状态标签提示错误
ui->labelSendStatus->setText("Send Erro!");
}
}
4.读取数据
调用时机:函数由 QSerialPort::readyRead 信号触发,即串口有数据到达时自动执行。
connect(serialPort,&QSerialPort::readyRead,this,&on_serialData_readyRead);
(1)自动换行
当勾选了自动换行时,会在接收数据后增加"\r\n",具体实现如下:
if(ui->checkBoxSwitchLine->isChecked()) readMsg += "\r\n";
(2)HEX显示
HEX显示是用于帮助用于阅读二进制数据的,当勾选了HEX显示,接收数据时会先将当前接收区内容转换为字节数组,然后将接收字节转换为16进制字节数组并进行拼接,具体实现如下:
if(ui->checkBoxHexDisplay->isChecked())
{
// 1. 获取接收区当前已显示的文本,转换为UTF-8编码的字节数组
QByteArray tmpArryHex = ui->textEditRev->toPlainText().toUtf8();
// 2. 将新接收的字节数组转换为十六进制字符串(大写),并拼接到历史数据后
// 例如:收到字节0xAA、0xBB,会转为"AA BB"(实际实现中可能无空格,根据需求调整)
QByteArray tmpHexString = tmpArryHex + readMsg.toUtf8().toHex().toUpper();
// 3. 将拼接后的十六进制字符串显示到接收区
ui->textEditRev->setText(QString::fromUtf8(tmpHexString));
}//QString::fromUtf8()函数将QByteArry的字节数组转换为QString类型
(3)系统时间
系统时间由主窗口类中的两个成员变量组成,分别是QTimer *sysTimer;QString myTime;其中sysTimer是一个每间隔1秒输出一次的定时器,myTime是用于保存当前系统时间的成员变量。
关于定时器,有如下代码:
connect(sysTimer,&QTimer::timeout,this,&Widget::on_freshTime);
sysTimer->start(100);
void Widget::getSysTime()
{
QDateTime currentTime = QDateTime::currentDateTime();
QDate date = currentTime.date();
QTime time = currentTime.time();
int year = date.year();
int month = date.month();
int day = date.day();
int hour = time.hour();
int minute = time.minute();
int second = time.second();
myTime = QString("%1-%2-%3 %4-%5-%6")
.arg(year,2,10,QChar('0')).arg(month,2,10,QChar('0')).arg(day,2,10,QChar('0'))
.arg(hour,2,10,QChar('0')).arg(minute,2,10,QChar('0')).arg(second,2,10,QChar('0'));
}
void Widget::on_freshTime()
{
getSysTime();
ui->labelCurrentTime->setText(myTime);
}
可以看到每隔一秒会刷新myTime变量,然后用于填充右下角的时间标签来更新系统时间。
(4)接收时间
当勾选接收时间时,会在接收数据前加入当前系统的时间,具体实现如下:
// 分支2:文本显示模式(默认模式)
else{
// 如果开启了"接收时间戳"功能,在数据前附加时间戳
if(timeDisplay)
{
// 格式:[时间戳]\n数据(例如:[2023-10-01 12:34:56]\nHello)
ui->textEditRev->insertPlainText('[' + myTime + ']' + '\n' + readMsg);
}
// 不附加时间戳,直接显示原始接收数据
else{
ui->textEditRev->insertPlainText(readMsg);
}
}
(5)完整代码
// 串口数据接收处理函数:当串口有数据到达时(readyRead信号触发),执行此函数
void Widget::on_serialData_readyRead()
{
// 读取串口接收缓冲区中的所有数据,存储到readMsg(QByteArray类型,字节数组)
readMsg = serialPort->readAll();
// 仅当接收的数据不为空时,进行后续处理
if(readMsg != NULL)
{
// 如果勾选了"自动换行",在接收数据后附加回车换行符(优化显示格式)
if(ui->checkBoxSwitchLine->isChecked())
readMsg += "\r\n";
// 分支1:如果勾选了"十六进制显示"(HEX显示)
if(ui->checkBoxHexDisplay->isChecked())
{
// 1. 获取接收区当前已显示的文本,转换为UTF-8编码的字节数组
QByteArray tmpArryHex = ui->textEditRev->toPlainText().toUtf8();
// 2. 将新接收的字节数组转换为十六进制字符串(大写),并拼接到历史数据后
// 例如:收到字节0xAA、0xBB,会转为"AA BB"(实际实现中可能无空格,根据需求调整)
QByteArray tmpHexString = tmpArryHex + readMsg.toUtf8().toHex().toUpper();
// 3. 将拼接后的十六进制字符串显示到接收区
ui->textEditRev->setText(QString::fromUtf8(tmpHexString));
}
// 分支2:文本显示模式(默认模式)
else{
// 如果开启了"接收时间戳"功能,在数据前附加时间戳
if(timeDisplay)
{
// 格式:[时间戳]\n数据(例如:[2023-10-01 12:34:56]\nHello)
ui->textEditRev->insertPlainText('[' + myTime + ']' + '\n' + readMsg);
}
// 不附加时间戳,直接显示原始接收数据
else{
ui->textEditRev->insertPlainText(readMsg);
}
}
// 累加接收的总字节数(readMsg.size()返回当前接收数据的字节长度)
readCntTotal += readMsg.size();
// 更新接收计数标签,显示累计接收字节数
ui->labelRevCnt->setText("Received:" + QString::number(readCntTotal));
// 将光标移动到接收区末尾,方便查看最新数据
ui->textEditRev->moveCursor(QTextCursor::End);
// 确保光标所在区域可见(自动滚动到末尾)
ui->textEditRev->ensureCursorVisible();
}
// 调试日志:输出接收到的原始数据(便于开发时排查问题)
qDebug()<<"Rev MSG:"<<readMsg;
}
5.定时发送
现象:当勾选定时发送复选框时,串口会每隔lineEditTimeEach文本框内的时间发送一次发送框(lineEditSendContext)内的数据。
因此不难推断,当选中或取消复选框也就会启动或关闭定时器,在主窗口类中有QTimer *timer;成员变量,其绑定了信号与槽,当计时时间到达时会触发数据发送函数on_btnSendContext_clicked。
connect(timer,&QTimer::timeout,this,&Widget::on_btnSendContext_clicked);
以下为定时发送复选框的槽函数:
void Widget::on_checkBoxSendInTime_clicked(bool checked)
{
if(checked)//如果选中复选框
{
timer->start(ui->lineEditTimeEach->text().toInt());//按照文本框内的时间启动定时器
ui->lineEditTimeEach->setEnabled(false);//定时时间文本框无效
ui->lineEditSendContext->setEnabled(false);//发送数据文本框无效
}
else{//如果取消复选框
timer->stop();//停止定时器
ui->lineEditTimeEach->setEnabled(true);
ui->lineEditSendContext->setEnabled(true);
}
}
6.多文本区
(1)快捷指令发送
首先我们来看一下多文本区域复选框、指令编辑框、发送按钮的命名,代码实现会利用命名特性:
从上图可以看到复选框命名为checkBox_N、指令编辑框命名为lineEdit_N、按钮命名为pushButton_N,我们可以使用QString btnName = QString("pushButton_%1").arg(i); QPushButton *btn = findChild<QPushButton *>(btnName);来获取到每一个控件的指针。
- 在主窗口中,有三个成员变量
QList<QPushButton *> buttons; QList<QLineEdit *> lineEdits; QList<QCheckBox *> checkBoxs;分别用于存储操作各个控件的指针。 - 在析构函数中,有如下代码来保存各个指针到列表中,同时绑定各个按钮的信号与槽
for(int i = 1;i <= 9;i++)
{
QString btnName = QString("pushButton_%1").arg(i);//组包按钮名字
QPushButton *btn = findChild<QPushButton *>(btnName);//通过按钮名字找到控件指针
if(btn)
{
btn->setProperty("btnID",i);//给按钮增加一个属性i为当前按钮序号,后续通过该属性找到对应的复选框和指令编辑框
buttons.append(btn);//将按钮加入到列表中
connect(btn,SIGNAL(clicked()),this,SLOT(on_command_button_clicked()));//绑定信号与槽
}
QString lineEditName = QString("lineEdit_%1").arg(i);
QLineEdit *lineEdit = findChild<QLineEdit *>(lineEditName);
lineEdits.append(lineEdit);
QString checkBoxName = QString("checkBox_%1").arg(i);
QCheckBox *checkBox = findChild<QCheckBox *>(checkBoxName);
checkBoxs.append(checkBox);
}
在看槽函数之前,需要了解一个函数sender():
作用:这是 QObject 类提供的一个成员函数,返回发送当前信号的对象指针(类型为 QObject*)。
例如:当点击一个按钮时,按钮会发送 clicked() 信号,若该信号关联到某个槽函数,则在槽函数中调用 sender(),会返回这个按钮的指针。
在了解了sender函数的用法后,接下来看看槽函数:
void Widget::on_command_button_clicked()
{
QPushButton *btn = qobject_cast<QPushButton *>(sender());//得到触发槽函数的按钮
if(btn)
{
int ID = btn->property("btnID").toInt();//根据之前添加的属性拿到按钮的序号
QString checkBoxName = QString("checkBox_%1").arg(ID);//组包得到复选框的名名字
QCheckBox *checkBox = findChild<QCheckBox *>(checkBoxName);//根据名字拿到操作复选框的指针
if(checkBox)
ui->checkBoxHexSend->setChecked(checkBox->isChecked());//如果复选框被选中,则选中HEX发送复选框
QString lineEditName = QString("lineEdit_%1").arg(ID);//拿到指令编辑文本框的名字
QLineEdit *lineEdit = findChild<QLineEdit *>(lineEditName);//拿到操作指针
if(lineEdit )
if(lineEdit->text().size() <= 0)
return;//如果文本框内没有内容,则直接返回
ui->lineEditSendContext->setText(lineEdit->text());//将文本框内容复制到发送指令的文本框中
on_btnSendContext_clicked();//调用发送指令槽函数
}
}
总体而言,在多文本区中的发送指令功能,其实是调用了on_btnSendContext_clicked槽函数。
(2)循环发送
现象:在打开串口的前提下,当选中循环发送复选框,会以spinBox内设置的时间为间隔循环发送指令框中的内容(从第一个开始,到最后一个有内容的指令框截至)。
例如,在上图情况下会循环发送1-7文本框中的内容。我们来看具体实现:
- 定时器的创建与信号与槽的绑定
btnCtrlTimer = new QTimer(this);//btnCtrlTime是主窗口类中专门处理循环发送的定时器
connect(btnCtrlTimer,&QTimer::timeout,this,&Widget::btnTimeHandler);//绑定信号与槽
- 槽函数
void Widget::btnTimeHandler()//循环发送计时器的超时函数
{
qDebug()<<"Handler"<<checkValidTextsNum();//获取有效文本框的数量,对应上图情况时,返回值为7
if(btnIndex < checkValidTextsNum())//确保btnIndex处于合理的范围
//btnIndex为成员变量,用于保存当前遍历的指令序号
{
QPushButton *btnTmp = buttons[btnIndex];//得到操作发送按钮的指针
emit btnTmp->clicked();//手动触发点击信号
btnIndex++;//切换到下一个指令
}
else{//此分支若写成btnIndex = 0;会当上面if分支不满足时,会在一次超时时间内什么都没有发送,因此在该分支内手动发送一次。
btnIndex = 1;//提前切换到buttons[0]后的下一个按钮
//以下代码为手动发送buttons[0]中的指令
QString checkBoxName = QString("checkBox_1");
QCheckBox *checkBox = findChild<QCheckBox *>(checkBoxName);
if(checkBox)
ui->checkBoxHexSend->setChecked(checkBox->isChecked());
QString lineEditName = QString("lineEdit_1");
QLineEdit *lineEdit = findChild<QLineEdit *>(lineEditName);
ui->lineEditSendContext->setText(lineEdit->text());
on_btnSendContext_clicked();
}
}
(3)checkValidTextsNum获取有效指令数
int Widget::checkValidTextsNum()
{
int num = 0;//保存当前有效的指令数
for(int i = 1;i <= 9;i++)
{
QString lineEditName = QString("lineEdit_%1").arg(i);//得到指令框名字
QLineEdit *lineEdit = findChild<QLineEdit *>(lineEditName);//得到指令框操作指针
if (!lineEdit) {//若没找到(此情况不会产生)
qWarning() << "控件" << lineEditName << "未找到!";
return num; // 控件不存在视为“无效”,返回当前计数
}
if(lineEdit->text().isEmpty())//如果指令框没有内容则返回有效指令数
return num;
num++;
}
return num;
}
(4)循环发送复选框
与定时发送逻辑相似,当选中复选框时启动定时器,当取消时停止定时器。
void Widget::on_checkBox_Send_clicked(bool checked)
{
if(checked)
{
btnIndex = 0;
btnCtrlTimer->start(ui->spinBox->text().toUInt());
ui->spinBox->setEnabled(false);
}
else{
btnCtrlTimer->stop();
ui->spinBox->setEnabled(true);
}
}
五、可执行文件与源代码获取
1.可执行文件

该可执行文件已在不同电脑测试,直接双击.exe文件即可执行。
2.源代码

3.百度网盘链接
通过网盘分享的文件:串口调试助手
链接: https://pan.baidu.com/s/1zxzOJqdtMPsIK6xssyM_Yw 提取码: dnu5
六、总结
本项目基于 QT 框架实现了一款功能完善的串口调试助手,涵盖串口搜索、参数配置、数据收发等核心功能,还支持十六进制转换、定时发送、多指令循环发送等扩展功能。
开发过程中,通过 QSerialPort 与 QSerialPortInfo 类实现串口通信底层逻辑,利用布局管理器构建清晰的界面结构,将界面分为显示区、交互区和状态栏,提升了用户体验。
在功能实现上,重点解决了十六进制与文本格式转换、多控件信号处理、定时器精准控制等问题,通过动态获取控件指针实现了多指令快捷发送功能。
该项目不仅巩固了 QT 信号与槽、布局管理等基础知识,还提供了硬件调试工具开发的实践经验,为后续同类项目开发奠定了基础。
更多推荐

所有评论(0)