简介这份资源是面向Qt开发者的QCustomPlot绘图库核心源码包适合需要在桌面应用中实现高质量2D图表、数据可视化的初中级开发者。QCustomPlot以C类库形式提供线图、散点图、柱状图、饼图及组合图表绘制能力并支持实时数据更新、滚动缩放、鼠标交互与多轴系统等进阶特性。压缩包共2个文件包含1个cpp实现文件与1个h头文件整体约323KB直接加入工程即可完成集成无需额外依赖。目前已有583人学习下载。借助源码读者可深入理解QCPGraph数据绑定、QCPAxisRect轴矩形布局、replot重绘机制与缓存优化思路并参考官方示例快速搭建科学计算、工程监控或数据分析场景下的可视化界面是一份轻量且实用的Qt绘图学习素材。1. qcustomplot当你的上位机需要一张“能打”的实时曲线图如果你做过工控上位机、仪器采集软件或者嵌入式调试工具大概率遇到过这个场景下位机每 10ms 推一帧数据上来你需要在界面上画一条实时刷新的曲线同时还要支持缩放、拖拽、游标取值、导出图片。用 Qt 自带的 QChart 吧数据量一上去就卡成幻灯片自己用 QPainter 手搓吧坐标轴刻度、图例、抗锯齿全得自己算工期直接翻倍。qcustomplot 就是在这个夹缝里活得很好的一类库单头文件加单源文件扔进 Qt 工程就能编译底层直接用 QPainter 画没有额外的图形栈依赖在 Windows、Linux、嵌入式 Linux 上表现一致。它解决的不是“画一张漂亮报表”的问题而是“在资源受限的机器上把几万到几十万个点稳定地画出来并且让人能交互”的问题。适合谁适合那些用 Qt Widgets 做桌面端、需要自己控制刷新节奏、又不想引入 QtCharts 或第三方重型绘图引擎的开发者。下面我按“先跑通、再调参、再避坑”的顺序把这条链路拆开讲。2. 把 qcustomplot 塞进工程从下载到第一条曲线2.1 为什么是“单文件”而不是“装库”qcustomplot 的发布形态很朴素一个qcustomplot.h加一个qcustomplot.cpp。你不需要 qmake 的LIBS 也不需要 CMake 的find_package直接把这两个文件拖进项目目录在.pro里加一句HEADERS qcustomplot.h和SOURCES qcustomplot.cpp就能用。这种“源码级集成”的好处是编译期就能看到所有符号调试时能直接单步进库内部坏处是每次升级版本都要手动替换文件而且如果项目里多个模块都包含它得注意别重复编译。常见做法是把它单独放在一个third_party/qcustomplot目录下用include($$PWD/third_party/qcustomplot/qcustomplot.pri)的方式引入这样主工程文件干净升级也只动一个地方。2.2 最小可运行示例一个能拖拽缩放的曲线窗口下面这段代码是一个完整的MainWindow构造函数片段假设你已经把 qcustomplot 的源文件加进了工程。它创建了一个绘图控件加了一条曲线塞了 1000 个正弦点并开启了拖拽和缩放。#include mainwindow.h #include ui_mainwindow.h #include qcustomplot.h #include cmath MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent) , ui(new Ui::MainWindow) { ui-setupUi(this); // 1. 创建绘图控件设置为中心窗口 QCustomPlot *plot new QCustomPlot(this); setCentralWidget(plot); // 2. 添加一条曲线返回 QCPGraph 指针 QCPGraph *graph plot-addGraph(); graph-setName(通道 A); graph-setPen(QPen(Qt::blue, 1.5)); // 3. 准备数据1000 个点x 从 0 到 10y 为正弦 QVectordouble x(1000), y(1000); for (int i 0; i 1000; i) { x[i] i * 0.01; y[i] std::sin(x[i] * 3.14159 * 2); } graph-setData(x, y); // 4. 设置坐标轴标签 plot-xAxis-setLabel(时间 (s)); plot-yAxis-setLabel(幅值 (V)); // 5. 开启交互拖拽和缩放 plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); // 6. 自动缩放坐标轴以显示全部数据 plot-rescaleAxes(); plot-replot(); }逻辑说明addGraph()返回的QCPGraph是 qcustomplot 里最常用的图层类型适合画折线。setData()接受两个QVectordouble内部会拷贝一份数据所以如果你后续要高频更新不要每次都重新构造 vector而是用addData()追加或者直接操作graph-data()返回的容器。setInteractions()的参数是位或组合iRangeDrag允许鼠标左键拖拽平移iRangeZoom允许滚轮缩放。rescaleAxes()会根据当前所有 graph 的数据范围自动调整四个轴的范围但注意它只考虑已经 setData 的数据如果你后面动态追加点需要手动调xAxis-setRange()或者再次rescaleAxes()。参数说明setPen()的线宽在 1.0 到 2.0 之间比较合适太细在缩放时容易断线太粗会掩盖数据细节。QCPGraph::setData()的第二个参数是 y 值x 值如果不传默认用索引 0,1,2…但工业场景一般都要显式给时间戳。replot()是同步重绘会立即触发 paintEvent如果你在循环里频繁调用界面会卡死后面会讲怎么用replot(QCustomPlot::rpQueuedReplot)异步刷新。2.3 编译与运行时的两个硬性条件第一个条件是 Qt 版本。qcustomplot 2.x 系列要求 Qt 5.0 以上如果你用的是 Qt 6需要确认你拿到的 qcustomplot 版本已经适配了 Qt 6 的 API 变化主要是QPainter的一些枚举和QVector到QList的隐式转换。第二个条件是打印支持。qcustomplot 默认会编译打印相关的代码如果你在嵌入式环境没有打印服务需要在.pro里加DEFINES QCUSTOMPLOT_USE_LIBRARY或者直接注释掉qcustomplot.cpp里#ifdef QT_PRINTSUPPORT_LIB包裹的段落否则链接时会报QPrinter未定义。我一般会在.pro里显式写QT printsupport桌面端省事嵌入式端再按需裁剪。3. 实时曲线不卡顿数据追加、刷新策略与坐标轴滚动3.1 用 addData 代替 setData增量更新的正确姿势很多新手写实时曲线时习惯每次来新数据就重新构造两个 vector然后setData()。这在数据量小于 500 时看不出问题一旦超过 2000 点每次 setData 都会触发一次完整的数据拷贝和范围重算CPU 直接飙上去。正确的做法是用addData(x, y)追加单点或者用addData(const QVectordouble keys, const QVectordouble values)批量追加。下面是一个定时器驱动的例子每 20ms 追加一个点同时只保留最近 5000 个点。// 在头文件里声明 QCustomPlot *m_plot; QCPGraph *m_graph; QTimer *m_timer; double m_x 0; // 构造函数里初始化 m_plot new QCustomPlot(this); setCentralWidget(m_plot); m_graph m_plot-addGraph(); m_graph-setPen(QPen(Qt::green, 1.2)); m_plot-xAxis-setLabel(时间 (s)); m_plot-yAxis-setLabel(ADC 采样值); m_plot-yAxis-setRange(-1.5, 1.5); m_timer new QTimer(this); connect(m_timer, QTimer::timeout, this, []() { m_x 0.02; double y std::sin(m_x * 3.14159 * 2) 0.1 * (rand() % 100 - 50) / 50.0; // 追加单点 m_graph-addData(m_x, y); // 只保留最近 5000 个点防止内存无限增长 if (m_graph-dataCount() 5000) { m_graph-data()-removeBefore(m_x - 100.0); } // 让 x 轴跟随最新数据滚动窗口宽度 10 秒 m_plot-xAxis-setRange(m_x, 10.0, Qt::AlignRight); // 异步刷新避免阻塞定时器 m_plot-replot(QCustomPlot::rpQueuedReplot); }); m_timer-start(20);逻辑说明addData()是 O(1) 的追加操作内部用QCPDataContainer维护有序数据比setData()的 O(n) 拷贝快得多。data()-removeBefore()会删除指定 key 之前的所有点注意这个操作在数据量大时也有开销所以不要每来一个点就删一次可以每 100 个点删一次或者用data()-removeBefore(m_x - 100.0)这种按时间窗口删的方式。setRange(m_x, 10.0, Qt::AlignRight)的意思是x 轴范围从m_x到m_x 10.0并且右对齐这样曲线看起来是从右往左滚动的。rpQueuedReplot会把重绘请求放到事件队列里合并多次请求避免每 20ms 都强制同步绘制。参数说明定时器间隔 20ms 对应 50Hz 刷新人眼看起来已经连续。如果你的数据源是 1kHz不要每个点都 addData 再 replot而是攒 20 个点批量 addData再 replot 一次。removeBefore的参数是 key 值不是索引所以你的 x 值必须是单调递增的否则会删错。setRange的第三个参数Qt::AlignRight是 qcustomplot 对Qt::AlignmentFlag的复用只对 x 轴有效y 轴一般用setRange(lower, upper)固定范围。3.2 刷新策略同步、异步与手动重绘的取舍qcustomplot 的replot()有两个重载replot()和replot(QCustomPlot::RefreshPriority)。默认的replot()是同步的会立即调用draw()如果你在定时器回调里直接调它而绘制又比较慢定时器就会漂移。rpQueuedReplot是异步的它把重绘请求标记为“待处理”等 Qt 事件循环空闲时再统一绘制。还有一种做法是关掉自动重绘在批量更新完数据后手动调一次replot()。我一般会这样组合数据追加阶段用addData不触发重绘攒够一帧的量后调replot(rpQueuedReplot)。如果你发现界面还是卡可以检查是不是开了setAntialiasedElements里的QCP::aeAll抗锯齿在低端 GPU 上很吃性能可以只对曲线开抗锯齿坐标轴和网格关掉。3.3 坐标轴滚动与固定窗口的三种写法第一种是上面用的setRange(m_x, 10.0, Qt::AlignRight)适合“最新数据永远在最右边”的场景。第二种是固定窗口比如你只想看 0 到 10 秒的数据超过就丢弃那就用xAxis-setRange(0, 10)然后removeBefore(0)。第三种是“触发式滚动”比如只在数据超出当前范围时才滚动可以用xAxis-range().upper判断如果m_x upper就setRange(m_x - 10.0, m_x)。这三种写法没有优劣取决于你的交互需求。注意setRange会触发rangeChanged信号如果你在信号槽里又调了setRange小心死循环。4. 避坑与排查qcustomplot 最常见的五类翻车现场4.1 现象曲线画出来了但拖拽和缩放没反应原因setInteractions()没设或者设了但被其他控件抢了鼠标事件。qcustomplot 的交互依赖鼠标事件如果你的绘图控件被放在QScrollArea里滚轮事件会被滚动区域先吃掉。解决确认plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom)已经调用并且plot-axisRect()-setRangeDrag(Qt::Horizontal | Qt::Vertical)和setRangeZoom也设了方向。如果放在滚动区域里给绘图控件装事件过滤器拦截QEvent::Wheel并返回 true。4.2 现象数据量到 10 万点后界面直接卡死原因默认的QCPGraph是逐点绘制没有做任何抽稀。qcustomplot 提供了setAdaptiveSampling(true)它会根据当前像素宽度自动跳过一些点但只对QCPGraph有效而且默认是开的。如果你关了它或者用的是QCPCurve就会全量绘制。解决确认graph-setAdaptiveSampling(true)并且不要用QCPCurve画折线。如果还是卡考虑把数据分片只把当前视口内的数据传给 graph视口外的数据存在自己的环形缓冲里。4.3 现象replot 时闪屏或者曲线有残影原因qcustomplot 默认会先清空背景再绘制但如果你的setBackground()设了半透明颜色或者setNoAntialiasingOnDrag(true)没开拖拽时就会闪。解决给QCustomPlot设setBackground(QBrush(Qt::white))不透明背景拖拽时开setNoAntialiasingOnDrag(true)拖完再恢复抗锯齿。残影一般是setNotAntialiasedElements没设对或者你在paintEvent里自己画了东西没清空。4.4 现象编译报错 “QPrinter has not been declared”原因qcustomplot 的打印模块依赖QtPrintSupport但你的.pro里没加QT printsupport。解决在.pro里加上这一行或者在qcustomplot.cpp开头把#include QPrinter相关的代码用宏包起来嵌入式环境直接禁用打印功能。4.5 现象多线程里更新数据程序随机崩溃原因qcustomplot 不是线程安全的所有对 graph 和 axis 的操作都必须在 GUI 线程。如果你在工作线程里直接addData会和 GUI 线程的replot竞争。解决用QMetaObject::invokeMethod把数据更新投递到 GUI 线程或者用信号槽跨线程传递数据槽函数里再做addData。如果数据量很大不要每个点都发信号攒一批再发。5. 进阶技巧用 QCPItemTracer 做游标取值与数据导出5.1 游标跟随让鼠标指哪打哪qcustomplot 内置了QCPItemTracer可以把它绑到某条 graph 上鼠标移动时自动吸附到最近的数据点。下面这段代码在MainWindow里加一个 tracer并连接鼠标移动信号。// 在构造函数里追加 QCPItemTracer *tracer new QCPItemTracer(m_plot); tracer-setGraph(m_graph); tracer-setGraphKey(0); tracer-setInterpolating(false); // 不插值直接吸附到最近点 tracer-setStyle(QCPItemTracer::tsCircle); tracer-setPen(QPen(Qt::red, 1.5)); tracer-setBrush(Qt::red); tracer-setSize(6); // 连接鼠标移动信号 connect(m_plot, QCustomPlot::mouseMove, this, [](QMouseEvent *event) { double x m_plot-xAxis-pixelToCoord(event-pos().x()); tracer-setGraphKey(x); double y tracer-position-value(); // 在状态栏或标签上显示 x, y statusBar()-showMessage(QString(x%1, y%2).arg(x, 0, f, 3).arg(y, 0, f, 3)); m_plot-replot(QCustomPlot::rpQueuedReplot); });逻辑说明setGraphKey(x)会把 tracer 移动到 graph 上 key 最接近 x 的那个数据点。setInterpolating(false)表示不插值直接取原始点如果设为 true会在两个点之间线性插值适合数据点稀疏的场景。pixelToCoord把鼠标像素坐标转成数据坐标注意要传event-pos().x()不要传全局坐标。tracer-position-value()返回的是 y 值因为 tracer 的 position 是QCPItemPosition它的 type 是ptGraphvalue 就是 y。参数说明setSize(6)是 tracer 圆圈的半径像素值太小看不清太大挡数据。setPen和setBrush都设成红色是为了醒目但如果你有多个 tracer记得用不同颜色区分。mouseMove信号在鼠标离开绘图区域时不会触发所以状态栏会保留最后一次的值可以在leaveEvent里清空。5.2 导出图片与数据三种格式的适用场景qcustomplot 提供了savePng()、saveJpg()、saveBmp()和savePdf()。PNG 适合截图发报告JPG 体积小但曲线边缘会有压缩伪影PDF 是矢量格式放大不糊适合打印。导出时要注意replot()先调一次确保当前视口是最新的。下面是一个导出 PNG 的封装函数。void MainWindow::exportPlotAsPng(const QString fileName) { // 先重绘一次确保数据是最新的 m_plot-replot(); // 导出为 PNG宽度 1920高度 1080缩放因子 1.0 bool ok m_plot-savePng(fileName, 1920, 1080, 1.0, -1, 96); if (!ok) { qWarning() 导出 PNG 失败: fileName; } }逻辑说明savePng的参数依次是文件名、宽度、高度、缩放因子、质量-1 表示默认、分辨率DPI。宽度和高度如果传 0会用当前控件尺寸。缩放因子大于 1 会放大绘制适合高 DPI 屏幕。质量参数对 PNG 无效对 JPG 有效。如果导出失败检查文件路径是否有写权限以及文件名是否带了非法字符。5.3 一个我踩过的坑导出时坐标轴标签被截断有一次我导出的 PNG 里x 轴标签“时间 (s)”只显示了一半。排查后发现是savePng的宽度设得太小而坐标轴标签的字体又比较大布局没算进去。解决要么把宽度设大一点要么在导出前调m_plot-axisRect()-setAutoMargins(QCP::msAll)让 qcustomplot 自动计算边距。我现在的习惯是导出前先调一次m_plot-rescaleAxes()再调m_plot-replot()最后用savePng(fileName, 0, 0, 2.0)让库自己决定尺寸缩放因子给 2.0 保证清晰度。这个习惯帮我省了很多次“图糊了重导”的后悔药。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?