Qt5-实现串口助手版“示波器”

一.前言

  • 闲了好久没更新博客,偷懒的我就拿了去年暑假备战电赛时做的一个串口版“示波器”来充充水。抛砖引玉哈。
  • 因为以前发过一篇主题一样的文章,所以就当该文就相当于上版的升级版(其实两者没啥关系,仅是主题一样,以前那篇是单纯为赛题弄的,这篇更有通用性)
  • 是当时为电赛准备的(吐槽一下,原本我组准备的电源题,怎么想着也会用上adc、pwm啥的吧,结果19年的A题一出来真是让我一言难尽啊)
  • 实现的功能有:自定义数据格式,多个数据图线显示,自动和手动两种模式,显示鼠标当前的位置
  • 因为时间有点久,很多注释没写到还请谅解。

二.效果介绍

  • 因为一直宅在家,又没带板子回来,所以就只能看看界面了,红色字体为说明
  • Qt5-实现串口助手版“示波器”_第1张图片

三.软件思路介绍

  1. 大概思路,串口接收数据,然后按照自定义格式解析数据,最后绘画波形图,因为总代码有点多,就不全贴出来,有空我上传到github上去,也可以评论发邮箱号我来发给你
  2. 这里用到到绘画波形图的库是qcustomplot,不清楚的可以百度一下,它的官网介绍得挺详细的。
  3. 先是定义一个Handle类里有串口的自动检测,即如果检测有串口出现会自动加到串口端口的栏中,和串口数据的接收、数据解析和曲线图的绘画(放在一个线程里执行,防止界面显示卡死)代码如下,先是头文件:
#include 
#include 
#include 
#include "qcustomplot.h"
#include 
#define MAX_COUNT       1500			// x轴可显示的最大范围
#define cout qDebug() << __LINE__ << ":"
class Handle : public QObject
{
    Q_OBJECT
public:
    explicit Handle(QCustomPlot *P,QObject *parent = nullptr);
signals:
    void finish();						// 数据解析完成时发出信号
public slots:
    void getValidPort(QComboBox* p);    // 获取有效串口端口
    void getText(QString t);
    void analyData();					// 解析数据
    void setFlag(QString b = "[",QString m=",",QString e="]"){
        begin = b;mid = m; end = e;
    }									// 自定义数据格式,分隔符
    void clear();						// 数据清空
    void setAuto(bool e){				// 是否设置为自动模式
        isAuto = e;
    }
    void setRun(bool e){				// 是否设置为运行
        isRun = e;
    }
    void setOpen(bool e){				// 是否打开串口
        isPortOpen = e;
    }
    bool getOpen(){return isPortOpen;}	// 获取串口打开状态
    bool getRun(){return isRun;}		// 获取串口是否在运行
    bool getAuto(){return isAuto;}		// 获取是否为自动模式
private:
    QString begin,mid,end;              // 自定义数据格式
    QString text;
    bool isPortOpen;                    // 标识串口端口是否打开
    QStringList list;

    QCustomPlot *Plot;                  // 图层
    QVector <QCPItemTracer *>   trace;  // 曲线上的跟随点
    QVector <QCPItemText *  >   Ptext;  // 每条曲线上的显示的文字
    QVector <QVector<double>>   YData;  // Y轴数据,可具有多个数据曲线
    QVector <double>            XData;  // x轴数据
    QVector<QPen>               pen;    // 曲线颜色的区分
    int Clk,xMid;                       // x轴的刻度
    int countLine;                      // 曲线的数量
    int firstLine;
    bool isAuto,isRun;                  // 标识是否自动,是否在运行
};
  1. 再贴点串口数据解析函数的代码
/*
 * 串口数据的解析
 */
void Handle::analyData()
{
	// 进行数据提取,比如begin="[",mid=",",end="]",串口数据为,30][10,20,30][...
	// 
    int _end = text.indexOf(begin);
    text.remove(0,_end+1);		// 移除前面多余的数据,比如移除,30][,还剩10,20,30][...
    if( _end < 0 || !isRun){
        return ;
    }
    // 将数据分段,比如分出10,20,30]和下一段[...
    QStringList _be = text.split(begin,QString::SkipEmptyParts);
    for(auto _b:_be){
        int _e = _b.indexOf(end);	// 找出数据段里的]的位置
        if(_e > 0){
            QStringList _list = _b.left(_e).split(mid,QString::SkipEmptyParts);	// 将数据10,20,30中的各个数据分离出来
            countLine = 0;			// 统计一段数据里的数据个数
            for(auto i:list){		// 分离出的数据放入各个曲线数据里
                YData[countLine].push_back(i.toDouble());
                if(YData[countLine].count() >= MAX_COUNT){
                    YData[countLine].pop_front();
                }
                countLine ++;
            }
            XData.push_back(Clk++);
            if(XData.count() >= MAX_COUNT){
                XData.pop_front();
            }
            for(int _i = firstLine; _i < countLine && _i < 6; _i ++){
                firstLine = countLine;
                Plot->addGraph();
                Plot->graph(_i)->setPen(pen.at(_i));	// 给数据曲线上色
                Plot->graph(_i)->setVisible(true);		// 曲线显示
                trace[_i] = new QCPItemTracer(Plot);	// 生成一个跟随点
                trace[_i]->setPen(pen.at(_i));			// 设置点的颜色
                trace[_i]->setSize(10);					// 设置点的大小
                trace[_i]->setBrush(QBrush(pen.at(_i).color()));
                trace[_i]->setGraph(Plot->graph(_i));	// 设置在该曲线显示
                trace[_i]->setStyle(QCPItemTracer::tsCircle);	// 圆形点
                trace[_i]->setInterpolating(true);

                Ptext[_i] = new QCPItemText(Plot);	// 跟随点的文本信息
                Ptext[_i]->setPen(pen.at(_i));
                Ptext[_i]->setText(QString("Graph:%1").arg(_i));
                Ptext[_i]->setRotation(4);
                Ptext[_i]->setTextAlignment(Qt::AlignRight | Qt::AlignBottom|Qt::AlignJustify);
                Ptext[_i]->setPositionAlignment(Qt::AlignRight | Qt::AlignBottom|Qt::AlignJustify);
                Ptext[_i]->position->setType(QCPItemPosition::ptPlotCoords);
                Ptext[_i]->setFont(QFont("Helvetica [Cronyx]", 12));
                Ptext[_i]->setPadding(QMargins(8, 0, 0, 0));

                YData[_i].resize(MAX_COUNT);
            }
            for(int _i = 0; _i < countLine; _i ++){
                Plot->graph(_i)->setData(XData,YData[_i]);
                if(Clk <= xMid){
                    Plot->xAxis->setRange(0,Clk);
                }
                else{
                    Plot->xAxis->setRange(Clk-xMid,Clk);
                }
                Ptext[_i]->position->setCoords(Clk,YData[_i].last());	// 文本显示的位置
                Ptext[_i]->setText(QString("%1:(%2,%3)").arg(_i+1).arg(Clk).arg(YData[_i].last()));	// 文本内容
                trace[_i]->setGraphKey(Clk);			// 跟随点的位置
                if(isAuto){								// 是否自动适配缩放
                    Plot->yAxis->rescale(true);
                }
            }
        }
    }
    Plot->replot();				// 图层更新
    if(text.length() > 200){	// 判断串口接收数据是否过多,过多久丢弃
        text.clear();
    }
    emit finish();
}
  1. 然后在widget类中,这里比较简单,开了个线程给Handle类,然后绑定各种信号与槽,先上头文件:
