QCustomPlot開發筆記(一):QCustomPlot簡介、下載以及基礎繪圖

語言: CN / TW / HK

前言

QCustomPlot開發筆記系列整理集合,這是目前使用最為廣泛的Qt圖表類(Qt的QWidget程式碼方向只有QtCharts,Qwt,QCustomPlot),使用多年,系統性的整理,過目並整理了原有文件,本系列旨在系統解說並逐步更新其各種Demo示例。

多年前文章

(本篇不再整理了)

Qt開發筆記之QCustomPlot:QCustomPlot介紹、編譯與使用

QCustomPlot介紹

QCustomPlot是一個小型的qt畫圖示類,效果可以,易用,只需要在專案中加入標頭檔案qcustomplot.h和qcustomplot.cpp檔案,然後使一個widget提升為QCustomPlot類,即可使用。

QCustomPlot 可以匯出為各種格式,如向量化 PDF 檔案和光柵化影象,如 PNG、JPG 和 BMP。QCustomPlot 是在應用程式內顯示實時資料以及為其他媒體生成高質量繪圖的解決方案。

支援的圖形

下圖顯示了使用 QCustomPlot 僅用幾行可以實現的功能。要檢視程式碼,請單擊相應的影象。所有程式碼也可在完整包中找到。

優點與缺點

從效能角度

QCustomPlot是一個比較完善的框架,其框架和快取化的處理使其處理效能大幅度提升(設定筆寬為1,可撐起幾百萬點),而QtCharts只是一個半成品,不論是否有bug,QtCharts在兩千個點以內是可以使用的,超過兩千個點就存在重新整理卡頓的問題(很大可能)。

從定製化角度

QCustomPlot修改原始碼因為其是一步一步繼承過來的,修改起來是比較麻煩的,還需要反覆除錯以防止修改了程式碼出現了其他問題,入新增一個少則半天一天,多則幾天,所以如果需要定製修改QCustomoPlot原始碼的需求,是需要進一步仔細評估是否值得這麼做。

下載地址

最新版本:2.1.0

官網: https://www.qcustomplot.com/index.php/introduction

CSDN粉絲零積分下載: https://download.csdn.net/download/qq21497936/85250427

QQ群:1047134658(點選“檔案”搜尋“qcustomplot”,群內與博文同步更新)

QCustomPlot的部署

直接下載後,將其中的qcstomplot.h和qcustomplot.cpp當作專案檔案新增即可:

更好的方式是模組化部署,如下圖;

QCustomPlot基礎繪圖

(注意:以下示例使用customPlot作為指向QCustomPlot例項的指標。在QtCreator中升級了一個小部件,可能會通過ui->customPlot(或者給小部件起的任何名字)訪問相應的小部件。)

新增圖形

可以通過customPlot->addGraph()在繪圖中建立新的圖形。然後為圖形指定一些資料點,例如通過customPlot->graph(0)->setData(…),例如,以兩個QVector< double >的形式表示x和y(鍵和值)。QCustomPlot使用術語鍵和值而不是x和y的原因是,在指定哪個軸具有什麼角色時允許更大的靈活性。

因此,如果將左軸定義為“關鍵軸”,將底部定義為“值軸”,則可以繪製一個豎立在繪圖左側的圖形。預設情況下,QCustomPlot小部件有四個軸:customPlot->xAxis、yAxis、xAxis2和QCPAxis型別的yAxis2,對應於底部、左側、頂部和右側軸。它們的範圍定義了繪圖的當前可見部分:customPlot->xAxis->setRange(-1,1)。

// generate some data:
QVector<double> x(101), y(101); // initialize with entries 0..100
for (int i=0; i<101; ++i)
{
  x[i] = i/50.0 - 1; // x goes from -1 to 1
  y[i] = x[i]*x[i]; // let's plot a quadratic function
}
// create graph and assign data to it:
customPlot->addGraph();
customPlot->graph(0)->setData(x, y);
// 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);
customPlot->replot();

