QCustomPlot 实战指南:从入门到性能优化

一、为什么选择 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);

六、性能优化实战

实时绘制到几万、几十万数据点时,必须注意以下几点:

  1. 开启 OpenGL 加速(QCustomPlot 2.x 已内置,无需额外库):
customPlot->setOpenGl(true);

注意:OpenGL 模式下部分功能(如 QCPErrorBars)受限,需要取舍。

  1. 数据降采样:数据点超过屏幕像素数时,肉眼根本分辨不出差异。可对历史数据做抽稀,只保留屏幕宽度 × 3 左右的点数。QCPGraph::setAdaptiveSampling(true) 默认开启,绘制时自动降采样。

  2. 关闭不必要的重绘

customPlot->setNoAntialiasingOnDrag(true);  // 拖拽时关闭抗锯齿
customPlot->setNotAntialiasedElements(QCP::aeAll);  // 全关抗锯齿,性能翻倍
  1. 批量追加:addData 支持传入 QVector,一次性追加一批数据,比逐点调用快得多。

  2. 避免频繁 setRange + replot:改用 QCPAxis::setRange 后统一一次 replot,减少布局计算。

  3. 数据存储:全部使用 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文件。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翎_鸢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值