基于Qt Creator的Serial Port串口调试助手项目(代码开源)

前言: 本文为手把手教学的基于 Qt Creator的 Serial Port 串口调试助手项目教程,项目使用 Qt 版本为 Qt 5.9.0,整体上实现了 Serial Port 串口助手所有的功能。本项目的 Serial Port 串口助手除了设计出常规的 Serial Port 串口打印功能外,还额外模仿了著名软件 VOFA+ 的上位机波形实时输出功能。本项目代码实现偏简单可行,可能存在很多可以优化性能的地方,亦或可能存在功能性上的 BUG ,欢迎各位读者朋友们使用该代码或在此代码框架上进行二次开发。当然,也欢迎各位读者朋友们提出珍贵的改进建议(篇末代码开源)!

软件界面:

所需环境和软件工具:

  1. Qt Creator 4.3.0

  2. Qt 5.9.0

  3. Window10

一、Serial Port串口助手

串口助手(Serial Port Debugger)是一款运行在计算机(PC)上的软件工具,其核心功能是通过计算机的物理串行通信接口(如 COM 口、USB 转串口适配器)或虚拟串口,与外部具备串行通信能力的硬件设备进行双向数据交互。它是嵌入式系统开发、工业控制、仪器仪表调试、物联网设备通信等领域的基础且至关重要的工具。本项目在传统的 Serial Port 助手的功能上还提供了 Plot 绘制的功能,该功能模仿了超级优秀的串口上位机 VOFA+ 进行设计,如下为 VOFA+ 这款非常优秀的串口调试助手界面:

本篇博客的 Serial Port 串口调试助手是模仿 VOFA+ 这款优秀上位机进行制作的,使用的开发软件为 Qt Creator 4.3.0,Qt 的版本为Qt 5.9.0。考虑到需要实现 Plot 波形数据绘制,还引入了 QCustomPlot 这个图形库,利用 QCustomPlot 提供的 API 函数实现 Plot 的绘制。本项目的 Serial Port 串口助手的 Plot 波形绘制功能如下:

二、Serial Port布局 ui 画面

2.1 Serial Port整体布局

一般情况下,为了保证设计的 Serial Port 助手的布局合理性,可能会固定窗口大小,共有 2 种方法;

方案 1:使用代码 this->setFixedSize(this->size());其中这里的this表示所需要固定的窗口。

方案 2:调整 ui 窗口的最大值与最小值,即使得二者相等,如下图所示:

2.2 功能性按键布局

Serial Port 串口助手的功能布局:

1、串口选择:USB_CH340_COM;myComboBox

2、波特率:1200~1382400;QComboBox

3、停止位:1、1.5、2;QComboBox

4、数据位:5、6、7、8;QComboBox

5、奇偶校验:无、奇校验、偶校验;QComboBox

6、串口操作:控制串口的打开;QPushButton
Serial Port 计数的功能布局:

1、发送计数:发送的字节数量;QLineEdit

2、接收计数:接收的字节数量;QLineEdit

3、发送速度:发送的字节速度;QLineEdit

4、接收速度:接收的字节速度;QLineEdit

5、清除接收/统计:清除接收内容与接收计数;QPushButton

6、清除发送:清除发送内容与发送计数;QPushButton

2.3 数据接收与发送布局

Serial Port 串口数据接收:

打开串口助手后,默认串口数据-接收区将会一直接收下位机发送过来的数据。该数据接收区的 Qt 部件为 QPlainTextEdit,读者朋友们可以自行设计其 Size() 大小、Font 字符内容和 Format 格式等

Serial Port 串口数据发送:

打开串口助手后,可以在串口数据发送区输入文本内容,点击发送按钮 PushButton 即可;亦或是使用定时器发送数据内容。补充说明:数据接收与数据发送区都可以使用 16 进制数据进行处理。

Serial Port 的串口数据接收区和发送区都属于 Qt 中的 QPlainTextEdit;

Serial Port 最终布局:

三、Serial Port 串口功能实现

3.1 Serial Port 初始化与接收

Serial Port 初始化需要选择 PC端现在存在的 COM 端口,之后需要设置 Serial Port 波特率、数据的停止位、数据位和奇偶校验。然后,点击串口操作按钮 PushButton 打开串口,作者这里使用了 Qt 槽函数进行连接;

补充说明:Qt 提供了一系列特别方便的 Serial Port 串口助手库函数,我们可以直接使用 mySerialPort->readAll() 和 mySerialPort->writeAll() 针对串口提供的数据进行操作;

cpp 复制代码
// Serial Port串口设置初始化
void MainWindow::on_btnSwitch_clicked()
{
    QSerialPort::BaudRate baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::StopBits stopBits;
    QSerialPort::Parity   checkBits;

    // 获取串口波特率
    baudRate = (QSerialPort::BaudRate)ui->cmbBaudRate->currentText().toUInt();
    // 获取串口数据位
    dataBits = (QSerialPort::DataBits)ui->cmbData->currentText().toUInt();
    // 获取串口停止位
    if(ui->cmbStop->currentText() == "1"){
        stopBits = QSerialPort::OneStop;
    }else if(ui->cmbStop->currentText() == "1.5"){
        stopBits = QSerialPort::OneAndHalfStop;
    }else if(ui->cmbStop->currentText() == "2"){
        stopBits = QSerialPort::TwoStop;
    }else{
        stopBits = QSerialPort::OneStop;
    }

    // 获取串口奇偶校验位
    if(ui->cmbCheck->currentText() == "无"){
        checkBits = QSerialPort::NoParity;
    }else if(ui->cmbCheck->currentText() == "奇校验"){
        checkBits = QSerialPort::OddParity;
    }else if(ui->cmbCheck->currentText() == "偶校验"){
        checkBits = QSerialPort::EvenParity;
    }else{
        checkBits = QSerialPort::NoParity;
    }

    // 想想用 substr strchr怎么从带有信息的字符串中提前串口号字符串
    // 初始化串口属性,设置 端口号、波特率、数据位、停止位、奇偶校验位数
    mySerialPort->setBaudRate(baudRate);
    mySerialPort->setDataBits(dataBits);
    mySerialPort->setStopBits(stopBits);
    mySerialPort->setParity(checkBits);
    //mySerialPort->setPortName(ui->cmbSerialPort->currentText());// 不匹配带有串口设备信息的文本
    // 匹配带有串口设备信息的文本
    QString spTxt = ui->cmbSerialPort->currentText();
    spTxt = spTxt.section(':', 0, 0);//spTxt.mid(0, spTxt.indexOf(":"));
    //qDebug() << spTxt;
    mySerialPort->setPortName(spTxt);

    // 根据初始化好的串口属性,打开串口
    // 如果打开成功,反转打开按钮显示和功能。打开失败,无变化,并且弹出错误对话框。
    if(ui->btnSwitch->text() == "打开串口"){
        if(mySerialPort->open(QIODevice::ReadWrite) == true){
            ui->btnSwitch->setText("关闭串口");
            // 让端口号下拉框不可选,避免误操作(选择功能不可用,控件背景为灰色)
            ui->cmbSerialPort->setEnabled(false);
            ui->cmbBaudRate->setEnabled(false);
            ui->cmbStop->setEnabled(false);
            ui->cmbData->setEnabled(false);
            ui->cmbCheck->setEnabled(false);
        }else{
            QMessageBox::critical(this, "错误提示", "串口打开失败!!!\r\n\r\n该串口可能被占用,请选择正确的串口\r\n或者波特率过高,超出硬件支持范围");
        }
    }else{
        mySerialPort->close();
        ui->btnSwitch->setText("打开串口");
        // 端口号下拉框恢复可选,避免误操作
        ui->cmbSerialPort->setEnabled(true);
        ui->cmbBaudRate->setEnabled(true);
        ui->cmbStop->setEnabled(true);
        ui->cmbData->setEnabled(true);
        ui->cmbCheck->setEnabled(true);
    }
}
cpp 复制代码
// 串口接收显示,槽函数
void MainWindow::serialPortRead_Slot()
{
    /* 利用QtSerial库接收数据 */
    QByteArray recBuf = mySerialPort->readAll();
    Plot_Num = recBuf;
    // 判断是否为16进制接收,将以后接收的数据全部转换为16进制显示(先前接收的部分在多选框槽函数中进行转换。最好多选框和接收区组成一个自定义控件,方便以后调用)
    if(ui->chkRec->checkState() == false){
        // GB2312编码输入
        QString strb = QString::fromLocal8Bit(recBuf);//QString::fromUtf8(recBuf);//QString::fromLatin1(recBuf);
        // 在当前位置插入文本,不会发生换行。如果没有移动光标到文件结尾,会导致文件超出当前界面显示范围,界面也不会向下滚动。
        ui->txtRec->insertPlainText(strb);
    }else{
        // 16进制显示,并转换为大写
        QString str1 = recBuf.toHex().toUpper();//.data();
        // 添加空格
        QString str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        ui->txtRec->insertPlainText(str2);
    }

    /* 1.计算接收到的字节数 */
    RecvNum += recBuf.size();

    /* 2.格式化并显示总字节数 */
    ui->RxCount->setText(QString::number(RecvNum));

    /* 3.计算并显示接收速度 */
    QString speedText = calculateSpeed(RecvNum);
    ui->RxSpeed->setText(speedText);

    /* 4.更新上一次记录(用于下次计算速度)*/
    lastRecvNum = RecvNum;
    lastUpdateTime = QDateTime::currentMSecsSinceEpoch();

    /* 5.移动光标到文本结尾 */
    ui->txtRec->moveCursor(QTextCursor::End);
}

