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上用于显示数据点的误差情况。
不同于Graph,其他的Plottable需要用new进行构造,而不是用addCurve、addBars等函数创建。已经存在的Plottable可以通过QCustomPlot::plottable(int index)访问,plottable的数量可以用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();