跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++大前端

C++ QT 串口调试助手项目开发实战

基于 QT 框架开发的串口调试助手支持跨平台运行,具备串口搜索、参数配置、数据收发等功能。实现十六进制与文本格式转换、接收时间戳显示、定时发送及多文本区快捷指令循环发送。利用 QSerialPort 类完成底层通信,结合信号槽机制处理界面交互与定时器控制。代码结构清晰,涵盖布局管理与控件动态查找,适用于硬件调试场景参考。

编程诗人发布于 2026/1/20更新于 2026/6/120 浏览
C++ QT 串口调试助手项目开发实战
串口调试助手

一、项目概述

本项目旨在基于 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)
功能概括
子控件/子布局展开
groupBoxRevQGroupBox数据接收显示区包含 1 个 textEditRev(QTextEdit)
groupBoxSendQGroupBox历史记录显示区包含 1 个 textEditRecord(QTextEdit)
groupBoxTextsQGroupBox自定义文本参数配置区嵌套多层布局,包含按钮、标签、子布局等

以上三个分组中,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 配置为网格布局,横向整合'串口参数配置'、'串口控制与数据收发辅助功能'两大板块,为串口调试的参数设置、功能操作提供交互入口,下面逐个介绍各个分组:

  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);
    }
}

六、总结

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

目录

  1. 串口调试助手
  2. 一、项目概述
  3. 二、开发环境与工具
  4. 三、界面设计与布局
  5. 1.总体界面概览
  6. 2.页面布局
  7. (1)显示区
  8. (2)用户交互区
  9. (3)状态显示区
  10. 四、核心功能实现
  11. 1.串口通信核心类 QSerialPort 与 QSerialPortInfo
  12. (1)QSerialPort
  13. (2)QSerialPortInfo
  14. 2.串口查找与打开/关闭串口
  15. (1)查找串口
  16. (2)打开/关闭串口(onbtnCloseOrOpenSerialclicked)
  17. 3.数据发送(onbtnSendContextclicked)
  18. 4.读取数据
  19. (1)自动换行
  20. (2)HEX 显示
  21. (3)系统时间
  22. (4)接收时间
  23. (5)完整代码
  24. 5.定时发送
  25. 6.多文本区
  26. (1)快捷指令发送
  27. (2)循环发送
  28. (3)checkValidTextsNum 获取有效指令数
  29. (4)循环发送复选框
  30. 六、总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 7 篇值得关注的大模型领域最新论文
  • JavaScript 集合 (Set、WeakSet) 与映射 (Map、WeakMap)
  • OpenClaw 完整部署指南:用户创建、安装配置及 Nginx 反向代理
  • ESP32 小智 AI 机器人语音对话系统设计与云端部署
  • C++ 设计模式详解:创建型、结构型与行为型实战
  • 六自由度机器人逆运动学详解及 MATLAB 实现
  • 前端监控实践:错误、性能与用户行为追踪
  • 深度拆解美团闪购 AIGC 营销案例:技术如何赋能品牌叙事
  • 零基础黑客入门:普通人成为安全专家的十个步骤
  • Rust 异步并发安全与内存管理最佳实践
  • 前端国际化开发实践:基于 i18next
  • Python + Neo4j 构建知识图谱入门实战
  • Docker 镜像国内拉取加速方案:使用渡渡鸟镜像站
  • Python 和 PyCharm 安装配置教程
  • C++ 继承中同名成员的隐藏规则解析
  • React Native Android 集成虹软 ArcFace 人脸识别实战方案
  • 基于 DroneVehicle 数据集的 YOLOv11 无人机车辆目标检测
  • CCF GESP C++2 级编程能力认证试题
  • Qt与Web混合编程:CEF与QCefView深度解析
  • Impala 分布式环境性能优化实战指南(下)

相关免费在线工具

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online

  • JSON美化和格式化

    将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online