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,所以,直接不帶參數就可以了。