QT实时绘图(利用QtChart,不添加第三方库)

前言:Qt实时绘图的需求,一般都是接收某些传感器的数据并实时显示到界面上。相关的方法有很多,如利用库Qwt, QCustomerPlot。由于本人对于Qt不是很熟悉,想着添加第三方库也需学习新的库函数,所以就一直在寻找直接利用QtChart实现实时绘图的方法。由于网上没有找到相关资料,在此记录本人的实现方式。

 

1. 官方的实时绘图实例

  在QT官方例程中,有一个实时绘图实例。打开QT Creator(本人的QT Creator版本为4.10.2,QT版本为5.12),点击左边示例->在搜索框搜索audio->找到实时绘图例程Audio  Example,如下图所示(也可以在安装路径的qt example中找到):

 

 

 打开该工程,总共有2个cpp文件:widget.cpp和xyseriesiodevice.cpp。其中widget.cpp定义了一个窗口类,该窗口主要用来设置基本的参数,如QChart,QChartView,QValueAxis,QLineseries(这些参数是利用QTChart绘图的基本配置)。然后xyseriesiodevice.cpp定义了一个IODevice类 。该类的主要作用就是重写了writeData()函数。并在writeData()函数中更新QLineseries的数据。主要的逻辑就是在Widget的构造函数中调用QAudioInput::start()函数,这样一旦audio设备有数据,都会发送到IODecive中,通过调用IODevice的writeData()函数,实现QLineseries数据的更新,从而达到实时数据显示。

 

2. 实现实时绘图

  从官方的例程中可以看到,为了实现实时绘图,必须不时更行跟界面绑定的QLineSeries的数据。由于例程利用了QAudioInput的数据接收产生的信号来更新series数据。为了实现自定义的数据刷新,可以利用QTimer定时中断来更新series数据。因此,改造xyseriesiodevice.cpp函数,不再需要writeData()函数,而是添加定时中断函数,在该函数中更新数据。改造后的xyseriesiodevice.h和xyseriesiodevice.cpp文件如下:

//xyseriesiodevice.h
class
XYSeriesIODevice : public QObject { Q_OBJECT public: explicit XYSeriesIODevice(QXYSeries *series, QObject *parent = nullptr); static const int sampleCount = 2000; void XYSeriesIODeviceInit(); //用于开启定时器,连接信号和槽 public slots: void timeoutslot(); //定义定时中断函数,更行m_series的数据 private: QXYSeries *m_series; QVector<QPointF> m_buffer; float m_time; QTimer m_timer; //定义QTimer,实现定时器更新数据 };
//xyseriesiodevice.cpp

#include "xyseriesiodevice.h" #include <QtCharts/QXYSeries> XYSeriesIODevice::XYSeriesIODevice(QXYSeries *series, QObject *parent) : QObject(parent), m_series(series) { } void XYSeriesIODevice::XYSeriesIODeviceInit() { connect(&m_timer,&QTimer::timeout,this,&XYSeriesIODevice::timeoutslot); if (m_buffer.isEmpty()) { m_buffer.reserve(sampleCount); for (int i = 0; i < sampleCount; ++i) { int x=rand()%10; m_buffer.append(QPointF(x, 0)); } } m_timer.start(100); } //定时中断中更新数据,即m_series,在此函数中每10个数据更新一次 void XYSeriesIODevice::timeoutslot() { int start=1990; for (int s = 0; s < start; ++s) { m_buffer[s].setX(m_buffer.at(s + 10).x()); m_buffer[s].setY(m_buffer.at(s + 10).y()); } for (int s = 1990; s < sampleCount;s++) { float x=10*cos(0.1*m_time); float y=10*sin(0.1*m_time); //由于没有数据来源,在此本人设置了圆形轨迹 m_buffer[s].setX(x); m_buffer[s].setY(y); m_time++; } m_series->replace(m_buffer); }


注:为了实现传感器数据显示这种实时显示功能,在定时中断函数中,设置X坐标为时间,Y坐标为传感器数据即可,同时还可以调整数据更新的频率(每收到几个数据更新一次)。修改后就能看到实时的圆形轨迹,如下图。

 

 

 

3. 将实时绘图类整合封装成一个类

  可以看到,上面的修改比较暴力,主要存在的问题如下:

1. 将一个实时绘图设置为了两个类:widget和XYSeriesIODevice,封装性并不好。

2. 主界面直接就是显示区,实际应用中,主界面除了实时显示区,通常还有其他部分(如按钮,面板之类的)。

实际上,可以发现XYSeriesIODevice类唯一的作用就是定义了QTimer并在定时中断中实时更新series数据,这个完全可以直接放到widget类实现。主界面直接就是显示区,是因为我们的项目并没有默认窗口(一般在创建项目时,都会默认生成一个界面MainWindow),widget类本身就是一个Widge窗口,因此在创建类的同时,创建了窗口。为了解决以上问题,我们从头建一个项目,将实时绘图封装成一个可供调用的类。

主要的操作有以下几步:

1. 创建一个带默认窗口的项目,并在界面添加QGraphicsView作为显示界面(其他可以显示QChartView的显示界面均可)。

2. 编写实时绘图类RealTimePlot,该类主要需要以下成员变量:

QVector<QPointF> m_buffer:用于保存数据

QTimer m_timer:用于更新数据

QChart m_chart及QChartView m_chartview:用于显示

QSplineSeries m_series :跟chart关联的数据series(在此我换成了QSplineSeries,用于画曲线)。

3. 将显示界面QGraphicsView跟自定义的实时绘图类绑定

下面看主要的RealTimePlot类的定义(实际上就是将第2节中的两个类整合):

//realtimeplot.h

#include <QtCharts>

QT_CHARTS_USE_NAMESPACE


class RealTimePlot:public QGraphicsView
{
    Q_OBJECT
public:
    RealTimePlot(QWidget* parent=nullptr);

    static const int sampleCount=2000;

public slots:
    void timeoutslot();

private:
    QVector<QPointF> m_buffer;
    float m_time;

    QTimer m_timer;

    QChart *m_chart;
    QSplineSeries *m_series ;
};

 

#include "realtimeplot.h"

RealTimePlot::RealTimePlot(QWidget* parent):
    QGraphicsView(parent)
{
    m_chart=new QChart();
    m_series=new QSplineSeries();

    QChartView *chartView = new QChartView(m_chart);
    chartView->setMinimumSize(800, 600);
    m_chart->addSeries(m_series);
    QValueAxis *axisX = new QValueAxis;
    axisX->setRange(-10,10);
    axisX->setLabelFormat("%g");
    axisX->setTitleText("Samples");
    QValueAxis *axisY = new QValueAxis;
    axisY->setRange(-10, 10);
    axisY->setTitleText("Audio level");
    m_chart->addAxis(axisX, Qt::AlignBottom);
    m_series->attachAxis(axisX);
    m_chart->addAxis(axisY, Qt::AlignLeft);
    m_series->attachAxis(axisY);
    m_chart->legend()->hide();
    m_chart->setTitle("Data from the microphone");

    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(chartView);

    connect(&m_timer,&QTimer::timeout,this,&RealTimePlot::timeoutslot);

    if (m_buffer.isEmpty()) {
        m_buffer.reserve(sampleCount);
        for (int i = 0; i < sampleCount; ++i)
        {
            int x=rand()%10;
            m_buffer.append(QPointF(x, 0));
        }
    }
    m_timer.start(500);
}


void RealTimePlot::timeoutslot()
{
    int start=1990;

        for (int s = 0; s < start; ++s)
        {
            m_buffer[s].setX(m_buffer.at(s + 10).x());
            m_buffer[s].setY(m_buffer.at(s + 10).y());
        }

    for (int s = 1990; s < sampleCount;s++)
    {
       float x=10*cos(0.1*m_time);
       float y=10*sin(0.1*m_time);
        m_buffer[s].setX(x);
        m_buffer[s].setY(y);
        m_time++;

    }

    m_series->replace(m_buffer);
    //update();
}

 

最后是将GraphicsView和自定义的类绑定,这个就是“提升”(“提升”在本人的理解,就是将一个界面控件(相当于一个类对象)变成自定义带特殊功能的控件(新的自定义类对象))。由于一个控件只能提升为同类型的类,所以我们编写的RealTimePlot类继承了QGraphicsView类。这样就能将一个GraphicsView控件提升为自定义RealTimePlot类。提升之后,一个GraphicsView控件(假设在MainWindow中叫graphicsView)就相当于函数语句:

ui->graphicsView=new RealTimePlot();

 

这样,最终的效果如下图(比较懒,没贴动图,实际会是实时显示的效果):

 

 源码见:https://github.com/yuanfuaccount/BalanceTrainer/tree/master/PersonalLib/RealTimePlot

 

posted @ 2020-10-23 17:09  晨枫1  阅读(4285)  评论(0编辑  收藏  举报