• QT串口调试助手V2.0(源码全开源)--上位机+多通道波形显示+数据保存(优化波形显示控件)


    首先关于Qt的安装和基本配置这里就不做重复说明了,注:本文在Qt5.14基础上完成 完整的项目开源仓库链接在文章末尾

    图形控件——qcustomplot

    QCustomPlot是一个基于Qt框架的开源绘图库,用于创建高质量的二维图表和数据可视化。

    QCustomPlot的主要功能:
    绘制多种图表类型:包括折线图、散点图、柱状图、面积图
    交互性:支持图表的缩放、平移、数据点选择等交互操作
    多轴支持:可以在图表中添加多个X轴和Y轴,以便绘制复杂的多轴图表
    定制化:提供丰富的样式和属性设置,用户可以自定义图表的外观,包括颜色、线条样式、标记
    高性能:针对大数据量绘图进行了优化,能够处理大量数据点而不影响性能

    主要组件:
    QCustomPlot:主绘图控件,所有的绘图操作都在这个控件上进行。
    QCPGraph:用于绘制常见的折线图和散点图。
    QCPAxis:表示图表的轴,可以自定义轴的范围、标签、刻度等。
    QCPItem:图表中的各种辅助元素,如直线、文本标签等。
    QCPPlottable:可绘制对象的基类,所有具体的绘图类型都继承自这个类。

    该控件使用方法
    将qcustomplot的源码一个cpp一个h文件添加到项目工程中,并添加头文件和编译链接就可以便捷的在源码中使用了。注意在放置显示控件时需要先放置一个qt内置的QWidget控件,然后通过右键控件,升格,选中头文件和类名称,设置为QCustomPlot,然后源代码中就可以快乐使用了。具体设置的效果可以参考文末完整的项目链接。
    在这里插入图片描述

    绘制曲线数据

    主要使用到了曲线控件customPlot->addGraph();

    这个控件的使用方法整体上和Qt自带的控件差别不大,主要这个控件的视觉效果和长时间大数据绘制效果更好一些

    在使用控件之前要先初始化相关的控件内容:

    // 绘图图表初始化
    void MainWindow::QPlot_init(QCustomPlot *customPlot)
    {
        // 创建定时器,用于定时生成曲线坐标点数据
        QTimer *timer = new QTimer(this);
        timer->start(10);
        connect(timer, SIGNAL(timeout()), this, SLOT(Plot_TimeData_Update()));
    
        // 图表添加两条曲线
        pGraph1_1 = customPlot->addGraph();
        pGraph1_2 = customPlot->addGraph();
    
        // 设置曲线颜色
        pGraph1_1->setPen(QPen(Qt::red));
        pGraph1_2->setPen(QPen(Qt::black));
    
        // 设置坐标轴名称
        customPlot->xAxis->setLabel("X-Times");
        customPlot->yAxis->setLabel("Amplitude of channel");
    
        // 设置y坐标轴显示范围
        customPlot->yAxis->setRange(-2, 2);
    
        // 显示图表的图例
        customPlot->legend->setVisible(true);
        // 添加曲线名称
        pGraph1_1->setName("Channel1");
        pGraph1_2->setName("Channel2");
    
        // 设置波形曲线的复选框字体颜色
        ui->checkBox_1->setStyleSheet("QCheckBox{color:rgb(255,0,0)}"); // 设定前景颜色,就是字体颜色
        // 允许用户用鼠标拖动轴范围,用鼠标滚轮缩放,点击选择图形:
        customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
    }
    

    当向曲线添加新数据的时候可以同时控制范围窗口内的坐标轴尺寸,以及打印一些绘图相关的属性数据

    void MainWindow::Plot_Show_Update(QCustomPlot *customPlot, double n1, double n2)
    {
        cnt++;
        // 给曲线添加数据
        pGraph1_1->addData(cnt, n1);
        pGraph1_2->addData(cnt, n2);
    
        // 设置x坐标轴显示范围,使其自适应缩放x轴
        customPlot->xAxis->setRange( 0, (pGraph1_1->dataCount() > 1000) ? (pGraph1_1->dataCount()) : 1000);
        // 更新绘图,这种方式在高填充下太浪费资源。rpQueuedReplot,可避免重复绘图。
        customPlot->replot(QCustomPlot::rpQueuedReplot);
    
        static QTime time(QTime::currentTime());
        double key = time.elapsed() / 1000.0; // 开始到现在的时间,单位秒
        //计算帧数
        static double lastFpsKey;
        static int frameCount;
        frameCount++;
        if (key - lastFpsKey > 1) // 每1秒求一次平均值
        {
            // 帧数和数据总数
            ui->statusbar->showMessage(
                QString("Refresh rate: %1 FPS, Total data volume: %2")
                    .arg(frameCount / (key - lastFpsKey), 0, 'f', 0)
                    .arg(customPlot->graph(0)->data()->size() + customPlot->graph(1)->data()->size()),
                0);
            lastFpsKey = key;
            frameCount = 0;
        }
    }
    

    曲线的绘制效果:
    在这里插入图片描述
    要实现上面的多通道效果还需要自行适配串口解析部分,然后匹配对应的数据后将数值传入到目标曲线对象。

    完整的项目源码如下,包含数据解析、串口控制部分、同时实现数据保存到csv文件(代码中只保存了数组前两位的数值,需要保存多少数据可以自行修改代码实现)

    项目Cpp文件:

    #include "mainwindow.h"
    #include "ui_mainwindow.h"
    
    MainWindow::MainWindow(QWidget *parent)
        : QMainWindow(parent), ui(new Ui::MainWindow)
    {
        ui->setupUi(this);
    
        // 给widget绘图控件,设置个别名,方便书写
        pPlot1 = ui->widget_1;
    
        // 状态栏指针
        sBar = statusBar();
    
        // 初始化图表1
        QPlot_init(pPlot1);
        cnt = 0;
    
        setWindowTitle("数据采集系统");
        serialport = new QSerialPort;
        find_port();                    //查找可用串口
        timerserial = new QTimer();
        QObject::connect(serialport,&QSerialPort::readyRead, this, &MainWindow::serial_timerstart);
        QObject::connect(timerserial,SIGNAL(timeout()), this, SLOT(Read_Date()));
    
    
        ui->close_port->setEnabled(false);//设置控件不可用
    }
    
    
    // 析构函数
    MainWindow::~MainWindow()
    {
        delete ui;
    }
    
    //查找串口
    void MainWindow::find_port()
    {
        //查找可用的串口
        bool fondcom = false;
        ui->com->clear();
        foreach(const QSerialPortInfo &info, QSerialPortInfo::availablePorts())
        {
            QSerialPort serial;
            serial.setPort(info);   //设置串口
            if(serial.open(QIODevice::ReadWrite))
            {
                //
                ui->com->addItem(serial.portName());        //显示串口name
                fondcom = true;
                QString std = serial.portName();
                QByteArray comname = std.toLatin1();
                //QMessageBox::information(this,tr("SerialFond"),tr((const char *)comname.data()),QMessageBox::Ok);
                serial.close();
                ui->open_port->setEnabled(true);
            }
        }
        if(fondcom==false)
        {
            QMessageBox::information(this,tr("Error"),tr("Serial Not Fond!Plase cheak Hardware port!"),QMessageBox::Ok);
        }
    }
    
    /* 打开并设置串口参数 */
    void MainWindow::on_open_port_clicked()
    {
        update();
        //find_port();     //重新查找com
         //初始化串口
             serialport->setPortName(ui->com->currentText());        //设置串口名
             if(serialport->open(QIODevice::ReadWrite))              //打开串口成功
             {
                 serialport->setBaudRate(ui->baud->currentText().toInt());       //设置波特率
                 switch(ui->bit->currentIndex())                   //设置数据位数
                 {
                     case 8:serialport->setDataBits(QSerialPort::Data8);break;
                     default: break;
                 }
                 switch(ui->jiaoyan->currentIndex())                   //设置奇偶校验
                 {
                     case 0: serialport->setParity(QSerialPort::NoParity);break;
                     default: break;
                 }
                 switch(ui->stopbit->currentIndex())                     //设置停止位
                 {
                     case 1: serialport->setStopBits(QSerialPort::OneStop);break;
                     case 2: serialport->setStopBits(QSerialPort::TwoStop);break;
                     default: break;
                 }
                 serialport->setFlowControl(QSerialPort::NoFlowControl);     //设置流控制
                 // 设置控件可否使用
                ui->close_port->setEnabled(true);
                ui->open_port->setEnabled(false);
                ui->refresh_port->setEnabled(false);
             }
             else    //打开失败提示
             {
                // Sleep(100);
    
                 QMessageBox::information(this,tr("Erro"),tr("Open the failure"),QMessageBox::Ok);
             }
    }
    
    
    /* 关闭串口并禁用关联功能 */
    void MainWindow::on_close_port_clicked()
    {
        serialport->clear();        //清空缓存区
        serialport->close();        //关闭串口
    
        ui->open_port->setEnabled(true);
        ui->close_port->setEnabled(false);
        ui->refresh_port->setEnabled(true);
    }
    
    /* 开始接收数据
     * */
    void MainWindow::on_recive_data_clicked()
    {
        QString str = "START_SEND_DATA\r\n";
        QByteArray str_utf8 = str.toUtf8();
        if(serialport->isOpen())serialport->write(str_utf8);
        else QMessageBox::information(this,tr("ERROE"),tr("串口未连接,请先检查串口连接"),QMessageBox::Ok);
    }
    
    void MainWindow::serial_timerstart()
    {
        timerserial->start(1);
        serial_bufferClash.append(serialport->readAll());
    }
    
    //串口接收数据帧格式为:帧头'*' 帧尾'#' 数字间间隔符号',' 符号全为英文格式
    void MainWindow::Read_Date()
    {
        QString string;
        QStringList serialBuferList;
        int list_length = 0;//帧长
        QString str = ui->Receive_text_window->toPlainText();
        timerserial->stop();//停止定时器
    //    qDebug()<< "[Serial LOG]serial read data:" <1)
            {
                clash.data1 = serialBuferList[0].toDouble();
                clash.data2 = serialBuferList[1].toDouble();
                plot_buffer.push_back(clash);
                clash.data1 = serialBuferList[2].toDouble();
                clash.data2 = serialBuferList[3].toDouble();
                plot_buffer.push_back(clash);
                clash.data1 = serialBuferList[4].toDouble();
                clash.data2 = serialBuferList[5].toDouble();
                plot_buffer.push_back(clash);
            }
        }
        else
        {
            qDebug()<< "[Serial LOG][ERROR]recive data:" <Receive_text_window->clear();
        ui->Receive_text_window->append(str);
        serial_bufferClash.clear();
    }
    
    /*  刷新串口按键的按钮槽函数
     * */
    void MainWindow::on_refresh_port_clicked()
    {
        find_port();
    }
    
    
    
    // 绘图图表初始化
    void MainWindow::QPlot_init(QCustomPlot *customPlot)
    {
    
        // 创建定时器,用于定时生成曲线坐标点数据
        QTimer *timer = new QTimer(this);
        timer->start(10);
        connect(timer, SIGNAL(timeout()), this, SLOT(Plot_TimeData_Update()));
    
        // 图表添加两条曲线
        pGraph1_1 = customPlot->addGraph();
        pGraph1_2 = customPlot->addGraph();
    
        // 设置曲线颜色
        pGraph1_1->setPen(QPen(Qt::red));
        pGraph1_2->setPen(QPen(Qt::black));
    
        // 设置坐标轴名称
        customPlot->xAxis->setLabel("X-Times");
        customPlot->yAxis->setLabel("Channel Data");
    
        // 设置y坐标轴显示范围
        customPlot->yAxis->setRange(-2, 2);
    
        // 显示图表的图例
        customPlot->legend->setVisible(true);
        // 添加曲线名称
        pGraph1_1->setName("Channel1");
        pGraph1_2->setName("Channel2");
    
        // 设置波形曲线的复选框字体颜色
        ui->checkBox_1->setStyleSheet("QCheckBox{color:rgb(255,0,0)}"); // 设定前景颜色,就是字体颜色
        // 允许用户用鼠标拖动轴范围,用鼠标滚轮缩放,点击选择图形:
        customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
    }
    
    int data_lens = 0;
    // 定时器溢出处理槽函数。用来生成曲线的坐标数据。
    void MainWindow::Plot_TimeData_Update()
    {
    
        int lens = plot_buffer.size();
        if (lens > data_lens)
        {
            for(int i=data_lens;iaddData(cnt, n1);
        pGraph1_2->addData(cnt, n2);
    
        // 设置x坐标轴显示范围,使其自适应缩放x轴
        customPlot->xAxis->setRange( 0, (pGraph1_1->dataCount() > 100) ? (pGraph1_1->dataCount()) : 100);
        // 更新绘图,这种方式在高填充下太浪费资源。rpQueuedReplot,可避免重复绘图。
        customPlot->replot(QCustomPlot::rpQueuedReplot);
    
        static QTime time(QTime::currentTime());
        double key = time.elapsed() / 1000.0; // 开始到现在的时间,单位秒
        //计算帧数
        static double lastFpsKey;
        static int frameCount;
        frameCount++;
        if (key - lastFpsKey > 1) // 每1秒求一次平均值
        {
            // 帧数和数据总数
            ui->statusbar->showMessage(
                QString("Refresh rate: %1 FPS, Total data volume: %2")
                    .arg(frameCount / (key - lastFpsKey), 0, 'f', 0)
                    .arg(customPlot->graph(0)->data()->size() + customPlot->graph(1)->data()->size()),
                0);
            lastFpsKey = key;
            frameCount = 0;
        }
    }
    
    /* 清空缓存数据
     * */
    void MainWindow::on_clean_data_clicked()
    {
        qDebug()<< "[Clean Data]";
        plot_buffer.clear();
        data_lens = 0;
        cnt = 0;
        pGraph1_1->data().data()->clear();
        pGraph1_2->data().data()->clear();
        pPlot1->graph(0)->data().clear();
        pPlot1->graph(1)->data().clear();
    }
    
    // setVisible设置可见性属性,隐藏曲线,不会对图例有任何影响。推荐使用。
    void MainWindow::on_checkBox_1_stateChanged(int arg1)
    {
        if (arg1)
        {
            pGraph1_1->setVisible(true);
        }
        else
        {
            pGraph1_1->setVisible(false); // void QCPLayerable::setVisible(bool on)
        }
        pPlot1->replot();
    }
    
    void MainWindow::on_checkBox_2_stateChanged(int arg1)
    {
        if (arg1)
        {
            pGraph1_2->setVisible(true);
        }
        else
        {
            pGraph1_2->setVisible(false); // void QCPLayerable::setVisible(bool on)
        }
        pPlot1->replot();
    }
    
    // 保存缓冲区数据为csv文件
    void MainWindow::on_savedata_csv_clicked()
    {
         if(plot_buffer.size()<1)
         {
             QMessageBox::information(this, "提示","当前数据为空");
             return;
         }
         serialport->clear();        //清空缓存区
         timerserial->stop();
         serialport->close();        //关闭串口
         ui->open_port->setEnabled(true);
         ui->close_port->setEnabled(false);
         QString csvFile = QFileDialog::getExistingDirectory(this);
         QDateTime current_date_time =QDateTime::currentDateTime();
         QString current_date =current_date_time.toString("yyyy_MM_dd_hh_mm");
         csvFile += tr("/sensor_Save_%1.csv").arg(current_date);
         if(csvFile.isEmpty())
         {
            QMessageBox::information(this,tr("警告"),tr("文件路径错误,无法打开文件,请重试"),QMessageBox::Ok);
         }
         else
         {
             qDebug()<< csvFile;
             QFile file(csvFile);
             if ( file.exists())
             {
                     //如果文件存在执行的操作,此处为空,因为文件不可能存在
             }
             file.open( QIODevice::ReadWrite | QIODevice::Text );
             QTextStream out(&file);
             out<open(QIODevice::ReadWrite);        //打开串口
         ui->open_port->setEnabled(false);
         ui->close_port->setEnabled(true);
    }
    

    完整项目链接->完整的项目工程Github链接

  • 相关阅读:
    MySQL之索引
    学习二十大奋进新征程线上知识答题小程序登录技术点分析与实现
    【PTQ】Cross-Layer Equalization跨层均衡-证明和实践详细解读
    [MAUI 项目实战] 笔记App(二):数据库设计
    Google Earth Engine(GEE)APP——一个监测影像各波段的DN值的app
    Docker 数据存储及持久化应用
    (一)JVM实战——jvm的组成部分详解
    (2022杭电多校三)1002-Boss Rush(状压DP+二分)
    【C语言】刷题训练营 —— 每日一练(十三)
    组播收数据问题,特定IP发来的数据包收不到,其余都可收到
  • 原文地址:https://blog.csdn.net/weixin_47407066/article/details/139562552