一、 项目概述

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

  1. 自动搜索并显示可用串口
  2. 支持对波特率、数据位、停止位、校验位等关键参数进行灵活配置
  3. 实现数据的实时发送与接收
    扩展功能包括:
  4. 数据格式的自由转换(可在十六进制与 ASCII 码之间切换)
  5. 涉及、定时发送功能的精准控制
  6. 接收数据的本地记录与导出
  7. 自定义数据的快速发送

二、开发环境与工具

在本次 串口调试项目开发过程中,所使用的开发环境与工具如下:

  1. 操作系统:Windows 10
  2. QT 版本:QT 5.8.0
  3. 编译器:MinGW_32bit
  4. 开发工具:QT Creator 4.2.1(Community)

三、界面设计与布局

1.总体界面概览

在这里插入图片描述
如上图所示,整体上将界面分为三个部分,分别是:

  1. 上方的显示区(接收组、历史记录组、多文本组)
  2. 中间的用户交互区(参数配置区、控制区、发送区)
  3. 下方的状态栏显示区(状态、接收字节、发送字节、时间)

2.页面布局

(1)显示区

在这里插入图片描述
在这里插入图片描述

如图所示,显示区由horizontalLayoutUp配置为水平布局,内部承载多个功能分组与控件,整体结构可拆为 3 大核心分组(groupBoxRev、groupBoxSend、groupBoxTexts ),逐层展开如下:

  1. 最外层:horizontalLayoutUp(QHBoxLayout)

作用:作为水平容器,将界面横向切分为 左、中、右 3 个功能区,让groupBoxRev(接收区)groupBoxSend(发送区)groupBoxTexts(文本配置区)水平排列,互不干扰。

  1. 第一层子控件: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)
  1. checkBox_Send(QCheckBox) : 是否启用上述指令集的循环发送(勾选后,子布局的发送逻辑生效 )
    2. label_4(QLabel):说明文字(循环发送)
    3. spinBox(QSpinBox):设置循环发送间隔参数
  • horizontalLayout_11(QHBoxLayout)
  1. btnInit(QPushButton):初始化参数(重置上述指令配置)
  2. btnLoad(QPushButton):加载预设参数
  3. btnSave(QPushButton):保存当前配置为文本文件

(2)用户交互区

在这里插入图片描述
在这里插入图片描述
如图所示,用户交互区由gridLayoutDownAll配置为网格布局,横向整合 “串口参数配置”、“串口控制与数据收发辅助功能” 两大板块,为串口调试的参数设置、功能操作提供交互入口 ,下面逐个介绍各个分组:

  1. 左侧串口参数配置区(垂直排列)

该部分为串口的相关配置,包括波特率、数据位、校验位、停止位和流控。其中每组由QLabel和QComboBox组成,由QHBoxLayout 水平布局。下面以串口控件为例:

  • 布局容器:horizontalLayout_serialnum(QHBoxLayout )
  • 控件
    combo_box_serialnum(MyComboBox ,自定义组合框 ):下拉选择可用串口号,如 COM1/COM2 等,是串口连接的基础配置 。
    -label_5(QLabel ):作为串口号选择的文本标识,显示 “串口” ,提示用户该控件功能 。
  1. 右上功能操作区——串口连接与基础操作(groupBox_DownRightUp 相关)
  • 布局:隐含水平排列,整合串口连接、数据清理功能按钮
  • 控件:

btnCloseOrOpenSerial(QPushButton ,界面显示 “打开串口” ):点击触发串口的连接 / 断开操作,是串口通信的核心控制入口 。
btnClearRev(对应界面 “清空接收” ,QPushButton ):一键清空接收区显示的串口数据,方便重新监测 。
btnSaveRev(对应界面 “保存接收” ,QPushButton ):将接收区数据保存到本地文件,用于数据记录与分析 。
checkBoxRevTime(QCheckBox ,界面 “接收时间” ):勾选后,接收数据时附加时间戳,便于定位数据收发时序 。
checkBoxHexDisplay(QCheckBox ,界面 “HEX 显示” ):切换接收数据的显示格式,开启后以十六进制展示,适合查看二进制数据 。
btnHidePanel(QPushButton ,界面 “隐藏面板” ):点击隐藏 / 显示多文本界面区域,优化操作空间 。
checkBoxAutoNewLine(QCheckBox ,界面 “自动换行” ):控制接收数据是否自动换行显示,提升长数据阅读体验 。
btnHideHistory(QPushButton ,界面 “隐藏历史” ):隐藏 / 显示历史数据记录区域,聚焦当前操作 。

  1. 右下串口发送区——数据发送控制(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文本框中的内容。我们来看具体实现:

  1. 定时器的创建与信号与槽的绑定
btnCtrlTimer = new QTimer(this);//btnCtrlTime是主窗口类中专门处理循环发送的定时器
connect(btnCtrlTimer,&QTimer::timeout,this,&Widget::btnTimeHandler);//绑定信号与槽
  1. 槽函数
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 信号与槽、布局管理等基础知识,还提供了硬件调试工具开发的实践经验,为后续同类项目开发奠定了基础。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