3.2 Serial Port 串口数据发送

Serial Port 串口助手通常拥有定时发送功能,例如:1000ms 进行一次数据发送。当然,还拥有 16 进制数转换的功能进行 Data 数据接收和发送。作者这边提供详细的代码,读者朋友可以直接参考作者提供的开源代码。

cpp 复制代码
// 串口发送数据
void MainWindow::on_btnSend_clicked()
{
    QByteArray sendData;

    // 判断是否为16进制发送,将发送区全部的asc2转换为16进制字符串显示,发送的时候转换为16进制发送
    if(ui->chkSend->checkState() == false){
        // 字符串形式发送,GB2312编码用以兼容大多数单片机
        sendData = ui->txtSend->toPlainText().toLocal8Bit();// GB2312编码输出
    }else{
        // 16进制发送,不要用.data(),.data()返回的是字符数组,0x00在ASC2中的意义为NUL,也就是'\0'结束符,所以遇到0x00就会终止
        sendData = QByteArray::fromHex(ui->txtSend->toPlainText().toLocal8Bit());// GB2312编码输出
    }

    // 记录发送前的时间戳
    qint64 sendStartTime = QDateTime::currentMSecsSinceEpoch();

    // 发送数据
    int bytesSent = mySerialPort->write(sendData);

    // 发送字节计数并显示
    if(bytesSent > 0) {
        // 更新总发送字节数
        SendNum += bytesSent;
        ui->TxCount->setText(QString::number(SendNum));

        // 记录发送结束时间
        qint64 sendEndTime = QDateTime::currentMSecsSinceEpoch();
        qint64 sendDuration = sendEndTime - sendStartTime;

        // 计算并显示发送速度
        calculateSendSpeed(bytesSent, sendDuration);
    }
}

// 16进制发送触发按键
void MainWindow::on_chkSend_stateChanged(int arg1)
{
    // 获取文本字符串
    QString txtBuf = ui->txtSend->toPlainText();

    // 获取多选框状态,未选为0,选中为2
    // 为0时,多选框未被勾选,将先前的发送区的16进制字符串转换为asc2字符串
    if(arg1 == 0){

        //QByteArray str1 = QByteArray::fromHex(txtBuf.toUtf8());//仅能处理Unicode编码,因为QString就是Unicode
        //QString str1 = QString::fromLocal8Bit(txtBuf.toUtf8());//仅能处理GB2312编码,Unicode的数据无论如何都会乱码
        //把gb2312编码转换成unicode
        QString str1 = QTextCodec::codecForName("GB2312")->toUnicode(QByteArray::fromHex(txtBuf.toLocal8Bit()));
        // 文本控件清屏,显示新文本
        ui->txtSend->clear();
        ui->txtSend->insertPlainText(str1);
        // 移动光标到文本结尾
        ui->txtSend->moveCursor(QTextCursor::End);

    }else{// 多选框被勾选,将先前的发送区的asc2字符串转换为16进制字符串

        //QByteArray str1 = txtBuf.toUtf8().toHex().toUpper();// Unicode编码输出
        QString str1 = txtBuf.toLocal8Bit().toHex().toUpper();// GB2312编码输出
        // 添加空格
        QString str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        // 文本控件清屏,显示新文本
        ui->txtSend->clear();
        ui->txtSend->insertPlainText(str2);
        // 移动光标到文本结尾
        ui->txtSend->moveCursor(QTextCursor::End);
    }
}

3.3 Serial Port 计数功能

Serial Port 的发送速度:

cpp 复制代码
// 计算发送速度(字节/秒)
void MainWindow::calculateSendSpeed(qint64 bytesSent, qint64 durationMs)
{
    // 处理零时间间隔(理论不可能,但安全处理)
    if (durationMs == 0) durationMs = 1;

    // 计算瞬时速度(字节/秒)
    double instantSpeed = (bytesSent * 1000.0) / durationMs;

    // 应用指数平滑滤波(减少数值跳动)
    const double smoothingFactor = 0.3;
    smoothedSendSpeed = smoothingFactor * instantSpeed + (1 - smoothingFactor) * smoothedSendSpeed;

    // 单位转换与显示
    QString speedText;
    if (smoothedSendSpeed >= 1024 * 1024 * 1024) { // GB/s
        speedText = QString("%1 GB/s").arg(smoothedSendSpeed / (1024 * 1024 * 1024), 0, 'f', 2);
    } else if (smoothedSendSpeed >= 1024 * 1024) { // MB/s
        speedText = QString("%1 MB/s").arg(smoothedSendSpeed / (1024 * 1024), 0, 'f', 2);
    } else if (smoothedSendSpeed >= 1024) { // KB/s
        speedText = QString("%1 KB/s").arg(smoothedSendSpeed / 1024, 0, 'f', 2);
    } else { // B/s
        speedText = QString("%1 B/s").arg(smoothedSendSpeed, 0, 'f', 2);
    }

    // 更新UI显示
    ui->TxSpeed->setText(speedText);

    // 更新最后发送状态
    lastSendTime = QDateTime::currentMSecsSinceEpoch();
    lastSendNum = SendNum;
}

Serial Port 的接收速度:

cpp 复制代码
// 计算接收速度(字节/秒)
QString MainWindow::calculateSpeed(qint64 currentNum)
{
    qint64 currentTime = QDateTime::currentMSecsSinceEpoch();
    qint64 deltaTime = currentTime - lastUpdateTime;

    if (deltaTime == 0 || lastRecvNum == 0) {
        // 首次计算或时间未变化时返回0
        lastRecvNum = currentNum;
        lastUpdateTime = currentTime;
        return "0 B/s";
    }

    // 计算每秒接收字节数
    double deltaBytes = currentNum - lastRecvNum;
    double speed = deltaBytes / deltaTime * 1000; // 转换为秒

    // 速度单位转换
    if (speed >= 1024 * 1024 * 1024) { // GB/s
        return QString("%1 GB/s").arg(speed / (1024 * 1024 * 1024), 0, 'f', 2);
    } else if (speed >= 1024 * 1024) { // MB/s
        return QString("%1 MB/s").arg(speed / (1024 * 1024), 0, 'f', 2);
    } else if (speed >= 1024) { // KB/s
        return QString("%1 KB/s").arg(speed / 1024, 0, 'f', 2);
    } else { // B/s
        return QString("%1 B/s").arg(speed, 0, 'f', 2);
    }
}

四、Waveform 波形功能实现

4.1 引入 QCustomPlot

