
几年前我做一个数据采集上位机界面需要实时刷新多条曲线、支持缩放平移、还要能把图表导出成图片。当时在QCustomPlot、Qwt和Qt自带的Charts之间纠结了很久最后整套方案都压在QCustomPlot上。如果你也在用Qt做绘图相关的功能或者正被曲线卡顿坐标轴控制不好使中文乱码这类问题折磨这篇东西应该能帮你少走不少弯路。我尽量按实际项目里会遇到的处理顺序来讲从下载源码、选版本、集成到项目再到高频功能和排坑都是可以直接照着操作的内容。1. 选型对比为什么我最后留了QCustomPlot先说结论QCustomPlot最适合希望绘图逻辑完全可控、又不想重新造轮子的Qt桌面项目。它不是一个重量级的数据可视化框架定位就是轻量、高自由度、跟Qt深度绑定的绘图控件尤其擅长实时曲线、散点图、柱状图、颜色图这类偏工程仪表的用途。当时我把候选方案挨个试了一遍说下直观感受方案上手成本实时性能可定制程度自带交互维护状态QCustomPlot低复制两个文件就能用高几十万点也能撑住极高画布内容几乎都能碰缩放、平移、选择都内置开源、持续更新Qwt中需要编译库高老牌稳定高但风格偏旧有但接口偏老更新缓慢Qt Charts低官方模块中数据量大时明显吃力中定制得绕道有基础够用官方维护但商用授权要留意我选QCustomPlot还有一个很实际的理由它是完整源码提供的qcustomplot.h和qcustomplot.cpp两个文件就覆盖了全部功能出了问题可以直接断点进源码里查这对调试来说太重要了。Qwt虽然也不错但它的设计思路还停留在比较早的Qt风格上用起来总感觉有点隔靴搔痒。Qt Charts倒是省事可一旦要在draw事件里做点自定义绘制或者处理几十万点数据的连续刷新性能和自由度都会受限制。另外QCustomPlot对Qt版本和编译器要求很宽松官方默认支持比较新的Qt6环境往下兼容Qt5也很顺。我自己的项目用的就是Qt6集成过程没遇到什么环境层面的麻烦。如果你打算做一个长期维护的桌面工具选它的另一个好处是迁移成本低——只要把绘图相关代码封装好后续换Qt版本基本不用大动。2. 下载源码与版本选择必须提前想清楚的几个问题2.1 从哪里拿源码QCustomPlot官网主页就有压缩包里面包含完整的qcustomplot.h、qcustomplot.cpp、帮助文档和大量官方示例。我建议你优先拿源码包而不是只去找编译好的库文件原因有两个一是你可能需要打开源码看内部实现排查一些查不到文档的细节行为二是QCustomPlot的发布页一直保留历史版本万一你项目里的Qt版本比较特殊可以回头选一个合适版本。下载后你会得到一个解压目录结构大致是这样的qcustomplot/ ├── qcustomplot.h ├── qcustomplot.cpp ├── documentation/ ├── examples/2.2 版本适配思路QCustomPlot在不同时期对C标准的要求不太一样新版本基本要求C11以上。如果你的工程还开着老旧的编译标准集成后会出现一些莫名其妙的模板报错。我的判断标准很简单Qt6 新项目直接用最新版Qt5.12到5.15的老项目优先选发布页里对Qt5兼容性标注清晰的版本如果遇到编译不过第一件事先确认是否用了C11以上标准版本选择还有个容易忽略的地方QCustomPlot官方包里的Demo很多只注册了基础的Qt模块而实际接入项目时需要根据你的用法再补模块。最典型的是如果用到QPrinter相关的导出PDF功能工程文件里一定要加上printsupport模块不然编译会直接报找不到头文件。2.3 关于文档和示例的使用习惯官方示例是很好的学习入口但我的建议是不要直接复制示例代码到生产项目里。因为它们为了演示各种能力往往一个文件里堆了很多场景耦合度高。更好的做法是先跑一遍示例找到和自己需求最接近的画面效果再回到代码里提炼关键API自己重新组织一遍。这样你后面维护时会轻松很多。3. 把QCustomPlot塞进Qt项目两种集成方式详解3.1 方式一源码直接加入工程最省事、也最适合中小型项目的方式就是把qcustomplot.h和qcustomplot.cpp复制到你项目的源码目录里然后在.pro文件qmake工程里加上QT widgets printsupport SOURCES qcustomplot.cpp HEADERS qcustomplot.h如果是CMake工程对应写成set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) add_executable(MyApp main.cpp qcustomplot.cpp)注意printsupport这个模块它和QCustomPlot的图片/PDF导出功能相关。如果不加你用savePng或者savePdf这类函数时链接阶段大概率会报错。这种方式优点很明显代码在你眼皮底下调试方便打包部署也简单。缺点是如果你的多个项目都要用每份代码都要各自维护一份副本后续想升级版本得多处替换。3.2 方式二编译成静态库复用我后来因为要同时维护几个上位机工具就把QCustomPlot编成了一个静态库统一管理。做法是先建一个空的Qt库工程把qcustomplot.cpp和qcustomplot.h加进去编译后得到静态库文件和头文件其他工程引用时链接它就行。这种做法适合几十个项目都是同一套Qt环境的情况而且编译这一步还会自动验证你的Qt环境和新版本QCustomPlot是否兼容有坑先暴露在编译期而不是等到运行时。不管用哪种方式都要确认编译器优化级别。发行版本建议开启-O2或/O2因为QCustomPlot在绘制曲线时某些算法的向量计算对优化比较敏感不开优化的话性能差距肉眼可见。3.3 第一个能跑的Demo画一条正弦曲线集成了之后先画一条最简单的曲线验证环境通不通#include qcustomplot.h QCustomPlot *customPlot new QCustomPlot(this); // 生成X轴数据0到100之间的正弦波 QVectordouble xs(101), ys(101); for (int i 0; i 101; i) { xs[i] i * 0.1; ys[i] sin(xs[i]); } // 创建一条曲线并设置数据 customPlot-addGraph(); customPlot-graph(0)-setData(xs, ys); customPlot-graph(0)-setPen(QPen(QColor(0, 120, 255))); // 给坐标轴起个名字 customPlot-xAxis-setLabel(时间 (s)); customPlot-yAxis-setLabel(幅值 (V)); // 自动调整坐标范围 customPlot-rescaleAxes(); customPlot-replot(); setCentralWidget(customPlot);这段代码里addGraph()是核心入口它创建了一个用于绘制折线的图层对象之后所有操作都围绕这个graph展开。rescaleAxes()会让坐标轴根据当前数据自动缩放非常省事。replot()则是真正触发重绘的时机注意它和QWidget的update()逻辑不同QCustomPlot把重绘控制完全交给你所以别漏调它。跑通这个Demo之后我的心就定了一大半。集成这一步其实不难最容易翻车的反而是工程文件里模块没写全、或者C标准没设置对这类低级问题。遇到编译报错先别急着怀疑库本身把编译输出里第一个error信息贴出来逐行看通常三五分钟就能定位。4. 从静态图到动态刷新最常用的三个高频场景4.1 实时波形刷新做采集类上位机最典型的场景就是串口或网络不断送来数据点界面要像走马灯一样滚动显示。QCustomPlot里实现实时刷新没有玄学无非就是一个QTimer定时去追加数据然后调replot()。void MainWindow::setupTimer() { QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, MainWindow::updatePlot); timer-start(30); // 约33帧每秒 } void MainWindow::updatePlot() { double t QDateTime::currentMSecsSinceEpoch() / 1000.0; double v amplitude * sin(2 * M_PI * frequency * t); customPlot-graph(0)-addData(t, v); customPlot-graph(0)-rescaleValueAxis(true); customPlot-xAxis-setRange(t - 10, t); // 只显示最近10秒 customPlot-replot(); }这里有个关键点rescaleValueAxis(true)表示只根据数据动态调整Y轴X轴用手动方式固定成一个滑动窗口。这种只滚Y、X固定窗口的组合是实时曲线最稳的用法。如果你让X轴也一直用rescaleAxes()自动缩放曲线框就会不停缩放跳动视觉体验很差。刷新间隔方面我测试下来30ms到50ms是一个甜点区间。刷新太频繁不仅CPU吃不消界面反复重绘反而可能闪烁刷新太慢又看不到实时性。如果单个界面同时显示4到6条曲线30ms间隔在主流台式机上CPU占用大约在8%到15%之间属于可以接受的范围。4.2 海量数据的静态可视化不是所有数据都要实时刷新。有时候你会一次性加载几十万个点做分析查看比如读取一个CSV文件把一整天的数据画出来。这种情况下QCustomPlot的setData()比逐点addData()快得多因为后者每次都要触发数据结构内部的调整。QVectordouble xs(largeData.size()); QVectordouble ys(largeData.size()); // ...填充xs, ys... customPlot-graph(0)-setData(xs, ys); customPlot-graph(0)-rescaleAxes(); // 关键自适应降采样可以把几十万点压成屏幕需要的精度 customPlot-setAdaptiveSampling(true); customPlot-replot();setAdaptiveSampling(true)这个开关值得专门说一下。它在绘制时会把超出屏幕分辨率的点合并成每一列像素的数据特征从而在大幅缩放、平移时也能保持流畅。直观感受就是之前平移时卡得像幻灯片开了这个选项以后体感顺畅非常多。省心的是它是全局开关设置一次之后所有graph的绘制都默认受益。海量数据场景还有一个容易踩的坑先清空原来的布局再重新布局。如果你在同一个graph上反复调用setData新数据长度和旧数据不一致时内部容器会重新分配内存频繁申请释放会导致明显的抖动。实际项目里如果可以复用已经分配好的QVector就先填充vector再传入setData性能会更稳定。4.3 多曲线和自定义图例多路曲线在工程仪表里实在太常见了比如温度、湿度、转速三路数据叠在一起看趋势。QCustomPlot的多曲线实现非常直白customPlot-addGraph(); customPlot-graph(0)-setPen(QPen(QColor(230, 80, 80))); customPlot-addGraph(); customPlot-graph(1)-setPen(QPen(QColor(80, 140, 240))); customPlot-addGraph(); customPlot-graph(2)-setPen(QPen(QColor(90, 190, 90))); // 第0路是温度第1路是湿度第2路是转速 customPlot-graph(0)-setName(温度); customPlot-graph(1)-setName(湿度); customPlot-graph(2)-setName(转速);图例默认会在右上角自动显示前提是你给graph设置过setName()。如果觉得位置挡住曲线了可以手动拖动或用代码调整customPlot-legend-setBrush(QBrush(QColor(255, 255, 255, 230))); customPlot-legend-setBorderPen(QPen(QColor(180, 180, 180))); customPlot-axisRect()-insetLayout()-setInsetAlignment(0, Qt::AlignRight | Qt::AlignTop);要注意的是多曲线场景下每条曲线的数据类型最好保持一致否则Y轴范围会互相拉扯——比如转速是千级数值温度是几十级数值放在同一个Y轴里温度曲线的变化就几乎看不见了。这种情况要么给每条曲线单独分配一个Y轴customPlot-yAxis2右侧轴要么把数值先做归一化处理。我习惯根据业务来如果只是快速看一眼趋势就归一化如果是要严谨比对量值就分轴。5. 我踩过的坑从CPU飙高到中文乱码到轴刻度错乱5.1 CPU飙高replot被无脑调用刚集成QCustomPlot那阵子我把刷新逻辑写成了一个高频定时器每10ms就调用一次replot结果一运行CPU直接飙到40%以上风扇狂响。后来查了下源码才发现replot并不是简单的重绘它内部会做数据空间索引的更新、画布绘制、内边框计算等等如果每次都实打实重算开销是相当大的。解决思路有三个层次降低刷新频率到30ms或更慢用customPlot-setNoAntialiasingOnDrag(true)在拖拽交互时关闭抗锯齿减少绘制成本如果数据更新频率高于绘制频率就在定时器里先暂存数据攒够一个刷新周期再统一replot避免每来一个点就重画一次。这几个手段叠加上去之后CPU占用从40%降到了10%以内。如果你的数据源特别凶猛比如每秒几千个点还可以考虑开一个后台线程只做数据接收和缓存界面线程只负责按节奏取数据段并repaint这样把两件事解耦界面就不会被数据风暴拖死。5.2 中文乱码坐标系和标签全是问号QCustomPlot对中文的支持本身没有问题乱码基本都是编码转换或者字体缺失造成的。最容易翻车的场景是代码文件保存格式不是UTF-8Qt6以上默认要求UTF-8源码如果你还在用本地GBK编码中文字符串进入QCustomPlot后就会变成问号。我的处理习惯是代码文件一律用UTF-8在main函数里设置字体时不只设QApplication默认字体连绘图的对应字体也要一起考虑坐标轴标签、图例、刻度文本里使用中文时先把字符串统一转成QString再传入。customPlot-xAxis-setLabel(QStringLiteral(时间)); customPlot-yAxis-setLabel(QStringLiteral(数值));如果之前是直接用字符串字面量导致乱码换成QStringLiteral包装一下通常就好了。还有一个不太起眼但真实存在的坑QCustomPlot的刻度文本和坐标轴标签、图例用的字体分组不一样你虽然设了xAxis-setLabelFont但刻度数字的字体没设某些自带字体中缺少中文字形时刻度也还是会长成方块。稳妥做法是用系统自带的雅黑类开源字体全局设置一遍。5.3 坐标轴刻度错乱手动指定ticker有次我画的是生意场景里的交易量数据量级从几万到几千万Y轴刻度一直显示成拥挤的科学计数法挤在一起根本看不清。QCustomPlot默认的刻度生成器是QCPAxisTicker它根据数据范围自动挑选步长在大动态范围的数据上往往会把步长选得很碎。解决办法是手动改刻度策略QSharedPointerQCPAxisTicker ticker(new QCPAxisTicker); ticker-setTickCount(8); ticker-setTickStepStrategy(QCPAxisTicker::tssMeetTickCount); customPlot-yAxis-setTicker(ticker);如果数据跨度太大还可以用QCPAxisTickerLog强制对数坐标或者用QCPAxisTickerText让刻度完全由自己指定。重点是你得知道刻度生成器这个概念——搜QCustomPlot的ticker相关文档大多数刻度显示异常的问题都能在这里面找到答案。5.4 内存只增不减数据点无限增长实时刷新场景里如果一直用addData追加数据而不清理旧数据内存会一路走高。虽然QCustomPlot内部用了分块存储点到一定量后内存体量也非常可观。我的做法是定期裁剪数据窗口customPlot-graph(0)-data()-removeBefore(t - windowSize);或者更彻底的直接重建graph的data容器。这块没有银弹只能在业务层做取舍。如果你是用setData覆盖整段数据的那就不存在这个问题。5.5 窗口缩放和布局resize后坐标轴挤在一起QCustomPlot是基于QWidget的自带layout布局能力但如果你在代码里手动创建多个坐标轴、多个inset布局窗口resize后时常出现坐标轴重叠或者图例跑到画布外面。遇到这种问题先把它简化成所有东西都挂在axisRect上的思路QCustomPlot的根布局默认只有一块axisRect区域图例是axisRect的孩子坐标轴是axisRect的属性。尽量让所有元素都归属到一个axisRect缩放时控件内部会自己算好位置。如果你确实要用多个画布区域比如上下两块上面波形下面频谱用plotLayout()-take(axisRect)再plotLayout()-addElement手动管理而不是直接new多个QCustomPlot塞进布局后者虽然也能显示但性能和交互差一个量级。6. 常用进阶配置与图片导出6.1 颜色图与色阶条QCustomPlot不只能画曲线QCPColorMap做二维色阶图也很好用适合表示热力分布、频谱瀑布图这类数据。用法大致是创建ColorMap、设置数据范围、按网格点填入数值QCPColorMap *colorMap new QCPColorMap(customPlot-xAxis, customPlot-yAxis); colorMap-data()-setSize(200, 100); colorMap-data()-setRange(QCPRange(0, 1), QCPRange(0, 1)); for (int x 0; x 200; x) { for (int y 0; y 100; y) { colorMap-data()-setCell(x, y, sin(x * 0.1) cos(y * 0.2)); } } colorMap-setInterpolate(true); colorMap-rescaleDataRange(); customPlot-replot();色阶条则是通过QCPColorScale挂到右侧把ColorMap和ColorScale关联起来数值和颜色之间就有了一条对应轴。这块用的时候需要多看几个示例因为ColorMap的左下角坐标和像素网格点是两套概念我一开始就搞混过画出来的图整体偏移。6.2 导出图片和PDF最终交付给同事老板看的时候常常需要把图表导成图片。QCustomPlot两条导出命令就够customPlot-savePng(result.png, 1920, 1080); customPlot-saveJpg(result.jpg, 1920, 1080, 90);如果导出到PDF要在工程里加上printsupport模块然后customPlot-savePdf(result.pdf, 1920, 1080);这里有个小坑savePng这类函数默认按当前控件大小导出如果控件本身尺寸很小导出的高清图片也会只有那么大。想要高分辨率图就显式指定宽和高或者设置setExportImageQuality。导出前最后调一次replot()确保数据是最新的。6.3 交互与动态缩放框、点击选中QCustomPlot默认就支持鼠标滚轮缩放、拖拽平移这些都是QCPAxisRect自带的交互能力。如果想让用户放大之后能快速回到初始视野可以连上mouseRelease信号或者在上下文菜单里重新调用rescaleAxes()connect(customPlot, QCustomPlot::mouseDoubleClick, this, []{ customPlot-graph(0)-rescaleAxes(); customPlot-replot(); });双击重置视野这件事在演示时特别受欢迎别人用你的工具看数据时操作越简单越容易被接受。6.4 可配置化的主题封装项目大了之后我强烈建议把QCustomPlot的初始化过程封装成一个统一入口背景色、网格线、坐标轴颜色、曲线配色、图例样式、字体大小全部从配置文件或启动参数读取。这样做的好处是后续换肤、换品牌色时不用在十几个界面里到处改代码。我自己就是封装了一个PlotThemeHelper传入一个结构体就能把整套视觉风格套上去。7. 性能优化让刷新和烫手键盘说再见实时绘图性能优化与其记一堆碎片技巧不如先理解QCustomPlot绘制一条曲线的整体链路数据存储 → 坐标变换 → 折线生成 → 抗锯齿 → 像素渲染。瓶颈一般出现在两步数据点过多导致坐标变换耗时抗锯齿导致像素填充耗时。所以我的优化顺序是降数据量能降采样就先降采样屏幕宽度只有1920像素画10万个点其实绝大部分是无效计算开自适应采样setAdaptiveSampling(true)让QCustomPlot自己决定哪些点要被真正画出来关闭不必要的抗锯齿setNotAntialiasedElements(QCP::aeAll)曲线边缘的轻微锯齿在实时刷新场景几乎看不出差异减少replot频率攒一批数据再重绘比每点重绘代价小一个数量级。这里我想特别强调第二步因为很多人不知道QCustomPlot有这个选项。开了自适应采样之后几十万点拖拽缩放的流畅度提升是肉眼可见的。你可以做一个对比实验生成20万个点先不开自适应采样拖动一下曲线再打开感受差异。试过之后你就再也不想关掉它了。另外如果你的界面里放了多个QCustomPlot实例每一个实例都会在replot时触发自己的整块重绘。界面无操作时也要确保没有定时器在repeat调replot否则多个实例叠加起来CPU占用轻松上30%。我踩过这个坑界面有三个plot刷新时每个都replot结果三个实例抢CPU同事看我电脑风扇狂转。后来统一改成只有一个全局刷新定时器三个plot依次replotCPU立刻降下来了。8. 常见问题速查表把前面讲到的容易翻车的点集中成一张速查表给新人和健忘的自己留底问题原因解决办法编译报错找不到头文件工程模块不全.pro里加QT widgets printsupport中文坐标轴变问号源码编码/字体问题统一UTF-8用QStringLiteral设置中文字体CPU高企replot调用过于频繁降低刷新率、攒批重绘、开关抗锯齿大量点拖动卡顿没开自适应采样setAdaptiveSampling(true)内存持续上涨数据无限增长定期data()-removeBefore()刻度排列密集默认ticker策略不匹配调整QCPAxisTicker步长或换对数/文本ticker图例遮挡曲线图例位置没设置用insetLayout指定图例对齐位置多个plot实例共同卡顿各plot独立高频重绘统一一个刷新定时器依次replot如果你按这张表操作下来问题还在别急先检查自己用的QCustomPlot版本——有些行为差异其实是不同版本造成的升级到最新版本往往能顺手解决不少奇怪问题。最后说个我自己的体会QCustomPlot不是那种装完就忘的库它给了你足够多的自由度代价是你必须理解它的绘图模型——数据、坐标轴、画布、图例、交互器这几个概念理顺了后面所有功能做起来都顺水推舟。它倒是有一个我一直不太适应的点老版本和新版本在部分API上有改名或调整比如旧代码里某些枚举和新版不兼容所以如果你从网上参考老代码记得先核对一下版本号否则会遇到同一件事两个写法的困惑。整体来说它仍然是我在Qt项目里最愿意信赖的绘图方案值得花时间好好掌握。