QCustomPlot 使用整理
QCustomPlot 是一个比较小的 QT 图表插件。使用时,我们在程序中写完相关调用的代码后,只需将 QCunstomPlot.cpp 和 QCustomPlot.h 两个文件加入工程,正常编译即可。看起来使用挺方便。对于简单的,效率不高的数据可视化需求,基本能满足。这里把使用该插件的一些经验做简单记录。
1. 插件的声明
QCustomPlot 的官方文档里,只介绍了在 QT Designer 中 prompt 插件的方法,如果是使用可视化窗口设计界面,这样就足够了。但是,如果用纯代码来设计界面,就麻烦了,针对初学者的文档里,并没有提到怎么 new 出一个 QCustomPlot 实体。
根据官方文档在图形界面中prompt 插件后,编译,查看designer生成的 ui_xxxx.h 文件,在里面发现了声明方法。下面是我自己在项目里的代码,已测试可行。
QWidget *paintArea = new QWidget; QCustomPlot *myPlot = new QCustomPlot(paintArea); myPlot->setFixedSize(480,300); //blue line myPlot->addGraph(); myPlot->graph(0)->setPen(QPen(Qt::blue)); //xAxis myPlot->axisRect()->setupFullAxesBox(); myPlot->xAxis->setRange(1, 1, Qt::AlignRight); myPlot->yAxis->setRange(30, 30, Qt::AlignRight); myPlot->xAxis->setLabel("I(A)"); myPlot->yAxis->setLabel("U(V)");
2. 关于 buffer
QCustomPlot 提供的几个 example 中,几乎都是用这两种给图表喂数据的方法:setData() 和 addData()。
在数据比较少,或者是设备性能比较好时,这没什么问题。但是,大数据量、有限资源时,效率真的很让人崩溃。看了 QCumstomPlot 实现这两个函数的代码。居然,都是先申请一个新的buffer,把旧的 buffer 里内容和新数据一起拷贝到新 buffer !!!而且,buffer 是用 QMap 实现的! 所以,就是我们的数据在内存里拷来拷去。
读了 API 文档,它其实提供了另一个接口,而且它在文档里推荐大家使用这个接口!!!看下面代码:
QCPDataMap *mData = myPlot->graph(0)->data(); mData->clear();
data() 这个调用,返回了指向内部画图 buffer 的指针!然后,我们就可以在需要的时候,往 buffer 里面添加数据:
void addToDataBuffer(float x, float y) { QCPData newData; newData.key = x; newData.value = y; mData->insert(x, newData); }
QCPDataMap 是使用 QMAP 结构实现的一个字典,使用 x 作为字典的 key;字典的值 QCPData 是一个自带操作方法的对象,可以看做坐标横纵坐标的组合 (x,y)。
所以,字典里添加元素,操作就是 insert 了;如果需要多个 x 对应一个 y,那就是字典的 key 可以对应多个值,可以使用 insertMulti() 往 buffer 里面添加数据。
同样的,更新完数据,我们需要 replot() 一下。还有,每次画新的曲线之前,先 clear 一下 buffer 就好。
-----疯哥线
今天有个哥们跟我讲 QCustomPlot 2.0,这个方法不能用了。正好有空,就翻了一下 2.0 的代码,找打下面的方法发给他。我也还没试。而且这方法并没有在官方文档中提到。
void MainWindow::addNewData() { QSharedPointer< QCPGraphDataContainer > dataContainer; dataContainer = customplot->graph(0)->data(); QVector<QCPGraphData> mData; //warning: it's a local var QCPGraphData newPoint; newPoint.key = 123; newPoint.value = 456; mData.append(newPoint); dataContainer->set(mData,true); //if the data has been sorted, set true }
因为画图数据的核心,就是这里的 mData,它就是一个 QCPGraphData 类型的 Vector,所以,我们直接构建这样一个 Vector,把内部的替换掉即可。可惜这里不是指针,函数内实现方式是用我们提供的 mData 给内部 mData 赋值,也就是,还是要进行一次拷贝。当然,你也可以去改它的代码把这个指针给放出来。
-----疯哥线
边吃早饭又看了一下,直接把 mData 给拿出来用了,避开了这次赋值。真不知道作者为什么要搞这么复杂。第一个是示例,第二个是要修改 qcustomplot.h 的(因为 QVector< >不会自动排序,所以,使用这种方法,默认你已经按照key 的大小将数据放好了;没排序的话,你还是用上面那种方法吧):
void MainWindow::mDataDirect(QCustomPlot *customPlot) { demoName = "mData"; customPlot->addGraph(); QVector<QCPGraphData> *mData; mData = customPlot->graph(0)->data()->coreData();
mData->clear();
QCPGraphData newPoint; for (int i=0; i<101; ++i) { double tmp = i/50.0 - 1; newPoint.key = tmp; newPoint.value = tmp * tmp; // let's plot a quadratic function mData->append(newPoint); } // give the axes some labels: customPlot->xAxis->setLabel("x"); customPlot->yAxis->setLabel("y"); // set axes ranges, so we see all data: customPlot->xAxis->setRange(-1, 1); customPlot->yAxis->setRange(0, 1); }
在 qcustomplot.h 中加一行
// setters: void setAutoSqueeze(bool enabled); // myMethod QVector<DataType>* coreData() {return &mData;} // non-virtual methods: void set(const QCPDataContainer<DataType> &data);
3. 画图区域的背景色
不要问我为什么,我是读代码发现的:
QBrush backRole; backRole.setColor("skyblue"); backRole.setStyle(Qt::SolidPattern); myPlot->setBackground(backRole);
如果不加 SolidPattern,画出来的图是透明的。
下面摘自某个已经打不开的博客:
QCustomPlot采用了大量的技术比如自适应采样和文本对象缓存为了减少replot的时间。然而一些特性比如半透明的填充,反锯齿和粗线条都可能导致低效率。如果你在你的程序中注意到了这些。这有一些提示关于如何跳高Replot的性能。
大部分时间耗费在绘图函数上尤其是绘制高密度的图形和其他图。为了最大性能思考下面几点:
使用Qt4.8.0及以上的版本,性能将会有双倍或者三倍的提升跟Qt4.7.4相比。然而QPainter被破坏了并且绘制精确像素的东西使用Qt>=4.8.0的版本是不可能的。因此它是性能和质量的权衡当转到Qt4.8.0时。QCustomPlot内部尝试解决这种严重的故障。
为了增加响应速度在进行范围拖拽的期间,思考设置QCustomPlot::setNoAntialiasingOnDrag为true.
在X11,避免本地缓慢的绘图系统,使用栅格通过应用 "-graphicssystem raster"作为命令行参数或者调用QApplication::setGraphicsSystem("raster") 在创建应用程序对象之前。
在所有的操作系统中,使用OpenGL硬件加速通过提供 "-graphicssystem
opengl"作为命令行参数或者调用QApplication::setGraphicsSystem("opengl")。如果OpenGL是可用的,这将略有减少抗锯齿的质量但是却增强了性能尤其是半透明的填充,抗锯齿和大量的QCustomPlot绘制表面。然而注意最大帧速率的可能被你的显示器的垂直同步频率约束因此对于简单的plot来说,OpenGL加速可能实现帧速率数值低于其他图形系统,因为他们不以垂直同步频率为限制。
避免任何形式的α(透明度),特别是在填充。
避免用宽度大于1的画笔画线。
避免任何反锯齿,尤其是在曲线图中的线。
避免重复设置完整的数据用QCPGraph::setData。使用QCPGraph::addData代替,如果大部分的数据点保持不变如在运行的测量。你可以访问并且操作存在的数据通过QCPGraph::data.
设置setData的拷贝参数为false,因此只有一些点得到转移。
尝试减少数据点的数量在可见的主演范围在给出的任意时刻,通过限制key的最大范围。QCustomPlot可以有效优化掉数以百万计的屏幕点。
4. 更新非 graph 的 plottable
在 qcustomplot 中,graph、curve、bar 这些,都被称为 plottable。其中,针对 graph,qcustomplot 在其顶层有单独的方法操作,如上面例子中的 graph(0) 可以返回添加的第一个 graph。
其他的几种 plottable 都没有单独的方法,都是共用方法。而且,该共用方法也使用 graph。
下面是以 bar 为示例,对其数据进行更新,并重画图:
QVector<double> x(42), y(42); for(int i = 0; i < 42; i++) { x[i] = i; y[i] = ( harmonic_rms[i] * 100) / harmonic_total; } QCPBars *theBar = (QCPBars *)harmonicPlot->plottable(); theBar->setData(x,y); harmonicPlot->rescaleAxes(); harmonicPlot->yAxis->setRange(0, 105); harmonicPlot->xAxis->setRange(0, 42); harmonicPlot->replot();
plottable(int index) 是一个重载函数,如果携带 index,那么返回第 index 个 plottable;如果不带 index,则返回最后一个。因为我这里只有一个 QCPBar,所以,直接不带参数就可以了。