QCustomPlot 是一个超强超小巧的 Qt 绘图类,非常漂亮,非常易用,只需要加入一个 qcustomplot.h 和 qcustomplot.cpp 文件即可使用,远比 qwt 方便和漂亮,可以自己使用两个源文件也可以自己编译成库文件,非常方便。

官方网站:http://www.qcustomplot.com/

进入 QCustomPlot 下载页,下载最新的完整包(包含:源码、文档、示例)!

将下载好的安装包进行解压缩,里面包含文档、示例、更改日志、GPL 授权、以及最重要的两个文件 qcustomplot.h 与 qcustomplot.cpp。

在 Examples 中我们会看到一些自带的示例,可以运行看一下效果。

如果在自己的项目中使用,需要进行以下配置:

首先,在 pro 中需要添加(由于 QCustomPlot 中存在导出功能,使用了 printsupport 模块):

cpp 复制代码
QT += printsupport

然后,将 qcustomplot.h 与 qcustomplot.cpp 拷贝到工程目录下,右键 -> 添加现有文件...,将这两个文件添加至工程。在调用 qcustomplot 的地方,需要引入,才可成功创建一个 qcustomplot 对象:

cpp 复制代码
#include "qcustomplot.h"
/* 创建qcustomplot */
QCustomPlot *pCustomPlot = new QCustomPlot(this);

4.2 Plot ui 格局设计

Plot 波形绘制的区域使用了 QCustomPlot 库中的 QCustomPlot 的类,曲线/散点设置则是一系列 Qt 自带的类进行设计的。最终的 Plot 绘制的情况如下图:

4.3 Plot data 数据筛选

作者设计的 Plot 绘制波形的数据筛选使用了如下格式,且最高支持8个波形的同时绘制:

cpp 复制代码
printf("simples:%f, %f\n", sin(t1), sin(t2)); 

Plot data 数据的筛选部分代码如下:

cpp 复制代码
// 数据清洗与转换
QString str = QString::fromUtf8(Plot_Num)
              .remove(" ")
              .replace(":", ",")
              .replace(":",  ",")
              .replace(",", ",");

// 拆分字符串
QStringList parts = str.split(",",  QString::SkipEmptyParts );

// 提取数字
QVector<double> numbers;
for (const QString& part : parts) {
    bool ok;
    double num = part.toDouble(&ok);
    if (ok) numbers.append(num);
}

巧妙的使用了 Qt 和 C++ 库函数进行操作,替换原数据文本中不符合要求的内容,在 QVector 容器的基础上进行有效数据的提取!

4.4 Plot 波形图绘制

作者使用的方式是定时器每 10ms 将串口接收到的数据进去筛选提取,并利用 QCustomPlot 库进行 Plot 的绘制(由于很多情况下,串口接收速率是比 Plot 绘制提取的速率快的,这将不可避免地导致数据缺失,读者朋友可以根据直接实际情况使用一个环形缓存区进行缓存处理)

Plot 波形绘图定时器设计:

cpp 复制代码
// 创建定时器,用于定时生成曲线坐标点数据
timer = new QTimer(this);
timer->setInterval(10);
connect(timer,SIGNAL(timeout()),this,SLOT(TimeData_Update()));
timer->start(10);
cpp 复制代码
// 定时器溢出处理槽函数。用来生成曲线的坐标数据。
void plot::TimeData_Update(void)
{
    // 数据清洗与转换
    QString str = QString::fromUtf8(Plot_Num)
                  .remove(" ")
                  .replace(":", ",")
                  .replace(":",  ",")
                  .replace(",", ",");

    // 拆分字符串
    QStringList parts = str.split(",",  QString::SkipEmptyParts );

    // 提取数字
    QVector<double> numbers;
    for (const QString& part : parts) {
        bool ok;
        double num = part.toDouble(&ok);
        if (ok) numbers.append(num);
    }

    // 计算需要绘制几个波形,移动x轴的数据
    int n = numbers.size();
    cnt++;
    // 给曲线添加数据
    for(int i=0; i<n; i++){
        pTxtValueCurve[i]->setText(QString::number(numbers[i],'g',8));// 显示曲线当前值
        pCurve[i]->addData(cnt, numbers[i]);
    }

    // 设置x坐标轴显示范围,使其自适应缩放x轴,x轴最大显示pointCountX个点。与chkTrackX复选框有关
    if(ui->chkTrackX->checkState()){
        //customPlot->xAxis->setRange((pCurve[0]->dataCount()>pointCountX)?(pCurve[0]->dataCount()-pointCountX):0, pCurve[0]->dataCount());
        setAutoTrackX(pPlot1);
    }
    // 设置y坐标轴显示范围,使其自适应曲线缩放
    if(ui->chkAdjustY->checkState()){
        setAutoTrackY(pPlot1);
    }

    // 更新绘图,这种方式在高填充下太浪费资源。有另一种方式rpQueuedReplot,可避免重复绘图。
    // 最好的方法还是将数据填充、和更新绘图分隔开。将更新绘图单独用定时器更新。例程数据量较少没用单独定时器更新,实际工程中建议大家加上。
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

五、代码解析与各项功能

5.1 mainwindow.cpp

本代码为 iKun Serial Port Debugger 串口调试助手的主界面代码功能,作者在上方拆解了部分 Serial Port 功能代码的实现原理与代码。本项目更多的细节点读者朋友可以参考下发的代码内容,其中包括 SendSpeed 和 RecvSpeed 速度的实时更新等。一个优秀的 APP 项目肯定是需要不断打磨的,后期作者会把本项目已知的不足与打算改进的地方就行更新!

cpp 复制代码
/******************************************************************
* @projectName   iKun Serial Port Debugger
* @brief         mainwindow.cpp
* @author        混分巨兽龙某某
* @email         1178305328@qq.com
*******************************************************************/
#include "mainwindow.h"
#include "ui_mainwindow.h"
#include "plot.h"

MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    setWindowTitle("iKun Serial Port Debugger");        /* 修改APP标题 */

    // 状态栏
    QStatusBar *sBar = statusBar();

    /* 实例化两个按钮对象,并设置其显示文本为窗口皮肤1和窗口皮肤2 */
    pushButton1 = new QPushButton("作者信息", this);

    /* 设定两个QPushButton对象的位置 */
    sBar->addPermanentWidget(pushButton1);

    // 状态栏添加超链接
    QLabel *lblLinkBlog = new QLabel(this);
    lblLinkBlog->setOpenExternalLinks(true);
    lblLinkBlog->setText("<style> a {text-decoration: none} </style> <a href=\"https://blog.csdn.net/black_sneak?type=blog\">博客");// 无下划线
    QLabel *lblLinkSource = new QLabel(this);
    lblLinkSource->setOpenExternalLinks(true);
    lblLinkSource->setText("<style> a {text-decoration: none} </style> <a href=\"https://blog.csdn.net/black_sneak/article/details/151232098\">源码下载");// 无下划线
    lblLinkBlog->setMinimumSize(40, 20);
    lblLinkSource->setMinimumSize(60, 20);
    // 从左往右依次添加
    sBar->addWidget(lblLinkBlog);
    sBar->addWidget(lblLinkSource);

    // 定时发送-定时器
    timSend = new QTimer;
    timSend->setInterval(1000);// 设置默认定时时长1000ms
    connect(timSend, &QTimer::timeout, this, [=](){
        on_btnSend_clicked();
    });

    // 初始化清零定时器(每500ms检查一次)
    speedClearTimer = new QTimer(this);
    connect(speedClearTimer, &QTimer::timeout, this, &MainWindow::checkSpeedClear);
    speedClearTimer->start(500);

    // 新建一串口对象
    mySerialPort = new QSerialPort(this);

    // 串口接收,信号槽关联
    connect(mySerialPort, SIGNAL(readyRead()), this, SLOT(serialPortRead_Slot()));

    // 信号槽连接,打开作者信息的按钮
    connect(pushButton1, SIGNAL(clicked()), this, SLOT(pushButton1_Clicked()));
}

MainWindow::~MainWindow()
{
    delete ui;
}

// 作者信息的信号槽函数
void MainWindow::pushButton1_Clicked()
{
    QMessageBox::information(NULL, "作者信息","作者:混粉巨兽龙某某\n联系方式:1178305328\n版本信息:V1.0");
}