軸當前使用的軸標記器會自動選擇標記步驟和標籤。這是QCPAxisTicker型別的例項,可以通過xAxis->ticker() 訪問。可以通過xAxis->ticker()->setTickCount(6) 調整ticker嘗試建立的大致刻度數。預設的軸標記器非常適合簡單的數字顯示,但是有專門的類別,例如時間跨度、日曆日期、類別、pi(或其他符號單位)和對數軸。參閱QCPAxisTicker文件。

軸的刻度標籤(數字)永遠不會到達小部件邊界之外,即使它們變寬了。這是由於預設情況下啟用了自動保證計算。如果記號標籤和軸標籤需要更多空間,它會使軸矩形收縮。如果不希望自動確定邊距,請通過呼叫customPlot->axisRect()->setAutoMargins(QCP::msNone) 禁用該行為。然後可以通過customPlot->axisRect()->setMargins(…) 手動調整邊距。

檢視定製

圖形的外觀由許多因素決定,所有這些因素都可以修改。以下是最重要的:

  • 線型:呼叫graph->setLineStyle(…)。有關所有可能的線條樣式,請參閱QCPGraph::LineStyle文件或簡介頁面上的線條樣式演示螢幕截圖。
  • 線條筆:QPainter 框架提供的所有筆都可用,例如實線、虛線、點線、不同的寬度、顏色、透明度等。通過 設定配置的筆graph->setPen(…)。
  • 散點符號:呼叫 graph->setScatterStyle(…) 以更改散點符號的外觀。對於所有可能的散佈樣式,請參閱QCPScatterStyle文件。如果不希望在資料點處顯示任何散點符號,請將圖形的散點樣式設定為 QCPScatterStyle::ssNone
  • 在圖形下或兩個圖形之間填充:QPainter 框架提供的所有畫筆都可以用於圖形填充:實心、各種圖案、紋理、漸變、顏色、透明度等,通過設定配置畫筆graph->setBrush(…)。

軸的外觀可以通過更改其繪製的筆及其標籤使用的字型來修改。檢視QCPAxis的文件應該是不言自明的。下面是最重要屬性的快速總結:setBasePen、setTickPen、setTickLength、setSubTickLength、setSubTickPen、setTickLabelFont、setLabelFont、setTickLabelPadding、setLabelPadding。可以使用setRangeReversed反轉軸(例如,使值從左到右減少而不是增加)。如果要在軸端進行裝飾(如箭頭),請使用setLowerEnding或SetPerEnding。

通過訪問axis的相應QCPGrid例項來修改網格。例如,通過訪問**customPlot->yAxis->grid()來更改與左軸相連的水平網格線的外觀。網格線的外觀基本上是用畫筆繪製的,可以通過yAxis->grid()->setPen()進行設定。勾號0處的網格線可以使用其他筆繪製,也可以使用setZeroLinePen進行配置。如果不想用特殊的鋼筆畫零線,只需將其設定為Qt::NoPen,勾號0處的網格線將用普通的網格筆畫出來。

預設情況下,子柵格線設定為不可見。可以使用grid()->setSubGridVisible(true)**使其可見。

官方示例

兩個圖的簡單圖

下面是一個建立具有指數包絡的衰減餘弦函式影象的示例:

// add two new graphs and set their look:
customPlot->addGraph();
// line color blue for first graph
customPlot->graph(0)->setPen(QPen(Qt::blue)); 
// first graph will be filled with translucent blue
customPlot->graph(0)->setBrush(QBrush(QColor(0, 0, 255, 20)));  
// line color blue for second graph
customPlot->addGraph();
// line color red for second graph
customPlot->graph(1)->setPen(QPen(Qt::red)); 
// generate some points of data (y0 for first, y1 for second graph):
QVector<double> x(251), y0(251), y1(251);
for (int i=0; i<251; ++i)
{
  x[i] = i;
  // exponentially decaying cosine
  y0[i] = qExp(-i/150.0)*qCos(i/10.0); 
  // exponential envelope
  y1[i] = qExp(-i/150.0);              
}
// configure right and top axis to show ticks but no labels:
// (see QCPAxisRect::setupFullAxesBox for a quicker method to do this)
customPlot->xAxis2->setVisible(true);
customPlot->xAxis2->setTickLabels(false);
customPlot->yAxis2->setVisible(true);
customPlot->yAxis2->setTickLabels(false);
// make left and bottom axes always transfer their ranges to right and top axes:
connect(customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), customPlot->xAxis2, SLOT(setRange(QCPRange)));
connect(customPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), customPlot->yAxis2, SLOT(setRange(QCPRange)));
// pass data points to graphs:
customPlot->graph(0)->setData(x, y0);
customPlot->graph(1)->setData(x, y1);
// let the ranges scale themselves so graph 0 fits perfectly in the visible area:
customPlot->graph(0)->rescaleAxes();
// same thing for graph 1, but only enlarge ranges (in case graph 1 is smaller than graph 0):
customPlot->graph(1)->rescaleAxes(true);
// Note: we could have also just called customPlot->rescaleAxes(); instead
// Allow user to drag axis ranges with mouse, zoom with mouse wheel and select graphs by clicking:
customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);

以上,是將填充應用於圖形就像設定一個不是Qt::NoBrush的筆刷一樣簡單。填充將從圖形(此處為圖形0)到與鍵(此處為x)軸平行的零值線。如果我們想在這個圖和另一個圖之間填充通道,我們會另外呼叫graph->setChannelFillGraph(otherGraph)。要刪除通道填充,只需像其他圖形一樣傳遞0,填充將像以前一樣一直到達零值線。要完全刪除填充,請呼叫graph->setBrush(Qt::NoBrush)。

使用多軸列印和更高階的樣式

一個更復雜的示例,用於建立演示如下圖,其中包含四個軸上的五個圖形、紋理填充、垂直錯誤條、圖例、小數點分隔符等。

// period as decimal separator and comma as thousand separator
customPlot->setLocale(QLocale(QLocale::English, QLocale::UnitedKingdom)); 
customPlot->legend->setVisible(true);
// start out with MainWindow's font..
QFont legendFont = font();  
// and make a bit smaller for legend
legendFont.setPointSize(9); 
customPlot->legend->setFont(legendFont);
// by default, the legend is in the inset layout of the main axis rect. So this is how we access it to change legend placement:
customPlot->legend->setBrush(QBrush(QColor(255,255,255,230)));
customPlot->axisRect()->insetLayout()->setInsetAlignment(0, Qt::AlignBottom|Qt::AlignRight);
// setup for graph 0: key axis left, value axis bottom
// will contain left maxwell-like function
customPlot->addGraph(customPlot->yAxis, customPlot->xAxis);
customPlot->graph(0)->setPen(QPen(QColor(255, 100, 0)));
// fill with texture of specified image
customPlot->graph(0)->setBrush(QBrush(QPixmap("./balboa.jpg"))); 
customPlot->graph(0)->setLineStyle(QCPGraph::lsLine);
customPlot->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc, 5));
customPlot->graph(0)->setName("Left maxwell function");

// setup for graph 1: key axis bottom, value axis left (those are the default axes)
// will contain bottom maxwell-like function with error bars
customPlot->addGraph();
customPlot->graph(1)->setPen(QPen(Qt::red));
// same fill as we used for graph 0
customPlot->graph(1)->setBrush(QBrush(QPixmap("./balboa.jpg"))); 
customPlot->graph(1)->setLineStyle(QCPGraph::lsStepCenter);
customPlot->graph(1)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, Qt::red, Qt::white, 7));
customPlot->graph(1)->setName("Bottom maxwell function");
QCPErrorBars *errorBars = new QCPErrorBars(customPlot->xAxis, customPlot->yAxis);
errorBars->removeFromLegend();
errorBars->setDataPlottable(customPlot->graph(1));

