一、为什么选择 QCustomPlot
在 Qt 生态中做数据可视化,绕不开 QCustomPlot。相比 QCharts,它开源免费(GPL 或商业授权)、无第三方依赖、绘制性能优秀,尤其在实时曲线场景下表现突出。它本质是一个 QWidget 子类,可以直接拖入 UI 或代码创建,适合工业监控、科学计算、金融行情等对实时性要求高的场景。
核心优势:
- 轻量:仅两个文件(qcustomplot.h / qcustomplot.cpp),集成成本极低
- 高性能:支持 OpenGL 加速、实时数据追加、局部重绘
- 功能全:曲线、柱状图、散点图、误差棒、颜色图、极坐标图等一应俱全
- 文档详尽:官方自带 60+ 示例程序,是学习的最佳教材
二、快速上手
引入头文件后,三行代码画出一条正弦曲线:
#include "qcustomplot.h"
// 在构造函数中
QCustomPlot *customPlot = new QCustomPlot(this);
// 1. 添加一条曲线
customPlot->addGraph();
// 2. 写入数据
customPlot->graph(0)->setData(xData, yData); // QVector<double>
// 3. 重绘
customPlot->replot();
关键点:setData 接收的是 QVector<double>,而不是 QList。在数据量大的场景下,QVector 的内存连续性对性能至关重要。
三、核心概念与层次结构
QCustomPlot ├── QCPAxisRect (坐标轴矩形,可多个) │ ├── QCPAxis xAxis / yAxis (轴) │ ├── QCPGrid (网格) │ └── QCPLayerable (图层对象) ├── QCPGraph (曲线,需 addGraph 创建) ├── QCPLegend (图例) └── QCPLayoutGrid (布局)
理解这个层次结构,是灵活使用 QCustomPlot 的前提。很多新手困惑"为什么我的图例不显示",往往是因为没有设置 graph->setName() 或图例被布局吞掉了。
四、核心 API 介绍
4.1 QCustomPlot 主类
| API | 说明 |
|---|---|
| addGraph() | 添加一条曲线,返回 QCPGraph*,索引从 0 开始 |
| graph(int index) | 获取指定索引的曲线 |
| graphCount() | 获取曲线数量 |
| removeGraph(QCPGraph*) | 移除曲线 |
| addPlottable(QCPAbstractPlottable*) | 添加任意绘图对象(柱状图、颜色图等) |
| addItem(QCPAbstractItem*) | 添加 Item 对象(文本、直线、箭头、光标等) |
| replot() | 手动重绘整个绘图区 |
| setOpenGl(bool) | 开启/关闭 OpenGL 加速(2.x 内置) |
| rescaleAxes() | 自动缩放所有轴以容纳全部数据 |
| savePng/saveJpg/savePdf/saveBmp | 导出图片/PDF |
| setInteractions(QCP::iRangeDrag | QCP::iRangeZoom) | 启用拖拽与滚轮缩放交互 |
4.2 QCPGraph 曲线 API
| API | 说明 |
|---|---|
| setData(QVector<double> x, QVector<double> y) | 整体替换数据 |
| addData(double x, double y) | 追加单个数据点 |
| addData(QVector<double> x, QVector<double> y) | 批量追加 |
| data() | 获取数据容器 QCPDataContainer* |
| setPen(QPen) | 设置线条样式 |
| setBrush(QBrush) | 设置填充(可做面积图) |
| setScatterStyle(QCPScatterStyle) | 设置散点样式 |
| setName(QString) | 设置名称(图例显示用) |
| setLineStyle(QCPGraph::lsNone/lsLine/lsStepLeft/lsImpulse) | 线条样式:无/直线/阶梯/脉冲 |
| rescaleValueAxis(bool, bool) | 只缩放值轴(Y 轴) |
| setAdaptiveSampling(bool) | 自适应降采样,默认开启 |
4.3 QCPAxis 坐标轴 API
| API | 说明 |
|---|---|
| setRange(double lower, double upper) | 设置显示范围 |
| setRange(QCPRange) | 同上 |
| setRangeLower/setRangeUpper | 单独设置上下限 |
| setLabel(QString) | 设置轴标题 |
| setTicks(bool) | 是否显示刻度 |
| setTickLabels(bool) | 是否显示刻度数值 |
| pixelToCoord(double pixel) | 像素坐标转数据坐标(十字光标常用) |
| coordToPixel(double coord) | 数据坐标转像素坐标 |
| setScaleType(QCPAxis::stLinear/stLogarithmic) | 线性/对数轴 |
| setDateTimeFormat(QString) | 时间轴格式(需 setTicker 配置) |
4.4 QCPColorMap 颜色图 API
| API | 说明 |
|---|---|
| data()->setSize(int nx, int ny) | 设置网格尺寸 |
| data()->setRange(QCPRange, QCPRange) | 设置数据范围 |
| data()->setCell(int x, int y, double value) | 逐格赋值 |
| rescaleDataRange() | 自动调整颜色映射范围 |
| setGradient(QCPColorGradient) | 设置颜色渐变(如 gpJet、gpHot、gpThermal) |
| setColorScale(QCPColorScale*) | 绑定颜色条 |
4.5 QCPItem 系列(标注/光标)
| 类 | 用途 |
|---|---|
| QCPItemText | 文本标注 |
| QCPItemLine / QCPItemStraightLine | 线段 / 无限长直线(阈值线常用) |
| QCPItemTracer | 轨迹光标,可绑定到曲线数据 |
| QCPItemRect / QCPItemEllipse | 矩形 / 椭圆区域 |
| QCPItemArrow | 箭头 |
4.6 交互与信号
// 启用交互
customPlot->setInteractions(QCP::iRangeDrag | QCP::iRangeZoom | QCP::iSelectPlottables);
// 常用信号
connect(customPlot, &QCustomPlot::mouseMove, this, &MyWidget::onMouseMove);
connect(customPlot, &QCustomPlot::mousePress, this, &MyWidget::onMousePress);
connect(customPlot, &QCustomPlot::plottableClick, this, &MyWidget::onPlottableClick);
connect(customPlot, &QCustomPlot::selectionChangedByUser, this, &MyWidget::onSelectionChanged);
五、使用例子
注意:提供的例子只有核心实现部分
5.1 例一:基础曲线 + 图例 + 网格
// 初始化
customPlot->addGraph();
customPlot->graph(0)->setName("正弦");
customPlot->graph(0)->setPen(QPen(QColor(40, 110, 255)));
customPlot->graph(0)->setData(x, ySin);
// 第二条曲线
customPlot->addGraph();
customPlot->graph(1)->setName("余弦");
customPlot->graph(1)->setPen(QPen(QColor(255, 110, 40)));
customPlot->graph(1)->setData(x, yCos);
// 坐标轴
customPlot->xAxis->setLabel("时间 (s)");
customPlot->yAxis->setLabel("幅值");
customPlot->xAxis->setRange(0, 10);
customPlot->yAxis->setRange(-1.5, 1.5);
// 网格与图例
customPlot->xAxis->grid()->setPen(QPen(QColor(200, 200, 200), 1, Qt::DotLine));
customPlot->legend->setVisible(true);
customPlot->legend->setFont(QFont("Microsoft YaHei", 9));
customPlot->replot();
5.2 例二:实时滚动曲线(监控场景)
// 头文件成员:QTimer m_timer; QVector<double> m_values;
m_timer.setInterval(50); // 20Hz 刷新
connect(&m_timer, &QTimer::timeout, this, &MyWidget::updateData);
void MyWidget::updateData()
{
static double t = 0;
t += 0.05;
double v = qSin(t * 2) + qCos(t * 0.7) * 0.5;
// 追加数据
customPlot->graph(0)->addData(t, v);
// 滚动窗口:X 轴跟随最新数据
customPlot->xAxis->setRange(t, 10, Qt::AlignRight);
// 合并重绘请求,避免高频全量重绘
customPlot->replot(QCustomPlot::rpQueuedReplot);
}
5.3 例三:柱状图
QCPBars *bars = new QCPBars(customPlot->xAxis, customPlot->yAxis);
bars->setName("销量");
bars->setPen(QPen(QColor(0, 160, 120)));
bars->setBrush(QColor(0, 160, 120, 100));
QVector<double> x = {1, 2, 3, 4, 5};
QVector<double> y = {120, 95, 210, 180, 260};
bars->setData(x, y);
customPlot->xAxis->setRange(0, 6);
customPlot->yAxis->setRange(0, 300);
customPlot->legend->setVisible(true);
customPlot->replot();
5.4 例四:十字光标 + 数值标签
// 创建光标线与文本
QCPItemStraightLine *vLine = new QCPItemStraightLine(customPlot);
vLine->setPen(QPen(QColor(100, 100, 100), 1, Qt::DashLine));
vLine->point1->setCoords(0, 0);
vLine->point2->setCoords(0, 1);
QCPItemText *tip = new QCPItemText(customPlot);
tip->setText("(0, 0)");
tip->setFont(QFont("Microsoft YaHei", 9));
tip->position->setType(QCPItemPosition::ptAxisRectRatio);
tip->position->setCoords(0.02, 0.95);
connect(customPlot, &QCustomPlot::mouseMove, this, [this, vLine, tip](QMouseEvent *e) {
double x = customPlot->xAxis->pixelToCoord(e->pos().x());
double y = customPlot->yAxis->pixelToCoord(e->pos().y());
vLine->point1->setCoords(x, 0);
vLine->point2->setCoords(x, 1);
tip->setText(QString("(%1, %2)").arg(x, 0, 'f', 2).arg(y, 0, 'f', 2));
customPlot->replot();
});
5.5 例五:热力图(ColorMap)
QCPColorMap *colorMap = new QCPColorMap(customPlot->xAxis, customPlot->yAxis);
const int NX = 100, NY = 80;
colorMap->data()->setSize(NX, NY);
colorMap->data()->setRange(QCPRange(0, NX), QCPRange(0, NY));
for (int x = 0; x < NX; ++x)
for (int y = 0; y < NY; ++y)
colorMap->data()->setCell(x, y, qSin(x / 10.0) * qCos(y / 8.0));
colorMap->rescaleDataRange();
colorMap->setGradient(QCPColorGradient::gpJet);
// 右侧颜色条
QCPColorScale *colorScale = new QCPColorScale(customPlot);
customPlot->plotLayout()->addElement(0, 1, colorScale);
colorMap->setColorScale(colorScale);
colorScale->setDataRange(colorMap->dataRange());
customPlot->rescaleAxes();
customPlot->replot();
5.6 例六:多图布局(上下两个绘图区)
// 在已有绘图区下方再添加一个
customPlot->plotLayout()->insertRow(1);
customPlot->plotLayout()->addElement(1, 0, new QCPAxisRect(customPlot));
// 获取第二个绘图区的轴
QCPAxisRect *rect2 = customPlot->axisRect(1);
rect2->axis(QCPAxis::atBottom)->setLabel("时间 (s)");
rect2->axis(QCPAxis::atLeft)->setLabel("功率 (W)");
// 在第二个绘图区画图
QCPGraph *g2 = customPlot->addGraph(rect2->axis(QCPAxis::atBottom), rect2->axis(QCPAxis::atLeft));
g2->setData(xPower, yPower);
5.7 例七:导出图片 / PDF
// 导出 PNG(默认 0 表示按当前控件尺寸)
customPlot->savePng("chart.png", 0, 0, 1.0, -1);
// 导出 PDF(矢量,适合报告插图)
customPlot->savePdf("chart.pdf");
// 高质量导出:设置分辨率缩放
customPlot->savePng("chart_hd.png", 1920, 1080, 2.0, -1);
六、性能优化实战
实时绘制到几万、几十万数据点时,必须注意以下几点:
- 开启 OpenGL 加速(QCustomPlot 2.x 已内置,无需额外库):
customPlot->setOpenGl(true);
注意:OpenGL 模式下部分功能(如 QCPErrorBars)受限,需要取舍。
-
数据降采样:数据点超过屏幕像素数时,肉眼根本分辨不出差异。可对历史数据做抽稀,只保留屏幕宽度 × 3 左右的点数。QCPGraph::setAdaptiveSampling(true) 默认开启,绘制时自动降采样。
-
关闭不必要的重绘:
customPlot->setNoAntialiasingOnDrag(true); // 拖拽时关闭抗锯齿
customPlot->setNotAntialiasedElements(QCP::aeAll); // 全关抗锯齿,性能翻倍
-
批量追加:addData 支持传入 QVector,一次性追加一批数据,比逐点调用快得多。
-
避免频繁 setRange + replot:改用 QCPAxis::setRange 后统一一次 replot,减少布局计算。
-
数据存储:全部使用 QVector,避免 QList 在大型数据下的内存碎片与随机访问开销。
七、常见问题速查表(FAQ)
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 曲线不显示 | 未 addGraph() 直接操作 graph(0) | 先 addGraph() 再 setData |
| 图例不显示 | 未设置 setName | 给每条曲线设置名称,并 legend->setVisible(true) |
| 实时刷新卡顿 | 每次数据都全量 replot() | 改用 rpQueuedReplot + 节流 |
| 坐标轴标签乱跳 | 频繁 rescaleAxes() | 手动 setRange 固定显示窗口 |
| 中文显示为方块 | 未设置中文字体 | 全局设置 QFont("Microsoft YaHei") 等中文字体 |
| 自定义 Item 不显示 | 未添加到图层 | addItem() 添加,或检查图层可见性 |
| 大数据量内存暴涨 | QList 存储 | 全程使用 QVector |
| 拖拽缩放无反应 | 未启用交互 | setInteractions(QCP::iRangeDrag | QCP::iRangeZoom) |
| 导出的 PDF 是空白 | 使用了 OpenGL 模式 | PDF 导出与 OpenGL 冲突,导出前临时关闭 setOpenGl(false) |
| 时间轴显示为科学计数法 | 未配置时间刻度器 | 使用 QCPAxisTickerDateTime 并 setDateTimeFormat("hh:mm:ss") |
最隐蔽的坑:在 replot() 之前直接修改 graph->data() 的内部容器,会导致 QCustomPlot 内部索引失效,表现为间歇性绘制错乱。正确做法是始终通过 setData / addData / data()->clear() 等公开接口操作。
时间轴正确用法:
QSharedPointer<QCPAxisTickerDateTime> dateTicker(new QCPAxisTickerDateTime);
dateTicker->setDateTimeFormat("MM-dd hh:mm:ss");
dateTicker->setDateTimeSpec(Qt::LocalTime);
customPlot->xAxis->setTicker(dateTicker);
// 时间戳用 QDateTime::toSecsSinceEpoch() 转换
customPlot->graph(0)->addData(QDateTime::currentDateTime().toSecsSinceEpoch(), value);
八、环境验证
把下面代码放入 Qt Widgets 工程(.pro 中加入 include(../qcustomplot/qcustomplot.pri),或直接把两个源文件加入工程),编译运行,看到一条滚动正弦曲线即为环境就绪:
#include "qcustomplot.h"
#include <QApplication>
#include <QTimer>
#include <QtMath>
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QCustomPlot plot;
plot.addGraph();
plot.graph(0)->setPen(QPen(QColor(40, 110, 255)));
plot.xAxis->setLabel("时间 (s)");
plot.yAxis->setLabel("幅值");
plot.resize(800, 480);
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, [&plot]() {
static double t = 0;
t += 0.02;
plot.graph(0)->addData(t, qSin(t));
plot.xAxis->setRange(t, 5, Qt::AlignRight);
plot.yAxis->rescaleValueAxis(false, true);
plot.replot(QCustomPlot::rpQueuedReplot);
});
timer.start(20);
plot.show();
return app.exec();
}
九、商业化授权提醒
QCustomPlot 采用 GPLv3 或商业授权双许可。如果你的项目需要闭源商用,务必购买商业授权(官网 qcustomplot.com,约 49 欧元起);个人学习或 GPL 开源项目则免费使用。这是很多企业踩过的法律坑,提前规避。
十、学习资源
- 官方示例:下载源码包后打开 examples/ 目录,重点看 plots、realtime、interactions 三个 demo
- 官方文档:qcustomplot.com/documentation,类继承图与成员索引非常完整
- 推荐路径:先跑通 plots 理解基础绘图 → 再研究 realtime 掌握实时性能 → 按需查阅各 plottable 的 setter API
十一、总结
QCustomPlot 是 Qt 生态中性价比极高的绘图库,学习曲线平缓、文档示例丰富。掌握它的关键在于理解数据组织方式(QVector + setData/addData)和重绘机制(replot 的触发时机与节流),这两点直接决定了你的应用能否流畅跑满 60 FPS。
建议从官方示例 plots 和 realtime 两个 demo 入手,一个讲基础绘图,一个讲实时性能,吃透它们就够应付 90% 的业务场景。
提供的Demo中包含对应的QCustomPlot文件。

9053

被折叠的 条评论
为什么被折叠?