// 调用Plot界面操作
void MainWindow::on_pushButton_clicked()
{
    plot *ConfigWindow = new plot;
    ConfigWindow->show();
}

// Serial Port串口设置初始化
void MainWindow::on_btnSwitch_clicked()
{
    QSerialPort::BaudRate baudRate;
    QSerialPort::DataBits dataBits;
    QSerialPort::StopBits stopBits;
    QSerialPort::Parity   checkBits;

    // 获取串口波特率
    baudRate = (QSerialPort::BaudRate)ui->cmbBaudRate->currentText().toUInt();
    // 获取串口数据位
    dataBits = (QSerialPort::DataBits)ui->cmbData->currentText().toUInt();
    // 获取串口停止位
    if(ui->cmbStop->currentText() == "1"){
        stopBits = QSerialPort::OneStop;
    }else if(ui->cmbStop->currentText() == "1.5"){
        stopBits = QSerialPort::OneAndHalfStop;
    }else if(ui->cmbStop->currentText() == "2"){
        stopBits = QSerialPort::TwoStop;
    }else{
        stopBits = QSerialPort::OneStop;
    }

    // 获取串口奇偶校验位
    if(ui->cmbCheck->currentText() == "无"){
        checkBits = QSerialPort::NoParity;
    }else if(ui->cmbCheck->currentText() == "奇校验"){
        checkBits = QSerialPort::OddParity;
    }else if(ui->cmbCheck->currentText() == "偶校验"){
        checkBits = QSerialPort::EvenParity;
    }else{
        checkBits = QSerialPort::NoParity;
    }

    // 想想用 substr strchr怎么从带有信息的字符串中提前串口号字符串
    // 初始化串口属性,设置 端口号、波特率、数据位、停止位、奇偶校验位数
    mySerialPort->setBaudRate(baudRate);
    mySerialPort->setDataBits(dataBits);
    mySerialPort->setStopBits(stopBits);
    mySerialPort->setParity(checkBits);
    //mySerialPort->setPortName(ui->cmbSerialPort->currentText());// 不匹配带有串口设备信息的文本
    // 匹配带有串口设备信息的文本
    QString spTxt = ui->cmbSerialPort->currentText();
    spTxt = spTxt.section(':', 0, 0);//spTxt.mid(0, spTxt.indexOf(":"));
    //qDebug() << spTxt;
    mySerialPort->setPortName(spTxt);

    // 根据初始化好的串口属性,打开串口
    // 如果打开成功,反转打开按钮显示和功能。打开失败,无变化,并且弹出错误对话框。
    if(ui->btnSwitch->text() == "打开串口"){
        if(mySerialPort->open(QIODevice::ReadWrite) == true){
            ui->btnSwitch->setText("关闭串口");
            // 让端口号下拉框不可选,避免误操作(选择功能不可用,控件背景为灰色)
            ui->cmbSerialPort->setEnabled(false);
            ui->cmbBaudRate->setEnabled(false);
            ui->cmbStop->setEnabled(false);
            ui->cmbData->setEnabled(false);
            ui->cmbCheck->setEnabled(false);
        }else{
            QMessageBox::critical(this, "错误提示", "串口打开失败!!!\r\n\r\n该串口可能被占用,请选择正确的串口\r\n或者波特率过高,超出硬件支持范围");
        }
    }else{
        mySerialPort->close();
        ui->btnSwitch->setText("打开串口");
        // 端口号下拉框恢复可选,避免误操作
        ui->cmbSerialPort->setEnabled(true);
        ui->cmbBaudRate->setEnabled(true);
        ui->cmbStop->setEnabled(true);
        ui->cmbData->setEnabled(true);
        ui->cmbCheck->setEnabled(true);
    }
}

// 串口接收显示,槽函数
void MainWindow::serialPortRead_Slot()
{
    /* 利用QtSerial库接收数据 */
    QByteArray recBuf = mySerialPort->readAll();
    Plot_Num = recBuf;
    // 判断是否为16进制接收,将以后接收的数据全部转换为16进制显示(先前接收的部分在多选框槽函数中进行转换。最好多选框和接收区组成一个自定义控件,方便以后调用)
    if(ui->chkRec->checkState() == false){
        // GB2312编码输入
        QString strb = QString::fromLocal8Bit(recBuf);//QString::fromUtf8(recBuf);//QString::fromLatin1(recBuf);
        // 在当前位置插入文本,不会发生换行。如果没有移动光标到文件结尾,会导致文件超出当前界面显示范围,界面也不会向下滚动。
        ui->txtRec->insertPlainText(strb);
    }else{
        // 16进制显示,并转换为大写
        QString str1 = recBuf.toHex().toUpper();//.data();
        // 添加空格
        QString str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        ui->txtRec->insertPlainText(str2);
    }

    /* 1.计算接收到的字节数 */
    RecvNum += recBuf.size();

    /* 2.格式化并显示总字节数 */
    ui->RxCount->setText(QString::number(RecvNum));

    /* 3.计算并显示接收速度 */
    QString speedText = calculateSpeed(RecvNum);
    ui->RxSpeed->setText(speedText);

    /* 4.更新上一次记录(用于下次计算速度)*/
    lastRecvNum = RecvNum;
    lastUpdateTime = QDateTime::currentMSecsSinceEpoch();

    /* 5.移动光标到文本结尾 */
    ui->txtRec->moveCursor(QTextCursor::End);
}

// 计算接收速度(字节/秒)
QString MainWindow::calculateSpeed(qint64 currentNum)
{
    qint64 currentTime = QDateTime::currentMSecsSinceEpoch();
    qint64 deltaTime = currentTime - lastUpdateTime;

    if (deltaTime == 0 || lastRecvNum == 0) {
        // 首次计算或时间未变化时返回0
        lastRecvNum = currentNum;
        lastUpdateTime = currentTime;
        return "0 B/s";
    }

    // 计算每秒接收字节数
    double deltaBytes = currentNum - lastRecvNum;
    double speed = deltaBytes / deltaTime * 1000; // 转换为秒

    // 速度单位转换
    if (speed >= 1024 * 1024 * 1024) { // GB/s
        return QString("%1 GB/s").arg(speed / (1024 * 1024 * 1024), 0, 'f', 2);
    } else if (speed >= 1024 * 1024) { // MB/s
        return QString("%1 MB/s").arg(speed / (1024 * 1024), 0, 'f', 2);
    } else if (speed >= 1024) { // KB/s
        return QString("%1 KB/s").arg(speed / 1024, 0, 'f', 2);
    } else { // B/s
        return QString("%1 B/s").arg(speed, 0, 'f', 2);
    }
}

// 计算发送速度(字节/秒)
void MainWindow::calculateSendSpeed(qint64 bytesSent, qint64 durationMs)
{
    // 处理零时间间隔(理论不可能,但安全处理)
    if (durationMs == 0) durationMs = 1;

    // 计算瞬时速度(字节/秒)
    double instantSpeed = (bytesSent * 1000.0) / durationMs;

    // 应用指数平滑滤波(减少数值跳动)
    const double smoothingFactor = 0.3;
    smoothedSendSpeed = smoothingFactor * instantSpeed + (1 - smoothingFactor) * smoothedSendSpeed;

    // 单位转换与显示
    QString speedText;
    if (smoothedSendSpeed >= 1024 * 1024 * 1024) { // GB/s
        speedText = QString("%1 GB/s").arg(smoothedSendSpeed / (1024 * 1024 * 1024), 0, 'f', 2);
    } else if (smoothedSendSpeed >= 1024 * 1024) { // MB/s
        speedText = QString("%1 MB/s").arg(smoothedSendSpeed / (1024 * 1024), 0, 'f', 2);
    } else if (smoothedSendSpeed >= 1024) { // KB/s
        speedText = QString("%1 KB/s").arg(smoothedSendSpeed / 1024, 0, 'f', 2);
    } else { // B/s
        speedText = QString("%1 B/s").arg(smoothedSendSpeed, 0, 'f', 2);
    }

    // 更新UI显示
    ui->TxSpeed->setText(speedText);

    // 更新最后发送状态
    lastSendTime = QDateTime::currentMSecsSinceEpoch();
    lastSendNum = SendNum;
}

