前言

  QCustomPlot开发笔记系列整理集合,这是目前使用最为广泛的Qt图表类(Qt的QWidget代码方向只有QtCharts,Qwt,QCustomPlot),使用多年,系统性的整理,过目并整理了原有文档,本系列旨在系统解说并逐步更新其各种Demo示例。

<br>

多年前文章

  (本篇不再整理了)
  《Qt开发笔记之QCustomPlot:QCustomPlot介绍、编译与使用

<br>

QCustomPlot介绍

简介

  QCustomPlot是一个小型的qt画图标类,效果可以,易用,只需要在项目中加入头文件qcustomplot.hqcustomplot.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

<br>

QCustomPlot的部署

  直接下载后,将其中的qcstomplot.hqcustomplot.cpp当作项目文件添加即可:
  在这里插入图片描述
  更好的方式是模块化部署,如下图;
  在这里插入图片描述

<br>

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的文档应该是不言自明的。下面是最重要属性的快速总结:setBasePensetTickPensetTickLengthsetSubTickLengthsetSubTickPensetTickLabelFontsetLabelFontsetTickLabelPaddingsetLabelPadding。可以使用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] = 3i/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
3M_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.01x4[i]x4[i] + 1.5(rand()/(double)RAND_MAX-0.5) + 1.5M_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);

&emsp;&emsp;可以自由定义哪个轴应该在图形中扮演哪个角色。例如,索引为0的图形使用左轴(yAxis)作为其键,使用底轴(xAxis)作为其值。因此,该图形相对于左轴向上:
&emsp;&emsp;![在这里插入图片描述](https://s4.51cto.com/images/blog/202205/01153054_626e372e1757830904.png?x-oss-process=image/watermark,size_14,text_QDUxQ1RP5Y2a5a6i,color_FFFFFF,t_30,g_se,x_10,y_10,shadow_20,type_ZmFuZ3poZW5naGVpdGk=)
&emsp;&emsp;为了显示图表1的错误条,我们创建了一个QCPerroBars实例,它可以附加到其他绘图仪(如QCPGraph)并为它们提供错误条。有关所用方法的进一步解释,请查看相应的文档。
## 绘制日期和时间数据
&emsp;&emsp;绘制与日期和/或时间相关的数据。基本上可以归结为在各自的轴上安装不同的QCPAxisTickerDateTime类型的轴计时器。
```cpp
// 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_tsetTime_t
  在这里插入图片描述
  为了达到小于1秒的精度,axis ticker使用浮点数。因此,小于1.0的值代表相应的秒分数。可以使用QCPAxisTickerDateTime::dateTimeToKeykeyToDateTime在浮点Unix时间和QDateTime之间进行转换,这与Qt版本无关(Qt的QDateTime::toMSecsSinceEpoch仅在Qt 4.7中引入)。

除了图形之外:曲线、条形图、统计方框图…

  到目前为止,看了图表。由于是要用例,QCustomPlot为它们提供了一个专门的接口。一直在使用它:QCustomPlot::addGraphQCustomPlot::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。