0、说明
本节翻译总结自:Qt Plotting Widget QCustomPlot - Basic Plotting
本节内容是使用QCustomPlot进行基本绘图。
本节教程都使用customPlot这个变量,它是一个指向QCustomPlot实例的指针,当然,在我们的项目中,我们更可能是通过ui->customPlot来访问这些QCustomPlot实例。
1、基本用法
1.1、创建新画布
customPlot -> addGraph();
每个Graph及其上的线构成一幅图。
1.2、给画布分配数据点
customPlot->graph(0)->setData(x,y)
x、y是大小相等的一组数据,其中x中存储的是横坐标,y中存储的是纵坐标。
1.3、轴
假设我们有两个QVector<double>,分别存放了一组点的x和y坐标(Key与Value)。不过QCustomPlot更倾向于使用Key与Value而非x与y,这样可以更灵活地区分哪个轴具有什么样的功能。所以当我们定义左边轴为Key轴而底部轴为Value轴时,我们就可以沿着左边的轴绘制一幅直立的图。
QCustomPlot有4个轴customPlot->xAxis, yAxis, xAxis2, 和 yAxis2,它们都是QCPAxis类型的,分别对应下、左、上、右。
如果要设置轴的范围为(-1,1),可以用:
customPlot->xAxis->setRange(-1,1);
1.4、重绘
如果对画布做了任何修改,可以用
customPlot->replot();
进行重绘。
不过每当Widget被拉伸或者触发了内置用户交互时,都会自动进行重绘;这里的交互是指通过鼠标拖动坐标轴的范围、用鼠标滚轮缩放等。
1.5、例子
绘制一个y=x2的曲线:
QVector<double> x(101),y(101); for(int i=0;i<101;i++){ x[i]=i/50.0-1;//设置x的范围为-1~1 y[i]=x[i]*x[i]; } //创建画布,设置画布上的点数据 ui->customPlot->addGraph(); ui->customPlot->graph(0)->setData(x,y); //设置坐标轴标签 ui->customPlot->xAxis->setLabel("x"); ui->customPlot->yAxis->setLabel("y"); //设置坐标轴范围,以便我们可以看到全部数据 ui->customPlot->xAxis->setRange(-1,1); ui->customPlot->yAxis->setRange(0,1); ui->customPlot->replot();
坐标轴刻度、间隔和显示的数字是由axis ticker决定的,它是QCPAxisTicker类型的变量,通过xAxis->ticker()访问。
可以通过xAxis->ticker()->setTickCount(6)来调整坐标轴上显示的刻度数值的个数。默认情况下,这个个数是自适应的最少的个数。不过,也有一些特殊的情况,比如坐标轴是时间、日期、分类、对数坐标系的情况。具体可以看QCPAxisTicker。
2、改变样式
2.1、画布
2.1.1、线的样式
有哪些线的样式,可以看QCPGraph::LineStyle。
2.1.2、画笔
graph->setPen(..)
2.1.3、点的样式
有哪些点的样式,可以看QCPScatterStyle
2.1.4、填充色
graph->setBrush(..):填充该线和0刻度线围成的区域。
graph->setChannelFillGraph(otherGraph):填充两条线之间的区域;如果要移除线间填充,只需要把参数设置为0即可,这时填充的区域为该线与0刻度线围成的区域。
如果要完全移除区域填充,用graph->setBrush(Qt::NoBrush)
2.2、坐标轴
坐标轴多是通过改变画笔和字体进行修改的。可以看QCPAxis。
这里给出一些比较重要的特性:
setBasePen
, setTickPen
, setTickLength
, setSubTickLength
, setSubTickPen
, setTickLabelFont
, setLabelFont
, setTickLabelPadding
, setLabelPadding
.
我们可以用setRangeReversed来颠倒一个坐标轴(例如按照从高到低的顺序绘制)。
如果想修饰坐标轴的尾部(例如添加箭头),可以用setLowerEnding或setUpperEnding。
2.3、网格线
网格线是QCPGrid对象。
网格线是和坐标轴绑定的,水平网格线是与左坐标轴绑定的,通过customPlot->yAxis->grid()访问。网格线的外观由绘制它的画笔决定,通过yAxis->grid()->setPen()进行设置。
位于0刻度处的网格线可以用不同的画笔绘制,该画笔样式通过setZeroLinePen进行配置。如果我们不想用特殊的画笔绘制零刻度线,只需要把它设置为Qt::NoPen即可,这样0刻度线就和普通刻度线的绘制方法一样了。
子网格线默认是隐藏的,可以用grid()->setSubGridVisible(true)激活。
2.4、例1:两幅图叠加绘制
下边的代码是绘制一幅衰减的余弦曲线和它的指数包裹曲线,每条曲线即是一条Graph
//添加两幅图并填充内容 //第一幅图 ui->customPlot->addGraph(); //线的颜色 ui->customPlot->graph(0)->setPen(QPen(Qt::blue)); //用半透明的蓝色填充 ui->customPlot->graph(0)->setBrush(QBrush(QColor(0,0,255,20))); //第二幅图 ui->customPlot->addGraph(); ui->customPlot->graph(1)->setPen(QPen(Qt::red)); //生成数据点 QVector<double> x(251),y0(251),y1(251); for(int i=0;i<251;i++){ x[i]=i; y0[i]=qExp(-i/150.0)*qCos(i/10.0);//指数衰减的余弦曲线 y1[i]=qExp(-i/150.0);//指数包裹曲线 } //配置上和右坐标轴来显式刻度,但是不显示数字 ui->customPlot->xAxis2->setVisible(true); ui->customPlot->xAxis2->setTickLabels(false); ui->customPlot->yAxis2->setVisible(true); ui->customPlot->yAxis->setTickLabels(false); //修改左和底坐标轴,使之与右和上坐标轴始终匹配 connect(ui->customPlot->xAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->xAxis2, SLOT(setRange(QCPRange))); connect(ui->customPlot->yAxis, SIGNAL(rangeChanged(QCPRange)), ui->customPlot->yAxis2, SLOT(setRange(QCPRange))); //为每幅图设置点 ui->customPlot->graph(0)->setData(x,y0); ui->customPlot->graph(1)->setData(x,y1); //使图0自适应范围 ui->customPlot->graph(0)->rescaleAxes(); //图1页一样,但是只允许放大,以免比图0小 ui->customPlot->graph(1)->rescaleAxes(true); //注意,以上两步也可以用ui->customPlot->rescaleAxes();来代替 //允许用户用鼠标拖拉、缩放、选择任一幅图 ui->customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
结果:
正如上文所写,为Graph进行色彩填充,只需要用setBrush()就可以实现。
2.5、例2:多轴、多样式绘图
接下来,我们来看一个更复杂的例子,这个例子中包含了4个坐标轴上的4个Graph,纹理填充,垂直误差、图例、分割点等内容
QCustomPlot * cp = ui->customPlot; QCustomPlot * customPlot = cp; //设置区域,点号作为小数分隔符、逗号作为千位分隔符 cp->setLocale(QLocale(QLocale::English,QLocale::UnitedKingdom)); cp->legend->setVisible(true); //用MainWindow字体,减小字体大小 QFont legendFont=font(); legendFont.setPointSize(9); cp->legend->setFont(legendFont); cp->legend->setBrush(QBrush(QColor(255,255,255,230))); //默认情况下,图例是嵌入主框架之中,接下来演示如何修改它的布局 cp->axisRect()->insetLayout()->setInsetAlignment(0,Qt::AlignBottom | Qt::AlignRight); //配置第一幅图,Key轴是左,Value轴是底 cp->addGraph(cp->yAxis,cp->xAxis); cp->graph(0)->setPen(QPen(QColor(255,100,0))); cp->graph(0)->setBrush(QBrush(QPixmap("./balboa.jpg"))); cp->graph(0)->setLineStyle(QCPGraph::lsLine); cp->graph(0)->setScatterStyle(QCPScatterStyle(QCPScatterStyle::ssDisc,5)); cp->graph(0)->setName("Left maxwell function"); //配置第二幅图,Key是底,Value是左 customPlot->addGraph(); customPlot->graph(1)->setPen(QPen(Qt::red)); customPlot->graph(1)->setBrush(QBrush(QPixmap("./balboa.jpg"))); // same fill as we used for graph 0 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(cp->graph(1)); //配置第三幅图,Key是顶,Value是右 customPlot->addGraph(customPlot->xAxis2, customPlot->yAxis2); customPlot->graph(2)->setPen(QPen(Qt::blue)); customPlot->graph(2)->setName("High frequency sine"); //配置第四幅图,轴与第三幅图相同 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"); //配置第五幅图,右为Key轴,顶为Value轴 //第五幅图中包含一些随机扰动的点 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"); //生成数据 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; } //为每幅图设置数据 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); //激活顶和右坐标轴 customPlot->xAxis2->setVisible(true); customPlot->yAxis2->setVisible(true); //设置显示数据的合适的范围 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); //为顶轴设置刻度为pi相关刻度 customPlot->xAxis2->setTicker(QSharedPointer<QCPAxisTickerPi>(new QCPAxisTickerPi)); //添加标题布局 customPlot->plotLayout()->insertRow(0); customPlot->plotLayout()->addElement(0, 0, new QCPTextElement(customPlot, "Way too many graphs in one plot", QFont("sans", 12, QFont::Bold))); //设置各个轴的标签 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"); //使底轴刻度向外延伸 customPlot->xAxis->setTickLength(0, 5); customPlot->xAxis->setSubTickLength(0, 3); //使右轴刻度向内和向外延伸 customPlot->yAxis2->setTickLength(3, 3); customPlot->yAxis2->setSubTickLength(1, 1);
结果:
正如结果所示,我们可以*定义哪个轴占主导地位。
为了展示图2的误差线,我们构造了QCPErrorBars实例用以绘制其他Graph的误差线。
2.6、例3:绘制日期、时间数据
本例介绍如何绘制时间、日期相关的数。要实现这一目的,需要在每个轴上使用QCPAxisTickerDateTime作为轴刻度。
QCustomPlot * cp = ui->customPlot; QCustomPlot * customPlot = ui->customPlot; cp->setLocale(QLocale(QLocale::English,QLocale::UnitedKingdom)); //当前时间戳,用它作为起始点 double now = QDateTime::currentDateTime().toSecsSinceEpoch(); //设置随机数种子 srand(8); //生成多幅graph for(int gi=0;gi<5;gi++){ cp->addGraph(); QColor color(20+200/4.0*gi,70*(1.6-gi/4.0),150,150); cp->graph()->setLineStyle(QCPGraph::lsLine); cp->graph()->setPen(QPen(color.lighter(200))); cp->graph()->setBrush(QBrush(color)); //产生随机数据 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); cp->graph()->data()->set(timeData); } //配置底轴以显示日期而非数字 QSharedPointer<QCPAxisTickerDateTime>dateTicker(new QCPAxisTickerDateTime); dateTicker->setDateTimeFormat("d._MMMM\nyyyy"); cp->xAxis->setTicker(dateTicker); //配置左轴 QSharedPointer<QCPAxisTickerText> textTicker(new QCPAxisTickerText); textTicker->addTick(10,"a bit\nlow"); textTicker->addTick(50,"quite/nhigh"); cp->yAxis->setTicker(textTicker); //为左轴和底轴的刻度设置合适的字体 cp->xAxis->setTickLabelFont(QFont(QFont().family(),8)); cp->yAxis->setTickLabelFont(QFont(QFont().family(),8)); //设置轴标签 cp->xAxis->setLabel("Date"); cp->yAxis->setLabel("Random wobbly lines value"); //使顶轴和右轴可见,但是并不显示刻度值和标签 customPlot->xAxis2->setVisible(true); customPlot->yAxis2->setVisible(true); customPlot->xAxis2->setTicks(false); customPlot->yAxis2->setTicks(false); customPlot->xAxis2->setTickLabels(false); customPlot->yAxis2->setTickLabels(false); //设置轴范围以显示所有数据 cp->xAxis->setRange(now,now+24*3600*249); cp->yAxis->setRange(0,60); //显示图例,图例背景轻微透明 cp->legend->setVisible(true); cp->legend->setBrush(QColor(255,255,255,150));
结果:
我们传入dateTicker->setDateTimeFormat()中的字符串,需要符合ISO8601时间格式,除了时间格式之外的其他字符都会原封不动的保留,而时间格式的字符会被正确填充。
需要注意的是,QCustomPlot处理的时间/日期坐标系,都是以时间戳0起始坐标点的,,单位是s。所以当我们构造时,也要以时间戳为横坐标,只是在显示的时候通过时间/日期占位符转化为指定的时间/日期。
如果精度是比秒更小的单元,那么可以用浮点数。我们可以用QCPAxisTickerDateTime::dateTimeToKey和keyToDateTime用于浮点Unix Time和QDateTime间的转换。
而对于QDateTime::toMSecsSinceEpoch,则是以毫秒为单位,也可以使用这种方式构建更微小的时间精度。
3、图形之外:曲线、图标、统计图
目前为止,我们只应用了Graph,因为Graph是我们使用QCustomPlot的主要内容,QCustomPlot为我们使用Graph提供了专门的接口,我们也一直在使用它们,比如QCustomPlot::addGraph、QCustomPlot::graph等等。但是这些并不是全部。
QCustomPlot也有许多更通用的绘图接口,称为Plottable,这个接口是围绕抽象类QCPAbstractPlottable构建的。所有的Plottables都是从这个类继承而来,当然也包括了最熟悉的QCPGraph类。现在介绍一些QCustomPlot提供的Plottable类:
- QCPGraph:这是我们本节经常用的一个Plottable类。用于点、线、填充的方式展示一系列的数据;
- QCPCurve:与QCPgraph类似,不同之处在于它用于展示参数曲线。不同于函数Graph,使用这个类时可能有循环;
- QCPBars:柱状图;
- QCPStatisticalBox::箱型图;
- QCPColorMap:通过使用颜色梯度将第三个维度可视化的2D图;
- QCPFinancial: 用于可视化股价的Plottable;
- QCPErrorBars: 一个特殊的Plottable,附加在另一个Plottable上用于显示数据点的误差情况。