// 新增:速度清零检查函数
void MainWindow::checkSpeedClear()
{
    qint64 currentTime = QDateTime::currentMSecsSinceEpoch();

    // 超过1秒无数据时清零显示
    if (currentTime - lastUpdateTime > 1000) {
        ui->RxSpeed->setText("0 B/s");
        ui->TxSpeed->setText("0 B/s");
    }
}

// 串口发送数据
void MainWindow::on_btnSend_clicked()
{
    QByteArray sendData;

    // 判断是否为16进制发送,将发送区全部的asc2转换为16进制字符串显示,发送的时候转换为16进制发送
    if(ui->chkSend->checkState() == false){
        // 字符串形式发送,GB2312编码用以兼容大多数单片机
        sendData = ui->txtSend->toPlainText().toLocal8Bit();// GB2312编码输出
    }else{
        // 16进制发送,不要用.data(),.data()返回的是字符数组,0x00在ASC2中的意义为NUL,也就是'\0'结束符,所以遇到0x00就会终止
        sendData = QByteArray::fromHex(ui->txtSend->toPlainText().toLocal8Bit());// GB2312编码输出
    }

    // 记录发送前的时间戳
    qint64 sendStartTime = QDateTime::currentMSecsSinceEpoch();

    // 发送数据
    int bytesSent = mySerialPort->write(sendData);

    // 发送字节计数并显示
    if(bytesSent > 0) {
        // 更新总发送字节数
        SendNum += bytesSent;
        ui->TxCount->setText(QString::number(SendNum));

        // 记录发送结束时间
        qint64 sendEndTime = QDateTime::currentMSecsSinceEpoch();
        qint64 sendDuration = sendEndTime - sendStartTime;

        // 计算并显示发送速度
        calculateSendSpeed(bytesSent, sendDuration);
    }
}

// 16进制发送触发按键
void MainWindow::on_chkSend_stateChanged(int arg1)
{
    // 获取文本字符串
    QString txtBuf = ui->txtSend->toPlainText();

    // 获取多选框状态,未选为0,选中为2
    // 为0时,多选框未被勾选,将先前的发送区的16进制字符串转换为asc2字符串
    if(arg1 == 0){

        //QByteArray str1 = QByteArray::fromHex(txtBuf.toUtf8());//仅能处理Unicode编码,因为QString就是Unicode
        //QString str1 = QString::fromLocal8Bit(txtBuf.toUtf8());//仅能处理GB2312编码,Unicode的数据无论如何都会乱码
        //把gb2312编码转换成unicode
        QString str1 = QTextCodec::codecForName("GB2312")->toUnicode(QByteArray::fromHex(txtBuf.toLocal8Bit()));
        // 文本控件清屏,显示新文本
        ui->txtSend->clear();
        ui->txtSend->insertPlainText(str1);
        // 移动光标到文本结尾
        ui->txtSend->moveCursor(QTextCursor::End);

    }else{// 多选框被勾选,将先前的发送区的asc2字符串转换为16进制字符串

        //QByteArray str1 = txtBuf.toUtf8().toHex().toUpper();// Unicode编码输出
        QString str1 = txtBuf.toLocal8Bit().toHex().toUpper();// GB2312编码输出
        // 添加空格
        QString str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        // 文本控件清屏,显示新文本
        ui->txtSend->clear();
        ui->txtSend->insertPlainText(str2);
        // 移动光标到文本结尾
        ui->txtSend->moveCursor(QTextCursor::End);
    }
}

// 定时器发送触发按键操作
void MainWindow::on_chkTimSend_stateChanged(int arg1)
{
    // 获取复选框状态,未选为0,选中为2
    if(arg1 == 0){
        timSend->stop();
        // 时间输入框恢复可选
        ui->txtSendMs->setEnabled(true);
    }else{
        // 对输入的值做限幅,小于20ms会弹出对话框提示
        if(ui->txtSendMs->text().toInt() >= 20){
            timSend->start(ui->txtSendMs->text().toInt());// 设置定时时长,重新计数
            // 让时间输入框不可选,避免误操作(输入功能不可用,控件背景为灰色)
            ui->txtSendMs->setEnabled(false);
        }else{
            ui->chkTimSend->setCheckState(Qt::Unchecked);
            QMessageBox::critical(this, "错误提示", "定时发送的最小间隔为 20ms\r\n请确保输入的值 >=20");
        }
    }
}

//
void MainWindow::on_btnClearRec_clicked()
{
    ui->txtRec->clear();
    // 清除接收字节计数
    RecvNum = 0;
    ui->RxCount->setText(QString::number(RecvNum));
}


void MainWindow::on_btnClearSend_clicked()
{
    ui->txtSend->clear();
    // 清除发送字节计数
    SendNum = 0;
    ui->TxCount->setText(QString::number(SendNum));
}

// 16进制的串口数据接收
void MainWindow::on_chkRec_stateChanged(int arg1)
{
    // 获取文本字符串
    QString txtBuf = ui->txtRec->toPlainText();

    // 获取多选框状态,未选为0,选中为2
    // 为0时,多选框未被勾选,接收区先前接收的16进制数据转换为asc2字符串格式
    if(arg1 == 0){

        //QString str1 = QByteArray::fromHex(txtBuf.toUtf8());
        //QString str1 = QByteArray::fromHex(txtBuf.toLocal8Bit());
        //把gb2312编码转换成unicode
        QString str1 = QTextCodec::codecForName("GB2312")->toUnicode(QByteArray::fromHex(txtBuf.toLocal8Bit()));
        // 文本控件清屏,显示新文本
        ui->txtRec->clear();
        ui->txtRec->insertPlainText(str1);
        // 移动光标到文本结尾
        ui->txtRec->moveCursor(QTextCursor::End);

    }else{// 不为0时,多选框被勾选,接收区先前接收asc2字符串转换为16进制显示

        //QString str1 = txtBuf.toUtf8().toHex().toUpper();// Unicode编码输出
        QString str1 = txtBuf.toLocal8Bit().toHex().toUpper();// GB2312编码输出
        // 添加空格
        QByteArray str2;
        for(int i = 0; i<str1.length (); i+=2)
        {
            str2 += str1.mid (i,2);
            str2 += " ";
        }
        // 文本控件清屏,显示新文本
        ui->txtRec->clear();
        ui->txtRec->insertPlainText(str2);
        // 移动光标到文本结尾
        ui->txtRec->moveCursor(QTextCursor::End);
    }
}

5.2 mycombobox.cpp

cpp 复制代码
/******************************************************************
* @projectName   Serial Port Scanning Function
* @brief         mycombobox.cpp
* @author        混分巨兽龙某某
* @email         1178305328@qq.com
*******************************************************************/
#include "mycombobox.h"

myComboBox::myComboBox(QWidget *parent) : QComboBox(parent)
{
    // 扫描可用串口
    scanActivePort();
}

// 扫描可用串口
void myComboBox::scanActivePort()
{
    // 先清空列表项,防止多次刷新后重叠
    clear();

    // 串口端口号列表
    QStringList serialPortName;

    // 自动扫描当前可用串口,返回值追加到字符数组中
    foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts()){

    // 携带有串口设备信息的文本
    QString serialPortInfo = info.portName() + ": " + info.description();               // 串口设备信息,芯片/驱动名称
    //QString serialPortInfo = info.portName() + ": " + info.manufacturer();            // 串口设备制造商
    //QString serialPortInfo = info.portName() + ": " + info.serialNumber();            // 串口设备的序列号
    //QString serialPortInfo = info.portName() + ": " + info.systemLocation();          // 串口设备的系统位置

    serialPortName << serialPortInfo;
    }
    /* 可用串口号,显示到串口选择下拉框中 */
    this->addItems(serialPortName);
}

// 重写鼠标点击事件
void myComboBox::mousePressEvent(QMouseEvent *event)
{
    if(event->button() == Qt::LeftButton)
    {
        // 扫描可用串口
        scanActivePort();

        // 弹出下拉框
        showPopup();
    }
}