// setup for graph 2: key axis top, value axis right
// will contain high frequency sine with low frequency beating:
customPlot->addGraph(customPlot->xAxis2, customPlot->yAxis2);
customPlot->graph(2)->setPen(QPen(Qt::blue));
customPlot->graph(2)->setName("High frequency sine");

// setup for graph 3: same axes as graph 2
// will contain low frequency beating envelope of graph 2
customPlot->addGraph(customPlot->xAxis2, customPlot->yAxis2);
QPen blueDotPen;
blueDotPen.setColor(QColor(30, 40, 255, 150));
blueDotPen.setStyle(Qt::DotLine);
blueDotPen.setWidthF(4);
customPlot->graph(3)->setPen(blueDotPen);
customPlot->graph(3)->setName("Sine envelope");
 
// setup for graph 4: key axis right, value axis top
// will contain parabolically distributed data points with some random perturbance
customPlot->addGraph(customPlot->yAxis2, customPlot->xAxis2);
customPlot->graph(4)->setPen(QColor(50, 50, 50, 255));
customPlot->graph(4)->setLineStyle(QCPGraph::lsNone);
customPlot->graph(4)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssCircle, 4));
customPlot->graph(4)->setName("Some random data around\na quadratic function");
 
// generate data, just playing with numbers, not much to learn here:
QVector<double> x0(25), y0(25);
QVector<double> x1(15), y1(15), y1err(15);
QVector<double> x2(250), y2(250);
QVector<double> x3(250), y3(250);
QVector<double> x4(250), y4(250);
for (int i=0; i<25; ++i) // data for graph 0
{
  x0[i] = 3*i/25.0;
  y0[i] = qExp(-x0[i]*x0[i]*0.8)*(x0[i]*x0[i]+x0[i]);
}
for (int i=0; i<15; ++i) // data for graph 1
{
  x1[i] = 3*i/15.0;;
  y1[i] = qExp(-x1[i]*x1[i])*(x1[i]*x1[i])*2.6;
  y1err[i] = y1[i]*0.25;
}
for (int i=0; i<250; ++i) // data for graphs 2, 3 and 4
{
  x2[i] = i/250.0*3*M_PI;
  x3[i] = x2[i];
  x4[i] = i/250.0*100-50;
  y2[i] = qSin(x2[i]*12)*qCos(x2[i])*10;
  y3[i] = qCos(x3[i])*10;
  y4[i] = 0.01*x4[i]*x4[i] + 1.5*(rand()/(double)RAND_MAX-0.5) + 1.5*M_PI;
}
 
// pass data points to graphs:
customPlot->graph(0)->setData(x0, y0);
customPlot->graph(1)->setData(x1, y1);
errorBars->setData(y1err);
customPlot->graph(2)->setData(x2, y2);
customPlot->graph(3)->setData(x3, y3);
customPlot->graph(4)->setData(x4, y4);
// activate top and right axes, which are invisible by default:
customPlot->xAxis2->setVisible(true);
customPlot->yAxis2->setVisible(true);
// set ranges appropriate to show data:
customPlot->xAxis->setRange(0, 2.7);
customPlot->yAxis->setRange(0, 2.6);
customPlot->xAxis2->setRange(0, 3.0*M_PI);
customPlot->yAxis2->setRange(-70, 35);
// set pi ticks on top axis:
customPlot->xAxis2->setTicker(QSharedPointer<QCPAxisTickerPi>(new QCPAxisTickerPi));
// add title layout element:
customPlot->plotLayout()->insertRow(0);
customPlot->plotLayout()->addElement(0, 0, new QCPTextElement(customPlot, "Way too many graphs in one plot", QFont("sans", 12, QFont::Bold)));
// set labels:
customPlot->xAxis->setLabel("Bottom axis with outward ticks");
customPlot->yAxis->setLabel("Left axis label");
customPlot->xAxis2->setLabel("Top axis label");
customPlot->yAxis2->setLabel("Right axis label");
// make ticks on bottom axis go outward:
customPlot->xAxis->setTickLength(0, 5);
customPlot->xAxis->setSubTickLength(0, 3);
// make ticks on right axis go inward and outward:
customPlot->yAxis2->setTickLength(3, 3);
customPlot->yAxis2->setSubTickLength(1, 1);

