
做上位机、搞数据采集、写仪器界面的同学恐怕都躲不过“实时画曲线”这个需求。Qt 自带的绘图组件在早期版本里并不顺手Qwt 又老得让人提不起折腾的劲。我这两年在项目中用得最多的方案其实是 QCustomPlot——一个把核心代码压缩到 source/header 两个文件里的轻量级绘图库。标题里提到的“下载与处理”恰恰是大多数人真正入坑的地方版本不知道下哪个、文件加进工程后一堆编译报错、中文标签变成方框、数据一多画面就卡……等这些基础问题处理完后面画图才顺得起来。这篇文章把我自己从下载到集成、从静态曲线到实时刷新、再到底层排障的完整流程记录下来一条线讲清楚。适合刚接触这个库的初学者也给已经被某些细节卡住的朋友做参考。1. 先搞清楚选它到底值不值1.1 它是什么为什么这么多人选它QCustomPlot 是一个基于 Qt 的 C 绘图控件库自称“没有第三方依赖的轻量绘图方案”。它的核心形态非常特殊整个库的代码基本就是qcustomplot.h和qcustomplot.cpp两个文件。你用的时候不需要编译动态库、不需要配置一堆链接路径把这两个文件丢进工程直接就能编译。这个设计在 Qt 生态里非常讨喜。很多开源库动辄给你一份 CMakeLists、生成几个 .so/.lib版本一换就得重新折腾依赖。而 QCustomPlot 的两个文件让“零成本集成”成为可能哪怕你只想要一部分功能也是直接打开源码去改不用写什么插件接口。从功能覆盖面上看折线图、散点图、柱状图、直方图、极坐标图、颜色图、色标、向量场等常见可视化类型都有现成实现。最核心的实时刷新场景它通过内部对数据容器的管理把性能控制得很好加上去抗锯齿、降采样等优化手段轻轻松松撑起几千到几万个数据点的动态刷新。它的使用逻辑也很直白一个QCustomPlot控件就是一张画布里面有“轴”、“图层”、“图例”、“元素”四个大类的概念。画曲线就addGraph()画柱子就new QCPBars画热力图就new QCPColorMap。每个绘图对象绑定到坐标轴上数据更新后调一次replot()图就重新画出来了。这种命令式模型读完 demo 基本就能上手。1.2 跟 Qwt、Qt Charts 放在一起怎么选不少人在选型时会在 Qwt、Qt Charts 和 QCustomPlot 之间纠结。我自己的体会是这三个方案各有个性关键看你是“演示型”还是“工程型”。Qwt 是最老牌的开源绘图库功能很全面早期资料也多。但它的代码风格偏古典文档多年没做现代化整理编译配置相对繁琐。如果你维护的是十几年的老项目且原本就基于 Qwt没必要迁移新项目再从头学它性价比偏低。Qt Charts 是 Qt 官方提供的图表模块外观现代QML 支持也很好。但它有几个硬伤其一授权模式对商业应用不友好闭源分发需要购买商业许可其二它依赖额外的 Qt Charts 模块在一些裁剪版 Qt、嵌入式板卡上根本没有这个模块其三定制细节时你受限于官方接口改起来不如源码在手来得痛快。QCustomPlot 的优势恰好对上“工程型”需求MIT 许可协议对商业项目很宽松依赖的是 Qt 最基础的 Widgets 和 PrintSupport 模块嵌入环境里几乎不会缺源码完全开放任何不符合需求的细节都可以直接改官方还提供了几十个完整 demo从最简单的折线图到复杂的热力图、拖拽标注开箱即用。当然它也有边界比如 3D 绘图、GIS 图层这类需求它并不擅长需要另找方案。我的选型建议很简单如果只是给设备写个小上位机、实时画几个传感器曲线或者需要完全掌控绘制细节QCustomPlot 是当下最优解如果项目本身要上 QML 动态交互Qt Charts 值得考虑如果只是要用最古老的稳定方案维护老系统继续 Qwt 也无可厚非。2. 获取与工程集成下载那点坑最值得细看2.1 版本怎么选文件怎么拿很多人在“下载”这一步就犹豫了打开官方仓库发现有 2.x 系列和 3.x 系列不知道选哪个。这个选择其实直接影响后面所有处理流程。当前官方仓库的主分支已经是 3.x 系列它重构了内部构建系统对 Qt6 和 CMake 的适配更完善也加入了一些新特性。但如果你翻看网上大量教程、博客、公司内部代码你会发现绝大多数参考资料还停留在 2.x 系列的写法。我个人的建议是新手初次学习、或者要在短期内交付项目的优先选用 2.1.1 稳定版。它的接口成熟、资料多踩坑时随便都能搜到解决方案。等把内部机制摸熟之后再评估要不要迁移到 3.x。下载方式以官方渠道为准。去官方仓库的 Releases 页面直接下载 zip 压缩包或 tar.gz 源码包解压后重点拿这几个东西qcustomplot.h和qcustomplot.cpp核心源码集成时用。examples/目录官方示例工程这是真正的宝藏里面有各种功能的最小可运行 demo。doc/目录离线版文档如果不想在线查看这个就够。至于某些站点提供的“一键集成包”“修改版”我建议尽量少碰。这种第三方魔改版本加了些小功能的同时往往是问题源头。用官方原版至少出了问题你能明确责任边界。2.2 qmake 工程里最快接入拿到文件后qmake 工程集成是最传统也是最快的方式。直接把两个源码文件加入.proQT core gui widgets printsupport TARGET plotdemo TEMPLATE app SOURCES \ main.cpp \ qcustomplot.cpp HEADERS \ qcustomplot.h这里有个关键点必须强调QT printsupport几乎不能省略。QCustomPlot 的打印和部分绘图底层依赖QPrinter相关模块而该模块就在 PrintSupport 里。不加这个后面连接时会出现找不到符号的报错。如果你用的是 Qt 5.15 以下的版本还需要注意widgets这个模块因为在 Qt 6 里QWidget系列被合并进了QtWidgets但.pro写法基本一致。完成之后在你的代码里#include qcustomplot.h然后直接在 UI 代码里创建一个QCustomPlot对象即可。这种复制源码进工程的做法的好处是修改和调试非常直观。比如你想在内部绘图函数里加一点自定义绘制逻辑或者断点跟进某个数据坐标的转换过程直接跳进qcustomplot.cpp就能看不需要在库工程和应用工程之间反复切。2.3 CMake 工程里的标准姿势现在不少跨平台项目已经全面转向 CMake。QCustomPlot 也考虑到了这种情况直接把它当普通源文件编译进来就行不需要额外find_package做什么。一个最简的 CMakeLists 是这样cmake_minimum_required(VERSION 3.16) project(plotdemo) set(CMAKE_CXX_STANDARD 11) set(CMAKE_AUTOMOC ON) find_package(Qt5 COMPONENTS Widgets PrintSupport REQUIRED) add_executable(plotdemo main.cpp qcustomplot.cpp qcustomplot.h ) target_link_libraries(plotdemo PRIVATE Qt5::Widgets Qt5::PrintSupport)注意我用的是Qt5前缀。如果你用的是 Qt6包名改成Qt6、链接库改成Qt6::Widgets Qt6::PrintSupport即可。如果你拿到的 QCustomPlot 是 3.x 版本通常它自身也提供了 CMake 配置理论上可以通过find_package(QCustomPlot)找到但我实际经验是在简单项目里直接编译两个源文件反而更省事少绕一层。CMake 集成的另一个注意事项是CMAKE_AUTOMOC必须开启。QCustomPlot 内部有大量继承QObject的类需要 Qt 的元对象系统处理信号槽。不开 AUTOMOC编译阶段会出现大量和 moc 相关的报错。2.4 集成时最容易犯的错集成阶段的问题九成都能归到下面几类。第一类是“文件没有真正进入编译”。看起来.pro里写了qcustomplot.cpp但工程里实际引用的却是另一个目录下的同名文件。这种问题在 Qt Creator 里特别隐蔽因为它展示的是“虚拟目录”真实文件路径和展示目录可能不一致。排查方法就是右键源文件、查看实际路径确认与工程文件目录匹配。第二类是“多版本 Qt 环境混用”。系统里装了 Qt5 和 Qt6或者装了多个编译器CMake 自动选了其中一个。结果编译时头文件来自一套版本链接时链接到另一套版本报的错五花八门。解决方法是显式指定 Qt 路径比如在 CMake 里写set(CMAKE_PREFIX_PATH your/qt/path)。第三类是“导入的代码是 3.x查的资料却是 2.x”。这种情况是代码编译不过的常见原因因为 3.x 对类结构做了调整部分方法签名有差异。如果你学习资料大多是 2.x先用 2.x 的源码避免自我折腾。3. 从静态曲线到实时刷新核心功能走一遍3.1 先画一条最简单的曲线画图之前先理解 QCustomPlot 的绘图流程创建画布控件、添加绘图对象、设置数据、调整坐标轴、调用replot()刷新画面。在代码里一个最朴素的折线图大概是这样// 假设 this 是 QWidget 或其子类 QCustomPlot *customPlot new QCustomPlot(this); // 添加一条曲线 customPlot-addGraph(); customPlot-graph(0)-setPen(QPen(QColor(30, 120, 255))); customPlot-graph(0)-setName(温度); // 构造数据 QVectordouble x(100), y(100); for (int i 0; i 100; i) { x[i] i * 0.1; y[i] qSin(x[i]); // 示例数据 } // 设置数据 customPlot-graph(0)-setData(x, y); // 坐标轴标签 customPlot-xAxis-setLabel(time (s)); customPlot-yAxis-setLabel(value); // 显示图例 customPlot-legend-setVisible(true); // 让坐标轴自动适配数据范围 customPlot-rescaleAxes(); // 关键一步刷新画面 customPlot-replot();这里必须多说一句很多新手把数据设置进去之后发现图上没东西十有八九是忘了replot()。QCustomPlot 默认不会在你改完数据后自动刷新画面——它不是数据绑定响应式模型而是命令式绘图库。所有的数据、坐标轴、图例、图层改动都只是修改了内部状态只有调用了replot()所有这些状态才会真正绘制到屏幕上。setData和addData的区别也值得注意。setData是整体替换数据适合刚开始或者需要重设整条曲线的场景addData是追加一个或一组数据点适合持续追加的实时场景。如果每帧都调用setData把整个向量复制一遍数据点一多就会白白浪费性能。3.2 实时数据滚动如何处理才不卡实时曲线是 QCustomPlot 最常见的应用场景比如串口数据采集、传感器波形监控等。核心思路是用一个定时器周期性获取新数据把新点追加到 graph 里同时把坐标轴范围向前滚动。一个比较成熟的实时刷新代码段是这样void DeviceWindow::setupRealtimePlot() { plot new QCustomPlot(this); plot-addGraph(); plot-graph(0)-setPen(QPen(QColor(0, 140, 60))); plot-xAxis-setRange(0, 20); plot-yAxis-setRange(-1.5, 1.5); plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, DeviceWindow::realtimeDataSlot); timer-start(30); // 约 33 帧/秒 } void DeviceWindow::realtimeDataSlot() { static double lastKey 0; double key lastKey 0.03; // 假设每帧间隔 0.03 秒 double value qSin(key); // 控制数据量丢弃窗口左侧旧数据 if (plot-graph(0)-dataCount() 2000) { plot-graph(0)-data().removeBefore(key - 20); } // 追加一个点 plot-graph(0)-addData(key, value); // 让 x 轴右端对齐当前时刻窗口跨度固定 20 个单位 plot-xAxis-setRange(key, 20, Qt::AlignRight); // 刷新 plot-replot(); }这个写法里有几个细节非常关键。第一数据量一定要限长否则曲线跑了十分钟内存里积累了十万个点再强的性能也要被拖垮。removeBefore这种方法可以一次性清掉某个 Key 之前的所有数据效率远高于循环删除。第二坐标轴范围使用Qt::AlignRight这种“锚定方式”来做滚动更新而不是每次去计算一个全新的 Range这样视觉效果更稳定也不会出现左右抖动。第三刷新频率不需要盲目追求过高的定时器频率。30 毫秒刷新一次人眼就已经觉得很流畅了如果定时器设到 1 毫秒反而会耗尽 CPU实际渲染跟不上也是白搭。另外一点是关于线程。QCustomPlot 对象应该始终在主线程GUI 线程里操作。在真正项目里采集数据的线程往往是工作线程不能直接把addData放到工作线程里调用。我常用的做法是工作线程把数据打包成结构体或QVector通过信号发到主线程主线程的槽函数再更新 QCustomPlot。这也意味着我上面那段代码里的realtimeDataSlot是在 GUI 线程里执行的。3.3 时间轴、柱状图和热力图几个高频扩展除了普通数值轴时间轴的用法几乎每天都会遇到。QCustomPlot 提供了QCPAxisTickerDateTime来处理时间格式的刻度。QSharedPointerQCPAxisTickerDateTime dateTicker(new QCPAxisTickerDateTime); dateTicker-setDateTimeFormat(HH:mm:ss); plot-xAxis-setTicker(dateTicker); // 假设 key 是 Unix 时间戳秒 plot-xAxis-setRange(QDateTime::currentMSecsSinceEpoch() / 1000.0 - 60, QDateTime::currentMSecsSinceEpoch() / 1000.0);这里最容易犯的错是时间戳单位不一致。QCustomPlot 的时间 ticker 默认处理的是“秒”而 Qt 里QDateTime::currentMSecsSinceEpoch()返回的是“毫秒”。如果你直接把毫秒数塞进去看到坐标轴横跨 1970 年就知道单位没对齐。统一先除以 1000 转成秒再处理问题就没了。柱状图和折线图的写法略有不同。折线图是通过画布对象的addGraph()创建的柱状图则需要new一个QCPBars对象并手动指定它挂在哪个坐标轴上QCPBars *bars new QCPBars(plot-xAxis, plot-yAxis); QVectordouble keyData, valueData; // ... 填充数据 ... bars-setData(keyData, valueData); bars-setBrush(QBrush(QColor(120, 180, 240, 120)));注意setBrush是柱子的填充色setPen是柱子边框颜色。柱状图如果要和折线图混画在同一个坐标轴上坐标轴数据范围要考虑两者最大最小值不然柱子容易顶出可视范围。热力图则推荐QCPColorMap。这个控件适合显示二维数组数据比如光谱数据、热分布、时频图。它的核心概念是网格先用setSize指定网格行列数再用setRange指定两个坐标轴的范围之后通过setCell往每个网格里填值最后用setGradient选择色带样式。整个操作思路跟图像像素差不多理解成本不高。4. 交互、导出与界面细节4.1 缩放、平移和鼠标取值一个只展示、不交互的图表通常不够用。QCustomPlot 内置了交互机制通过一行代码就能打开拖拽平移和滚轮缩放plot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom);iRangeDrag允许用户按住鼠标左键拖动坐标轴范围iRangeZoom允许滚轮缩放。如果你不希望坐标系上下左右乱晃可以指定只拖动 X 轴或 Y 轴。比如下面的写法只允许横向拖拽和横向缩放plot-axisRect()-setRangeDrag(Qt::Horizontal); plot-axisRect()-setRangeZoom(Qt::Horizontal);交互和实时滚动往往会产生冲突。如果实时模式下用户还想拖拽缩放一旦新数据到来setRange又会强制把坐标轴拉回去用户会觉得“图表不受控制”。我的处理思路是给界面加一个“暂停滚动”开关用户拖拽时自动暂停自动滚动退出交互或者点击“跟随新数据”后再恢复。鼠标取值是另一个高频需求。通过QCustomPlot的mouseMove信号把鼠标像素坐标反算成数据坐标connect(plot, QCustomPlot::mouseMove, this, [](QMouseEvent *event) { double x plot-xAxis-pixelToCoord(event-pos().x()); double y plot-yAxis-pixelToCoord(event-pos().y()); // 更新界面上的坐标显示 });需要留意的是pixelToCoord返回的是在当前坐标系下的数据坐标如果用户进行了大幅缩放或拖拽这个值会随之变化。如果你的场景需要的是“离某个数据点最近的那个值”而不是纯粹的散点坐标那就得自己遍历数据容器找最近点了。数据量小可以直接遍历数据量大就要考虑空间索引或二分查找不过大多数上位机场景数据点几千个线性遍历完全没问题。4.2 高清导出和剪贴板存图把图表导出成图片几乎每个项目都会用到。QCustomPlot 提供了非常方便的savePng、saveJpg、saveBmp等方法也可以先toPixmap再处理。// 保存为 png指定 1920x1080dpi 参数控制清晰度 plot-savePng(report.png, 1920, 1080, 2.0, 100);这里最值得说的是 dpi 参数。如果直接用默认的1.0导出的图在屏幕上看着没问题但放进印刷文档或者高分屏展示时就会糊。我通常把 dpi 设成2.0或者更高。但这也会带来副作用如果图表里有密集的曲线高 dpi 下线条反而会显得非常细。所以发布图片前建议在 100% 缩放比例下肉眼确认过再导出。导出到剪贴板也不难QApplication::clipboard()-setImage(plot-toPixmap(800, 600));这样用户按一个按钮图表就能直接粘贴到 Word、微信对话或画图工具里体验比保存文件再上传自然得多。我在设备上位机里就加过这种逻辑配合一个“复制截图”按钮运维人员排查问题时方便不少。4.3 中文字体与高分屏适配坐标轴标签、图例出现中文时最常见的问题是“中文变成方框”。QCustomPlot 底层使用的是QPainter绘制文字默认字体族往往不支持中文。解决办法就是手动给所有可能显示中文的文本对象设置中文字体。QFont font(Microsoft YaHei); font.setPointSize(9); plot-xAxis-setLabelFont(font); plot-yAxis-setLabelFont(font); plot-xAxis-setTickLabelFont(font); plot-yAxis-setTickLabelFont(font); plot-legend-setFont(font);这里容易漏掉的是刻度标签字体。如果你只设置了轴标签字体坐标轴刻度上的数字和日期可能还是旧字体。如果刻度数字也显示正常而图例中文乱掉那就是图例字体没设置。在不同操作系统上中文字体的写法也有差异Windows 使用“Microsoft YaHei”Linux 常见“WenQuanYi Micro Hei”macOS 则用“PingFang SC”。跨平台项目可以通过QFontDatabase检测系统可用字体再决定用什么。高分屏适配也常被忽视。如果应用没有开启高 DPI 缩放QCustomPlot 的图形和文字在 Windows 高分屏上会显得模糊。自 Qt 5.6 起在main函数最前面加上一句QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);并把main里的QApplication放到这行之后整体清晰度就会明显提升。注意这个设置必须在任何窗口创建之前调用否则不会生效。5. 常见问题与排查技巧实录5.1 编译链接问题速查表整理了一份我在实际项目里遇到过的编译链接问题直接照着对照排查现象可能原因解决办法链接报错 undefined reference缺少 PrintSupport 模块.pro中加QT printsupport编译报错找不到 qcustomplot.h头文件路径没包含把文件放在工程目录或增加INCLUDEPATHmoc 相关报错没有开启 AUTOMOC / 文件路径不对CMake 打开CMAKE_AUTOMOC代码中使用的方法不存在版本是 3.x资料是 2.x统一版本或查阅官方迁移文档界面显示空白忘记replot()数据修改后调用replot()中文显示方框字体 / 编码问题设置中文字体检查源码编码为 UTF-8这里还想单独强调一下“undefined reference”这个报错的高发场景。特别是工程使用 CMake 时链接库目标里很容易漏掉Qt5::PrintSupport。QCustomPlot 对打印的支持是集成在核心绘图流程里的很多导出、剪贴板功能都间接引用了它。所以最省心的办法就是无条件把它加进链接列表。5.2 数据多、刷新快界面卡顿怎么压实时数据项目跑到后来最普遍的抱怨是“曲线拖不动、界面卡”。一帧一帧看单次绘制几十万个点时CPU 占用率会接近 100%。我总结下来性能优化按优先级应该这么做第一减少绘制点数。不要把所有历史数据都画出来用removeBefore定期清理窗口外的数据点只保留可视范围和少量缓冲区内的点。如果点是均匀采样的还可以对超出窗口的部分做轻量降采样。第二关闭不必要的抗锯齿。抗锯齿对图形质量提升明显但在实时刷新场景下开销巨大。可以针对单个绘图对象关闭plot-graph(0)-setAntialiased(false);如果整张图都不需要抗锯齿还可以全局统一关闭。这个操作常常能换来 30% 以上的性能提升。第三合并重绘。如果你在一个刷新周期里既要加新数据、又要改坐标轴、还有可能移动标注不要在每次操作后都调replot()而是全部状态改完后再统一replot()一次。多个操作合并成一次重绘比多次小重绘要省非常多。第四控制定时器频率。每 30ms 刷新一帧已经足够顺滑调成 10ms 只会白白增加 CPU 负担画面上根本看不出区别。如果还想要更精细的视觉体验可以用QElapsedTimer控制实际渲染间隔而不是死板地依赖定时器 tick。5.3 从 2.x 升到 3.x实际上要注意什么如果你已经熟练使用 2.x看到 3.x 的更新时心里可能会痒。我做过几次迁移总体的感受是基础绘图的 API 变化不大但“周边”变了。3.x 重构了构建系统对 Qt6 和 CMake 的支持比 2.x 好很多这是最大的卖点。如果你的项目已经全面转向 Qt6那直接用 3.x 是明智的。但在代码层面3.x 对内部类结构、部分枚举值和函数签名做了调整直接拿旧代码编译大概率会报错。官方提供了从 2.x 迁移到 3.x 的指南里面有逐项变更说明照着改并不难。但我个人的建议是不要把升级当成一种时髦。如果你的核心需求就是画几条实时曲线、导出图片、缩放拖拽2.1.1 稳定版完全够用而且互联网上多数问题解答都是基于 2.x 的。只有当你确实需要 Qt6 支持、需要官方 CMake 构建方式、或者想用 3.x 里新加的某些功能时才值得动迁移的念头。数据容器接口类的差异也需要注意。2.x 里大量使用QCPGraphDataContainer、QCPErrorDataContainer等3.x 对容器做了归类整理。对于普通用户来说直接使用graph-data()和graph-addData()不会有感知差异但如果你之前写过直接遍历数据容器的底层代码迁移时就需要逐行确认容器接口是否一致。我个人在实际操作中的体会是QCustomPlot 最值钱的地方不只是画图本身而是它把底层绘制逻辑摊开在源码里。遇到任何奇怪现象顺着qcustomplot.cpp从头跟一遍基本都能定位到根因。我每接一个需要绘图的工程都会把官方examples/目录复制一份改参数比看文档快得多。与其到处找集成包、魔改版不如就抓稳官方源码这两个文件你在项目里才能做到真正的心里有底。