5.3 plot.cpp

引入 QCustomPlot 之后实现的 Plot 绘制动态波形是比较简单的,核心的部分的代码为串口接收数据的提取,这部分代码作者在上述已经拆解说明了。其余部分代码功能的时候,读者朋友们可以借鉴一下作者提供的开源代码。当然,Plot 波形绘制过程中的 x 和 y轴自适应也挺重要的,读者朋友们也可以学习一下!

cpp 复制代码
/******************************************************************
* @projectName   Serial Port Plot Function
* @brief         plot.cpp
* @author        混分巨兽龙某某
* @email         1178305328@qq.com
*******************************************************************/
#include "plot.h"
#include "ui_plot.h"
#include <stdio.h>
#include "mainwindow.h"

QByteArray Plot_Num;

plot::plot(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::plot)
{
    ui->setupUi(this);
    setWindowTitle("iKun Plot Debugger");

    // 给widget绘图控件,设置个别名,方便书写
    pPlot1 = ui->winPlot;

    // 初始化图表1
    QPlot_init(pPlot1);
    // 绘图图表的设置控件初始化,主要用于关联控件的信号槽
    QPlot_widget_init();

    // 创建定时器,用于定时生成曲线坐标点数据
    timer = new QTimer(this);
    timer->setInterval(10);
    connect(timer,SIGNAL(timeout()),this,SLOT(TimeData_Update()));
    timer->start(10);

    // 关联控件初始化
    ui->txtPointOriginX->setEnabled(false);
    // 图表重绘后,刷新原点坐标和范围
    connect(pPlot1,SIGNAL(afterReplot()),this,SLOT(repPlotCoordinate()));
}

plot::~plot()
{
    delete ui;
}

// 定时器溢出处理槽函数。用来生成曲线的坐标数据。
void plot::TimeData_Update(void)
{
    // 数据清洗与转换
    QString str = QString::fromUtf8(Plot_Num)
                  .remove(" ")
                  .replace(":", ",")
                  .replace(":",  ",")
                  .replace(",", ",");

    // 拆分字符串
    QStringList parts = str.split(",",  QString::SkipEmptyParts );

    // 提取数字
    QVector<double> numbers;
    for (const QString& part : parts) {
        bool ok;
        double num = part.toDouble(&ok);
        if (ok) numbers.append(num);
    }

    // 计算需要绘制几个波形,移动x轴的数据
    int n = numbers.size();
    cnt++;
    // 给曲线添加数据
    for(int i=0; i<n; i++){
        pTxtValueCurve[i]->setText(QString::number(numbers[i],'g',8));// 显示曲线当前值
        pCurve[i]->addData(cnt, numbers[i]);
    }

    // 设置x坐标轴显示范围,使其自适应缩放x轴,x轴最大显示pointCountX个点。与chkTrackX复选框有关
    if(ui->chkTrackX->checkState()){
        //customPlot->xAxis->setRange((pCurve[0]->dataCount()>pointCountX)?(pCurve[0]->dataCount()-pointCountX):0, pCurve[0]->dataCount());
        setAutoTrackX(pPlot1);
    }
    // 设置y坐标轴显示范围,使其自适应曲线缩放
    if(ui->chkAdjustY->checkState()){
        setAutoTrackY(pPlot1);
    }

    // 更新绘图,这种方式在高填充下太浪费资源。有另一种方式rpQueuedReplot,可避免重复绘图。
    // 最好的方法还是将数据填充、和更新绘图分隔开。将更新绘图单独用定时器更新。例程数据量较少没用单独定时器更新,实际工程中建议大家加上。
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

// 绘图图表初始化
void plot::QPlot_init(QCustomPlot *customPlot)
{
    // 添加曲线名称
    QStringList lineNames;//设置图例的文本
    lineNames << "波形1" << "波形2" << "波形3" << "波形4" << "波形5" << "波形6" << "波形7" << "波形8";
    // 曲线初始颜色
    QColor initColor[8] = {QColor(0,146,152), QColor(162,0,124), QColor(241,175,0), QColor(27,79,147), QColor(229,70,70),\
                           QColor(0,140,94), QColor(178,0,31), QColor(91,189,43)};//QColor(255,255,255)};//白色

    // 图表添加20条曲线,并设置初始颜色,和图例名称
    for(int i=0; i<8; i++){
        pCurve[i] = customPlot->addGraph();
        pCurve[i]->setPen(QPen(QColor(initColor[i])));
        pCurve[i]->setName(lineNames.at(i));
    }

    // 设置背景颜色
    customPlot->setBackground(QColor(255,255,255));
    // 设置背景选择框颜色
    ui->btnColourBack->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(QColor(255,255,255).name()));

    // 曲线选择框颜色,与曲线同步颜色。这样写太复杂了,用控件指针数组在下面写过了,记得要在addGraph()之后才有效。
    //ui->btnColourCurve1->setStyleSheet("border:0px solid;background-color:rgb(0,146,152)");
    //ui->btnColourCurve1->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(initColor[0].name()));
    //ui->btnColourCurve20->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(pCurve[]->pen().color().name()));

    // 设置坐标轴名称
    customPlot->xAxis->setLabel("X");
    customPlot->yAxis->setLabel("Y");

    // 设置x,y坐标轴显示范围
    pointCountX = ui->txtPointCountX->text().toUInt();
    pointCountY = ui->txtPointCountY->text().toUInt();
    customPlot->xAxis->setRange(0,pointCountX);
    customPlot->yAxis->setRange(pointCountY/2*-1,pointCountY/2);

    //customPlot->axisRect()->setupFullAxesBox();//四边安装轴并显示
    //customPlot->xAxis->ticker()->setTickOrigin(1);//改变刻度原点为1
    //customPlot->xAxis->setNumberFormat("gbc");//g灵活的格式,b漂亮的指数形式,c乘号改成×
    //customPlot->xAxis->setNumberPrecision(1);//精度1
    customPlot->xAxis->ticker()->setTickCount(ui->txtMainScaleNumX->text().toUInt());//11个主刻度
    customPlot->yAxis->ticker()->setTickCount(ui->txtMainScaleNumY->text().toUInt());//11个主刻度
    customPlot->xAxis->ticker()->setTickStepStrategy(QCPAxisTicker::tssReadability);//可读性优于设置
    customPlot->yAxis->ticker()->setTickStepStrategy(QCPAxisTicker::tssReadability);//可读性优于设置

    // 显示图表的图例
    customPlot->legend->setVisible(true);

    // 设置波形曲线的复选框字体颜色
    //ui->chkVisibleCurve1->setStyleSheet("QCheckBox{color:rgb(255,0,0)}");//设定前景颜色,就是字体颜色

    // 允许用户用鼠标拖动轴范围,以鼠标为中心滚轮缩放,点击选择图形:
    customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
    // 设置鼠标滚轮的缩放倍率,如果不设置默认为0.85,大于1反方向缩放
    //customPlot->axisRect()->setRangeZoomFactor(0.5);
    // 设置鼠标滚轮缩放的轴方向,仅设置垂直轴。垂直轴和水平轴全选使用:Qt::Vertical | Qt::Horizontal
    customPlot->axisRect()->setRangeZoom(Qt::Vertical);
}