#ifndef WIDGET_H
#define WIDGET_H

#include 
#include 
#include 
#include 
#include 
#include "qcustomplot.h"
#include 
#include 
#include 
#include 
#include 
#include "handle.h"
#include 
namespace Ui {
class Widget;
}
class Widget : public QWidget
{
    Q_OBJECT
public:
    explicit Widget(QWidget *parent = nullptr);
    ~Widget();
    void goClear();				// 清除
    void autoHandle();			// 没用上 
signals:
    void analy(QString t);		
    void clearT();
    void getValidP(QComboBox* p);
    void startA();
private slots:
    void on_pushOpen_clicked();
    void on_pushClear_clicked();
    void on_pushRun_clicked();
    void on_pushAuto_clicked();
private:
    Ui::Widget *ui;
    Handle *hand;
    QCustomPlot *Plot;
    QSerialPort *SerialPort;

    QThread *thread;
};
#endif // WIDGET_H

  1. widget类构造函数里的各种信号与槽的绑定
Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);
    SerialPort = new QSerialPort(this);
    Plot = ui->widget;
    Plot->setMouseTracking(true);	// 使能图层鼠标跟随
    // 开启图层上的图层刻度可手动调动
    Plot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
    connect(Plot->xAxis, SIGNAL(rangeChanged(QCPRange)), Plot->xAxis2, SLOT(setRange(QCPRange)));
    connect(Plot->yAxis, SIGNAL(rangeChanged(QCPRange)), Plot->yAxis2, SLOT(setRange(QCPRange)));

    connect(Plot,&QCustomPlot::mouseMove,[=](QMouseEvent *e){
        double _x = Plot->xAxis->pixelToCoord(e->x()),_y = Plot->yAxis->pixelToCoord(e->y());
        ui->labelWhere->setText(QString("(%1,%2)").arg(_x).arg(_y));

    });
    connect(Plot,&QCustomPlot::plottableClick,[=](
            QCPAbstractPlottable * plottable,
            int ,
            QMouseEvent * ){

        ui->labelGraph->setText(plottable->name());
    });

    ui->tabWidget->setTabText(0,"Gui");
    ui->tabWidget->setTabText(1,"Data");

    hand = new Handle(Plot);
    thread = new QThread(this);
    hand->moveToThread(thread);
    // 获取自定义数据
    hand->setFlag(ui->lineEditBegin->text(),ui->lineEditMid->text(),ui->lineEditEnd->text());
    // 绑定清除按键的响应
    connect(this,&Widget::clearT,hand,&Handle::clear,Qt::QueuedConnection);
    // 有串口信息时,将数据给Handle类处理
    connect(this,&Widget::analy,hand,&Handle::getText,Qt::QueuedConnection);
	// 绑定获取有效串口端口信息    
   	connect(this,&Widget::getValidP,hand,&Handle::getValidPort,Qt::QueuedConnection);
	// 这个也没用上,还请注意    
    connect(hand,&Handle::finish,this,&Widget::autoHandle,Qt::QueuedConnection);
    connect(SerialPort,&QSerialPort::readyRead,[=](){
       emit analy(SerialPort->readAll());
    });
    connect(SerialPort,&QSerialPort::errorOccurred,[=](QSerialPort::SerialPortError e){
        cout << "error";
        if(e == QSerialPort::DeviceNotFoundError){
            SerialPort->close();
            hand->setOpen(false);
            on_pushClear_clicked();
        }
    });
    thread->start();		// 开启线程
    goClear();
}

四.完结

注:这个程序有个小问题,就是当窗口全屏时,就会发生数据丢失加剧的现象。当时的我本想试着再加一个线程来专门处理曲线绘画的,后来发现数据丢失的现象更严重,就没加那个线程了。数据丢失是由下位机发信息太过于频繁,比如频率大于1000HZ时,频率降低可解决。

你可能感兴趣的:(Qt)