串口调试助手
一、项目概述
本项目旨在基于 QT 框架实现一款串口调试助手,帮助大家回顾 QT 项目的开发流程,以及如何使用帮助文档来进行 QT 新控件的学习。该助手具备跨平台特性,可在 Windows、Linux、macOS 等操作系统上稳定运行,满足不同开发环境下的使用需求。 从功能层面来看,这款串口调试助手涵盖了串口通信的核心需求与实用扩展功能。核心功能包括:
基于 QT 框架开发的串口调试助手支持跨平台运行,具备串口搜索、参数配置、数据收发等功能。实现十六进制与文本格式转换、接收时间戳显示、定时发送及多文本区快捷指令循环发送。利用 QSerialPort 类完成底层通信,结合信号槽机制处理界面交互与定时器控制。代码结构清晰,涵盖布局管理与控件动态查找,适用于硬件调试场景参考。

本项目旨在基于 QT 框架实现一款串口调试助手,帮助大家回顾 QT 项目的开发流程,以及如何使用帮助文档来进行 QT 新控件的学习。该助手具备跨平台特性,可在 Windows、Linux、macOS 等操作系统上稳定运行,满足不同开发环境下的使用需求。 从功能层面来看,这款串口调试助手涵盖了串口通信的核心需求与实用扩展功能。核心功能包括:
在本次串口调试项目开发过程中,所使用的开发环境与工具如下:

如上图所示,整体上将界面分为三个部分,分别是:


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


如图所示,用户交互区由 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,界面'隐藏历史'):隐藏 / 显示历史数据记录区域,聚焦当前操作。
checkBoxSendInTime(QCheckBox,界面'定时发送'):勾选后启用定时发送功能,配合lineEditTimeEach实现周期性数据发送。lineEditTimeEach(QLineEdit,界面'1000 ms / 次'):输入定时发送的时间间隔(毫秒),决定定时发送频率。checkBoxSendNewLine(QCheckBox,界面'发送换行'):设置发送数据时是否自动附加换行符,适配接收端的数据解析需求。checkBoxHexSend(QCheckBox,界面'HEX 发送'):切换发送数据的格式,开启后以十六进制发送,用于调试二进制指令。btnSendContext(QPushButton,界面'发送'):点击发送lineEditSendContext中输入的内容,是数据发送的直接操作按钮。lineEditSendContext(QLineEdit,界面输入框):用户输入待发送的文本 / 指令,支持手动输入、粘贴,作为串口发送的数据来源。


由图可知,状态显示区由 widgetStatus 及子标签组成,用于展示串口通信的状态信息,使用水平布局,以下为各个控件:
labelCurrentTime(QLabel):显示当前系统时间,或配合功能展示数据收发时刻。labelRevCnt(QLabel):统计并显示接收数据的字节数,反馈串口通信的活跃度。labelSendCnt(QLabel):统计并显示发送数据的字节数,辅助监测发送操作。labelSendStatus(QLabel):展示发送操作的状态(如'发送成功'/'发送失败''串口未连接'),提供反馈。作用:QT 串口通信的核心类,封装了串口设备的打开、关闭、参数配置、数据读写等底层操作,是实现串口通信的基础。 关键用法:设置串口参数:setPortName()(串口号)、setBaudRate()(波特率)、setDataBits()(数据位)等。数据收发:write() 发送数据,readAll() 读取接收缓冲区数据。状态控制:open(QIODevice::ReadWrite) 打开串口,close() 关闭串口,isOpen() 判断是否连接。
作用:提供串口设备的信息查询功能,用于枚举系统中可用的串口,获取串口名称、厂商信息等。 关键用法:
availablePorts():返回系统中所有可用串口的列表(QList)。portName():获取串口名称(如'COM1''/dev/ttyUSB0')。
如下为串口查找对应的槽函数:
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 没有被点击的信号可供我们直接使用,因此需要提前捕获鼠标点击事件,因此创建子类 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);
触发机制:信号与槽 功 能:该按钮有两种状态,分别为打开串口和关闭串口。 打开串口:设置 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();
}
}
QByteArray::fromHex()(十六进制字符串转字节流);toLocal8Bit()(文本转本地编码字节)。// 发送按钮点击事件处理函数:负责将用户输入的数据通过串口发送
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!");
}
}
调用时机:函数由 QSerialPort::readyRead 信号触发,即串口有数据到达时自动执行。
connect(serialPort,&QSerialPort::readyRead,this,&on_serialData_readyRead);
当勾选了自动换行时,会在接收数据后增加"\r\n",具体实现如下:
if(ui->checkBoxSwitchLine->isChecked()) readMsg+="\r\n";
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 类型
系统时间由主窗口类中的两个成员变量组成,分别是 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 变量,然后用于填充右下角的时间标签来更新系统时间。
当勾选接收时间时,会在接收数据前加入当前系统的时间,具体实现如下:
// 分支 2:文本显示模式(默认模式)
else{
// 如果开启了"接收时间戳"功能,在数据前附加时间戳
if(timeDisplay){
// 格式:[时间戳]\n 数据(例如:[2023-10-01 12:34:56]\nHello)
ui->textEditRev->insertPlainText('['+ myTime +']'+'\n'+ readMsg);
}
// 不附加时间戳,直接显示原始接收数据
else{
ui->textEditRev->insertPlainText(readMsg);
}
}
// 串口数据接收处理函数:当串口有数据到达时(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;
}
现象:当勾选定时发送复选框时,串口会每隔 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);
}
}
首先我们来看一下多文本区域复选框、指令编辑框、发送按钮的命名,代码实现会利用命名特性:

从上图可以看到复选框命名为 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 槽函数。
现象:在打开串口的前提下,当选中循环发送复选框,会以 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();
}
}
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;
}
与定时发送逻辑相似,当选中复选框时启动定时器,当取消时停止定时器。
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);
}
}
本项目基于 QT 框架实现了一款功能完善的串口调试助手,涵盖串口搜索、参数配置、数据收发等核心功能,还支持十六进制转换、定时发送、多指令循环发送等扩展功能。 开发过程中,通过 QSerialPort 与 QSerialPortInfo 类实现串口通信底层逻辑,利用布局管理器构建清晰的界面结构,将界面分为显示区、交互区和状态栏,提升了用户体验。 在功能实现上,重点解决了十六进制与文本格式转换、多控件信号处理、定时器精准控制等问题,通过动态获取控件指针实现了多指令快捷发送功能。 该项目不仅巩固了 QT 信号与槽、布局管理等基础知识,还提供了硬件调试工具开发的实践经验,为后续同类项目开发奠定了基础。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online