// 绘图图表的设置控件初始化,主要用于关联控件的信号槽
void plot::QPlot_widget_init(void)
{
    // 获取控件指针数组,方便设置时编码书写
    pChkVisibleCurve[0] = ui->chkVisibleCurve1; pBtnColourCurve[0] = ui->btnColourCurve1; pTxtValueCurve[0] = ui->txtValueCurve1; pRdoBoldCurve[0] = ui->rdoBoldCurve1;
    pChkVisibleCurve[1] = ui->chkVisibleCurve2; pBtnColourCurve[1] = ui->btnColourCurve2; pTxtValueCurve[1] = ui->txtValueCurve2; pRdoBoldCurve[1] = ui->rdoBoldCurve2;
    pChkVisibleCurve[2] = ui->chkVisibleCurve3; pBtnColourCurve[2] = ui->btnColourCurve3; pTxtValueCurve[2] = ui->txtValueCurve3; pRdoBoldCurve[2] = ui->rdoBoldCurve3;
    pChkVisibleCurve[3] = ui->chkVisibleCurve4; pBtnColourCurve[3] = ui->btnColourCurve4; pTxtValueCurve[3] = ui->txtValueCurve4; pRdoBoldCurve[3] = ui->rdoBoldCurve4;
    pChkVisibleCurve[4] = ui->chkVisibleCurve5; pBtnColourCurve[4] = ui->btnColourCurve5; pTxtValueCurve[4] = ui->txtValueCurve5; pRdoBoldCurve[4] = ui->rdoBoldCurve5;
    pChkVisibleCurve[5] = ui->chkVisibleCurve6; pBtnColourCurve[5] = ui->btnColourCurve6; pTxtValueCurve[5] = ui->txtValueCurve6; pRdoBoldCurve[5] = ui->rdoBoldCurve6;
    pChkVisibleCurve[6] = ui->chkVisibleCurve7; pBtnColourCurve[6] = ui->btnColourCurve7; pTxtValueCurve[6] = ui->txtValueCurve7; pRdoBoldCurve[6] = ui->rdoBoldCurve7;
    pChkVisibleCurve[7] = ui->chkVisibleCurve8; pBtnColourCurve[7] = ui->btnColourCurve8; pTxtValueCurve[7] = ui->txtValueCurve8; pRdoBoldCurve[7] = ui->rdoBoldCurve8;

    pCmbScatterStyle[0] = ui->cmbScatterStyle1;
    pCmbScatterStyle[1] = ui->cmbScatterStyle2;
    pCmbScatterStyle[2] = ui->cmbScatterStyle3;
    pCmbScatterStyle[3] = ui->cmbScatterStyle4;
    pCmbScatterStyle[4] = ui->cmbScatterStyle5;
    pCmbScatterStyle[5] = ui->cmbScatterStyle6;
    pCmbScatterStyle[6] = ui->cmbScatterStyle7;
    pCmbScatterStyle[7] = ui->cmbScatterStyle8;


    // 设置颜色选择框的初始背景颜色,与曲线同步颜色
    for(int i=0; i<8; i++){
        pBtnColourCurve[i]->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(QColor(pCurve[i]->pen().color()).name()));
    }

    // 可见性选择框关联
    for(int i=0; i<8; i++){
        connect(pChkVisibleCurve[i], &QCheckBox::clicked, [=](){
            curveSetVisible(pPlot1, pCurve[i], pChkVisibleCurve[i]->checkState());
        });
    }

    // 颜色选择框关联
    for(int i=0; i<8; i++){
        connect(pBtnColourCurve[i], &QPushButton::clicked, [=](){
            curveSetColor(pPlot1, pCurve[i], pBtnColourCurve[i]);
        });
    }

    // 加粗显示多选框关联。尽量别用,会导致CPU使用率升高
    for(int i=0; i<8; i++){
        connect(pRdoBoldCurve[i], &QRadioButton::clicked, [=](){
            curveSetBold(pPlot1, pCurve[i], pRdoBoldCurve[i]->isChecked());
        });
    }

    // 散点样式选择关联
    for(int i=0; i<8; i++){
        connect(pCmbScatterStyle[i], &QComboBox::currentTextChanged, [=](){
            curveSetScatterStyle(pPlot1, pCurve[i], pCmbScatterStyle[i]->currentIndex()+1);
        });
    }

    //QIcon ssCircleIcon (":/pic/ssCircle.png");
    //ui->cmbScatterStyle1->addItem(ssCircleIcon,"空心圆");
    for(int i=0; i<8; i++){
        pCmbScatterStyle[i]->setIconSize(QSize(25,17)); // 设置图片显示像素大小,不然会默认大小显示会模糊
    }

}

/* 功能:隐藏/显示曲线n
 * QCustomPlot *pPlot:父控件,绘图图表
 * QCPGraph *pCurve:图表的曲线
 * int arg1:曲线的可见性,>0可见,0不可见
 * */
void plot::curveSetVisible(QCustomPlot *pPlot, QCPGraph *pCurve, int arg1)
{
    if(arg1){
        pCurve->setVisible(true);
    }else{
        pCurve->setVisible(false);
    }
    pPlot->replot(QCustomPlot::rpQueuedReplot);
}

/* 功能:弹出颜色对话框,设置曲线n的颜色
 * QCustomPlot *pPlot:父控件,绘图图表
 * QCPGraph *pCurve:图表的曲线
 * QPushButton *btn:曲线颜色选择框的按键,与曲线的颜色同步
 * */
void plot::curveSetColor(QCustomPlot *pPlot, QCPGraph *pCurve, QPushButton *btn)
{
    // 获取当前颜色
    QColor bgColor(0,0,0);
    //bgColor = btn->palette().color(QPalette::Background);// 由pushButton的背景色获得颜色
    bgColor = pCurve->pen().color();// 由curve曲线获得颜色
    // 以当前颜色打开调色板,父对象,标题,颜色对话框设置项(显示Alpha透明度通道)
    //QColor color = QColorDialog::getColor(bgColor);
    QColor color = QColorDialog::getColor(bgColor, this,
                                     tr("颜色对话框"),
                                     QColorDialog::ShowAlphaChannel);
    // 判断返回的颜色是否合法。若点击x关闭颜色对话框,会返回QColor(Invalid)无效值,直接使用会导致变为黑色。
    if(color.isValid()){
        // 设置选择框颜色
        btn->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(color.name()));
        // 设置曲线颜色
        QPen pen = pCurve->pen();
        pen.setBrush(color);
        pCurve->setPen(pen);
    }
    // 更新绘图
    pPlot->replot(QCustomPlot::rpQueuedReplot);
}

/* 功能:加粗显示曲线n
 * QCustomPlot *pPlot:父控件,绘图图表
 * QCPGraph *pCurve:图表的曲线
 * int arg1:曲线的粗细,>0粗,0细
 * */
void plot::curveSetBold(QCustomPlot *pPlot, QCPGraph *pCurve, int arg1)
{
    // 预先读取曲线的颜色
    QPen pen = pCurve->pen();
    //pen.setBrush(pCurve->pen().color());// 由curve曲线获得颜色

    if(arg1){
        pen.setWidth(3);
        pCurve->setPen(pen);
    }else{
        pen.setWidth(1);
        pCurve->setPen(pen);
    }
    pPlot->replot(QCustomPlot::rpQueuedReplot);
}

/* 功能:选择曲线样式(线,点,积)
 * QCustomPlot *pPlot:父控件,绘图图表
 * QCPGraph *pCurve:图表的曲线
 * int arg1:曲线样式(线,点,积)
 * */
void plot::curveSetLineStyle(QCustomPlot *pPlot, QCPGraph *pCurve, int arg1)
{
    // 设置曲线样式
    //customPlot->graph(19)->setLineStyle(QCPGraph::lsLine); // 数据点通过直线连接
    //customPlot->graph(19)->setLineStyle((QCPGraph::LineStyle)i);//设置线性
    //pCurve->setLineStyle(QCPGraph::LineStyle(arg1));
    pCurve->setLineStyle((QCPGraph::LineStyle)arg1);
    pPlot->replot(QCustomPlot::rpQueuedReplot);
}

/* 功能:选择散点样式(空心圆、实心圆、正三角、倒三角)
 * QCustomPlot *pPlot:父控件,绘图图表
 * QCPGraph *pCurve:图表的曲线
 * int arg1:散点样式(空心圆、实心圆、正三角、倒三角)
 * */
void plot::curveSetScatterStyle(QCustomPlot *pPlot, QCPGraph *pCurve, int arg1)
{
    // 设置散点样式
    //customPlot->graph(19)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 5)); // 空心圆
    //pCurve->setScatterStyle(QCPScatterStyle::ScatterShape(arg1)); // 散点样式
    //pCurve->setScatterStyle((QCPScatterStyle::ScatterShape)arg1); // 散点样式
    if(arg1 <= 10){
        pCurve->setScatterStyle(QCPScatterStyle((QCPScatterStyle::ScatterShape)arg1, 5)); // 散点样式
    }else{ // 后面的散点图形略复杂,太小会看不清
        pCurve->setScatterStyle(QCPScatterStyle((QCPScatterStyle::ScatterShape)arg1, 8)); // 散点样式
    }
    pPlot->replot(QCustomPlot::rpQueuedReplot);
}