可以自由定義哪個軸應該在圖形中扮演哪個角色。例如,索引為0的圖形使用左軸(yAxis)作為其鍵,使用底軸(xAxis)作為其值。因此,該圖形相對於左軸向上:

為了顯示圖表1的錯誤條,我們建立了一個QCPerroBars例項,它可以附加到其他繪圖儀(如QCPGraph)併為它們提供錯誤條。有關所用方法的進一步解釋,請檢視相應的文件。

繪製日期和時間資料

繪製與日期和/或時間相關的資料。基本上可以歸結為在各自的軸上安裝不同的QCPAxisTickerDateTime型別的軸計時器。

// set locale to english, so we get english month names:
customPlot->setLocale(QLocale(QLocale::English, QLocale::UnitedKingdom));
// seconds of current time, we'll use it as starting point in time for data:
double now = QDateTime::currentDateTime().toTime_t();
srand(8); // set the random seed, so we always get the same random data
// create multiple graphs:
for (int gi=0; gi<5; ++gi)
{
  customPlot->addGraph();
  QColor color(20+200/4.0*gi,70*(1.6-gi/4.0), 150, 150);
  customPlot->graph()->setLineStyle(QCPGraph::lsLine);
  customPlot->graph()->setPen(QPen(color.lighter(200)));
  customPlot->graph()->setBrush(QBrush(color));
  // generate random walk data:
  QVector<QCPGraphData> timeData(250);
  for (int i=0; i<250; ++i)
  {
    timeData[i].key = now + 24*3600*i;
    if (i == 0)
      timeData[i].value = (i/50.0+1)*(rand()/(double)RAND_MAX-0.5);
    else
      timeData[i].value = qFabs(timeData[i-1].value)*(1+0.02/4.0*(4-gi)) + (i/50.0+1)*(rand()/(double)RAND_MAX-0.5);
  }
  customPlot->graph()->data()->set(timeData);
}
// configure bottom axis to show date instead of number:
QSharedPointer<QCPAxisTickerDateTime> dateTicker(new QCPAxisTickerDateTime);
dateTicker->setDateTimeFormat("d. MMMM\nyyyy");
customPlot->xAxis->setTicker(dateTicker);
// configure left axis text labels:
QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText);
textTicker->addTick(10, "a bit\nlow");
textTicker->addTick(50, "quite\nhigh");
customPlot->yAxis->setTicker(textTicker);
// set a more compact font size for bottom and left axis tick labels:
customPlot->xAxis->setTickLabelFont(QFont(QFont().family(), 8));
customPlot->yAxis->setTickLabelFont(QFont(QFont().family(), 8));
// set axis labels:
customPlot->xAxis->setLabel("Date");
customPlot->yAxis->setLabel("Random wobbly lines value");
// make top and right axes visible but without ticks and labels:
customPlot->xAxis2->setVisible(true);
customPlot->yAxis2->setVisible(true);
customPlot->xAxis2->setTicks(false);
customPlot->yAxis2->setTicks(false);
customPlot->xAxis2->setTickLabels(false);
customPlot->yAxis2->setTickLabels(false);
// set axis ranges to show all data:
customPlot->xAxis->setRange(now, now+24*3600*249);
customPlot->yAxis->setRange(0, 60);
// show legend with slightly transparent background brush:
customPlot->legend->setVisible(true);
customPlot->legend->setBrush(QColor(255, 255, 255, 150));

