前言: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