// 图例显示与否
void plot::on_chkShowLegend_stateChanged(int arg1)
{
    if(arg1){
        // 显示图表的图例
        pPlot1->legend->setVisible(true);
    }else{
        // 不显示图表的图例
        pPlot1->legend->setVisible(false);
    }
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

// 绘图演示-曲线
void plot::on_chkDrawDemo_stateChanged(int arg1)
{
    if(arg1){
        timer->start(10);
    }else{
        timer->stop();
    }
}

// 设置曲线x轴自动跟随
void plot::setAutoTrackX(QCustomPlot *pPlot)
{
    pointCountX = ui->txtPointCountX->text().toUInt();
    if(pCurve[0]->dataCount() < pointCountX){
        pPlot->xAxis->setRange(0,pointCountX);
    }else{
        pPlot->xAxis->setRange((pCurve[0]->dataCount()>pointCountX)?(pCurve[0]->dataCount()-pointCountX):0, pCurve[0]->dataCount());
    }
}

// 设置曲线x轴手动设置范围(依照右下角输入框)
void plot::setManualSettingX(QCustomPlot *pPlot)
{
    pointOriginX = ui->txtPointOriginX->text().toInt();
    pointCountX = ui->txtPointCountX->text().toUInt();
    pPlot->xAxis->setRange(pointOriginX, pointOriginX+pointCountX);
}

// 设置Y轴自适应
void plot::setAutoTrackY(QCustomPlot *pPlot)
{
    pPlot->graph(0)->rescaleValueAxis();// y轴自适应,可放大可缩小
    for(int i=0; i<8; i++){
        pPlot->graph(i)->rescaleValueAxis(true);// y轴自适应,只能放大
    }
}

// 重新设置X轴显示的点数
void plot::on_txtPointCountX_returnPressed()
{
    if(ui->chkTrackX->checkState()){
        setAutoTrackX(pPlot1);
    }else{
        setManualSettingX(pPlot1);
    }
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

void plot::on_txtPointCountY_returnPressed()
{
    pointCountY = ui->txtPointCountY->text().toUInt();
    pPlot1->yAxis->setRange(pointCountY/2*-1,pointCountY/2);
    ui->txtPointOriginY->setText(QString::number(pointCountY/2*-1));
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

void plot::on_btnColourBack_clicked()
{
    // 获取当前颜色
    QColor bgColor(0,0,0);
    bgColor = ui->btnColourBack->palette().color(QPalette::Background);// 由pushButton的背景色获得颜色
    // 以当前颜色打开调色板,父对象,标题,颜色对话框设置项(显示Alpha透明度通道)
    //QColor color = QColorDialog::getColor(bgColor);
    QColor color = QColorDialog::getColor(bgColor, this,
                                     tr("颜色对话框"),
                                     QColorDialog::ShowAlphaChannel);

    // 判断返回的颜色是否合法。若点击x关闭颜色对话框,会返回QColor(Invalid)无效值,直接使用会导致变为黑色。
    if(color.isValid()){
        // 设置背景颜色
        pPlot1->setBackground(color);
        // 设置背景选择框颜色
        ui->btnColourBack->setStyleSheet(QString("border:0px solid;background-color: %1;").arg(color.name()));
    }
    // 更新绘图
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

void plot::on_txtPointOriginX_returnPressed()
{
    setManualSettingX(pPlot1);
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

void plot::on_chkTrackX_stateChanged(int arg1)
{
    if(arg1){
        ui->txtPointOriginX->setEnabled(false);
        setAutoTrackX(pPlot1);
        pPlot1->replot(QCustomPlot::rpQueuedReplot);
    }else{
        ui->txtPointOriginX->setEnabled(true);
    }
}

void plot::on_chkAdjustY_stateChanged(int arg1)
{
    if(arg1){
        ui->txtPointOriginY->setEnabled(false);
        ui->txtPointCountY->setEnabled(false);
        setAutoTrackY(pPlot1);
        pPlot1->replot(QCustomPlot::rpQueuedReplot);
    }else{
        ui->txtPointOriginY->setEnabled(true);
        ui->txtPointCountY->setEnabled(true);
    }
}

void plot::on_txtPointOriginY_returnPressed()
{
    pointOriginY = ui->txtPointOriginY->text().toInt();
    pointCountY = ui->txtPointCountY->text().toUInt();
    pPlot1->yAxis->setRange(pointOriginY, pointOriginY+pointCountY);
    qDebug() << pointOriginY << pointCountY;
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

// 每次图表重绘后,都会更新当前显示的原点坐标与范围。与上次不同时才会更新显示,解决有曲线数据时无法输入y的参数的问题
void plot::repPlotCoordinate()
{
    static int xOrigin, yOrigin, yCount;
    static int xOriginLast, yOriginLast, yCountLast;

    xOrigin = pPlot1->xAxis->range().lower;
    yOrigin = pPlot1->yAxis->range().lower;
    yCount = pPlot1->yAxis->range().size();
    // 与上次不同时才会更新显示,解决有曲线数据时无法输入y的参数的问题
    if(xOriginLast != xOrigin){
        ui->txtPointOriginX->setText(QString::number(xOrigin));
    }
    if(yOriginLast != yOrigin){
        ui->txtPointOriginY->setText(QString::number(yOrigin));
    }
    if(yCountLast != yCount){
        ui->txtPointCountY->setText(QString::number(yCount));
    }
    // 记录历史值
    xOriginLast = xOrigin;
    yOriginLast = yOrigin;
    yCountLast = yCount;
}

// 清空绘图
void plot::on_btnClearGraphs_clicked()
{
    //pPlot1->clearGraphs(); // 清除图表的所有数据和设置,需要重新设置才能重新绘图
    //pPlot1->clearPlottables(); // 清除图表中所有曲线,需要重新添加曲线才能绘图
    for(int i=0; i<8; i++){
        pPlot1->graph(i)->data().data()->clear(); // 仅仅清除曲线的数据
    }
    cnt = 0;
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

// 设置X轴主刻度个数
void plot::on_txtMainScaleNumX_returnPressed()
{
    pPlot1->xAxis->ticker()->setTickCount(ui->txtMainScaleNumX->text().toUInt());
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

// 设置Y轴主刻度个数
void plot::on_txtMainScaleNumY_returnPressed()
{
    pPlot1->yAxis->ticker()->setTickCount(ui->txtMainScaleNumY->text().toUInt());
    pPlot1->replot(QCustomPlot::rpQueuedReplot);
}

5.4 APP 功能使用

1、Serial Port 数据接收:

2、Serial Port 数据发送(含定时功能):

3、Plot 波形绘制:

六、代码开源

代码地址: 基于QtCreator的SerialPort串口调试助手项目代码资源-CSDN下载

如果积分不够的朋友,点波关注,评论区留下邮箱,作者无偿提供源码和后续问题解答。求求啦关注一波吧 !!!

相关推荐
小冯记录编程2 小时前
C++指针陷阱:高效背后的致命危险
开发语言·c++·visual studio
C_Liu_3 小时前
C++:类和对象(下)
开发语言·c++
coderxiaohan3 小时前
【C++】类和对象1
java·开发语言·c++
阿昭L3 小时前
MFC仿真
c++·mfc
老赵的博客5 小时前
c++ unqiue指针
java·jvm·c++
程序猿编码6 小时前
基于 Linux 内核模块的字符设备 FIFO 驱动设计与实现解析(C/C++代码实现)
linux·c语言·c++·内核模块·fifo·字符设备
怎么没有名字注册了啊6 小时前
MFC_Install_Create
c++·mfc
Wadli7 小时前
C++语法 | static静态|单例模式
开发语言·c++·单例模式
进击的_鹏8 小时前
【C++11】initializer_list列表初始化、右值引用和移动语义、可变参数模版等
开发语言·c++