傳遞給dateTicker->setDateTimeFormat()的字串與傳遞給QDateTime::toString的字串具有相同的日期格式選項,請參見Qt文件。自1970年1月1日物業其,QCustomPlot中的所有日期/時間座標均以秒為單位處理。UTC(被稱為Unix/大紀元時間),這也是這個單位。

在Qt日期/時間類上呼叫QDateTime::toTime_t或setTime_t。

為了達到小於1秒的精度,axis ticker使用浮點數。因此,小於1.0的值代表相應的秒分數。可以使用QCPAxisTickerDateTime::dateTimeToKey和keyToDateTime在浮點Unix時間和QDateTime之間進行轉換,這與Qt版本無關(Qt的QDateTime::toMSecsSinceEpoch僅在Qt 4.7中引入)。

除了圖形之外:曲線、條形圖、統計方框圖…

到目前為止,看了圖表。由於是要用例,QCustomPlot為它們提供了一個專門的介面。一直在使用它:QCustomPlot::addGraph、QCustomPlot::graph等等,但這並不是全部。**QCustomPlot為在繪圖中繪製資料的類提供了一個更通用的介面,稱為Plottables。**該介面圍繞抽象基類QCPAbstractPlottable構建。所有Plottable都源於這個類,也是我們熟悉的QCPGraph類。

QCustomPlot提供了許多其他可繪製類:

  • QCPGraph:這是我們一直在使用的plottable類。將一系列資料點顯示為具有不同線型、填充和散點的圖形。
  • QCPCurve:與QCPGraph類似,不同之處在於它用於顯示引數曲線。與函式圖不同,它們可能有迴圈。
  • QCPBars:條形圖。獲取一系列資料點,並用條形圖表示它們。如果繪圖中有多個QCPBAR繪圖表,則可以將它們堆疊在一起,如簡介頁面上的螢幕截圖所示。
  • QCPStatisticalBox:一個統計方框圖。獲取五位數的摘要(最小值、下四分位、中位數、上四分位、最大值),並將其表示為一個統計框。也可以顯示異常值。
  • QCPColorMap:一種2D地圖,通過使用顏色漸變來視覺化第三個資料維度。QCPColorScale類隨此繪圖表一起顯示繪圖中的資料比例。
  • QCPFFinancial:一個繪圖表,可以通過使用燭臺或OHLC條顯示股票價格的開盤、高點、低點和收盤資訊。
  • QCPErrorBars:這是一個特殊的繪圖表,因為它連線到第二個繪圖表,以便在其他繪圖表的資料點上顯示錯誤條。
    與圖形不同,其他繪圖表需要使用QCustomPlot外部的新圖形建立。這意味著沒有像addGraph函式那樣的addCurve或addBars函式。繪圖儀應屬於的QCustomPlot例項是從繪圖儀建構函式中傳遞的軸推斷出來的。然後,QCustomPlot獲得繪圖表的所有權。可以使用QCustomPlot::plottable(int index)訪問現有的繪圖表,並且可以使用QCustomPlot::plottableCount檢索繪圖中的繪圖表總數(包括圖形)

下面是一個建立三條條形圖的快速示例:

QCPBars *myBars = new QCPBars(customPlot->xAxis, customPlot->yAxis);
// now we can modify properties of myBars:
myBars->setName("Bars Series 1");
QVector<double> keyData;
QVector<double> valueData;
keyData << 1 << 2 << 3;
valueData << 2 << 4 << 8;
myBars->setData(keyData, valueData);
customPlot->rescaleAxes();
customPlot->replot();

有關其他繪圖表的更多詳細資訊,請參見示例專案和其他教程。此外,每種繪圖儀型別在相應類別的文件頁面上都有詳細說明。

當然,完全有可能編寫自己的繪圖儀,使任何資料都完全符合需要,可以去檢視QCPAbstractPlottable文件,瞭解如何開始子類化它。還可以檢視現有的繪圖表,瞭解它們的工作原理。為此,建議先看看QCPBars或QCPCurve。