QT曲线绘制实战:QCustomPlot、QChart与QPainter选型及性能优化 简介这是一份面向QT初学者与图形界面开发者的曲线绘制入门示例基于QT框架在QT Creator环境中演示如何利用QGraphicsView、QGraphicsScene与QPainter实现静态或动态曲线展示可用于数据可视化场景的快速上手与教学参考。资源包共6个文件包含2个cpp源文件、1个h头文件、1个ui界面文件、1个pro工程文件及1个user配置压缩包仅4KB结构精简覆盖窗口搭建、场景关联、QPainterPath曲线定义、QPen样式设置及视图刷新等核心环节。已有366人学习下载适合希望掌握QT二维绘图流程、理解信号与槽交互及布局管理的开发者参考。通过阅读源码可快速理清从工程配置到曲线渲染的完整脉络并在此基础上扩展缩放、拖动与动态数据绑定等功能。1. 从一块“画歪了”的波形图说起QT 曲线绘制到底解决什么问题去年帮一个做工业数据采集的朋友看代码他拿 QT 的 QCustomPlot 画实时温度曲线结果窗口一缩放曲线直接糊成一片坐标轴刻度还跟数据对不上。他以为是控件有 bug折腾两天没找到原因最后发现是数据缓冲区没做环形队列每次重绘都把整段历史数据重新塞了一遍。这个场景在 QT 曲线绘制里太典型了——不是控件不好用是没人把“数据怎么进、怎么画、怎么刷新”这条链路讲清楚。QT 曲线绘制本质上解决的是把动态数据映射到二维坐标系并高效渲染的问题。它适合三类人做上位机、工控 HMI、仪器仪表界面的开发者需要把传感器数据、日志指标、算法输出可视化的工程师以及被 QChart 性能坑过、想换轻量方案的从业者。核心矛盾只有一个——数据在变画面要跟但你不能每来一个点就重绘整个画布。后面几章我会把 QCustomPlot、QChart、QPainter 自绘三条路拆开讲重点落在数据流组织、坐标映射和刷新策略上这些都是能直接抄进项目的部分。2. 三条技术路线怎么选QCustomPlot、QChart 与 QPainter 自绘的边界2.1 先看数据规模和刷新频率再决定用哪个选型不是看哪个名字熟而是看你的数据量和刷新节奏。我一般按下面这张表做初筛方案适用数据量刷新频率依赖典型场景QCustomPlot1万~10万点20~60 FPS单头文件源文件实时波形、频谱Qt Charts1千~1万点5~30 FPSQt Charts 模块报表、仪表盘QPainter 自绘任意自己控自己控无定制交互、超大数据QCustomPlot 的优势在于它是两个文件qcustomplot.h / qcustomplot.cpp拖进工程就能用不依赖 Qt Charts 模块渲染走的是自己实现的绘制路径对实时曲线友好。Qt Charts 是官方模块API 更“Qt 味”但默认的 QLineSeries 每追加一个点都会触发内部数据结构调整数据量上去之后掉帧明显。QPainter 自绘最灵活但坐标变换、抗锯齿、交互命中检测全得自己写适合有明确定制需求且愿意投入维护成本的团队。提示如果你的 QT 版本是 5.15 以上且用 CMakeQt Charts 需要显式 find_package(Qt5 COMPONENTS Charts)否则编译期就报找不到头文件。2.2 用 QCustomPlot 搭一个最小可跑的曲线窗口下面这段代码是我常用的起步模板包含坐标轴配置、一条曲线、以及一个定时器驱动的数据追加。关键点在于只追加、不重建。// mainwindow.cpp #include mainwindow.h #include ui_mainwindow.h #include qcustomplot.h MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) { ui-setupUi(this); // 1. 创建 QCustomPlot 并设为中心控件 QCustomPlot *plot new QCustomPlot(this); setCentralWidget(plot); // 2. 添加一条曲线指定名称和关联坐标轴 QCPGraph *graph plot-addGraph(); graph-setName(温度曲线); graph-setPen(QPen(Qt::blue, 2)); // 3. 配置 X 轴为时间轴风格Y 轴自适应 plot-xAxis-setLabel(时间 (s)); plot-yAxis-setLabel(温度 (℃)); plot-xAxis-setRange(0, 60); plot-yAxis-setRange(0, 100); // 4. 定时器每 100ms 追加一个点 QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, []() { static double t 0; double value 50 30 * qSin(t * 0.1); // 模拟数据 graph-addData(t, value); t 0.1; // 只保留最近 600 个点防止内存无限增长 if (graph-dataCount() 600) { graph-data()-removeBefore(t - 60); } plot-xAxis-setRange(t - 60, t); plot-replot(QCustomPlot::rpQueuedReplot); // 队列重绘避免阻塞 }); timer-start(100); }逻辑说明addData是 QCustomPlot 提供的追加接口内部用 QVector 存储追加是 O(1)。removeBefore按 X 轴值删除旧点保证内存可控。rpQueuedReplot把重绘请求排到事件循环末尾避免在定时器回调里同步重绘导致界面卡顿。参数上setRange的窗口宽度决定了可视时间跨度dataCount的上限决定了内存占用这两个值要根据你的采样率和屏幕宽度调。2.3 QChart 方案里最容易翻车的一步如果你坚持用 Qt Charts注意 QLineSeries 的 append 是逐点触发的正确做法是先series-append()再统一chart-update()或者用QVectorQPointF批量替换。下面是一个批量更新的写法// 批量更新 QLineSeries避免逐点触发内部重算 QVectorQPointF points; points.reserve(buffer.size()); for (const auto sample : buffer) { points.append(QPointF(sample.time, sample.value)); } series-replace(points); // 一次性替换比 clearappend 快一个量级 chartView-chart()-update();replace会一次性重建内部索引适合每帧全量刷新的场景。如果你的数据是滑动窗口维护一个固定长度的buffer每帧替换即可。注意QVector的 reserve 不能省否则频繁扩容会拖慢帧率。3. 数据流与坐标映射把“点”变成“线”的中间层3.1 环形缓冲区是实时曲线的标配不管用哪个绘图库数据进入绘图控件之前都应该经过一个环形缓冲区。原因很简单绘图控件的内部存储不是为高频删除设计的你让它自己管理历史数据它就会在每次删除时搬移内存。我一般自己维护一个固定容量的环形队列绘图时只把当前窗口内的数据喂给控件。// 简易环形缓冲区容量固定写入 O(1) class RingBuffer { public: explicit RingBuffer(size_t capacity) : m_capacity(capacity), m_head(0), m_size(0), m_data(capacity) {} void push(double t, double v) { m_data[m_head] {t, v}; m_head (m_head 1) % m_capacity; if (m_size m_capacity) m_size; } // 导出按时间排序的窗口数据 QVectorQPointF snapshot() const { QVectorQPointF out; out.reserve(m_size); size_t start (m_head m_capacity - m_size) % m_capacity; for (size_t i 0; i m_size; i) { out.append(m_data[(start i) % m_capacity]); } return out; } private: struct Sample { double t; double v; }; size_t m_capacity, m_head, m_size; QVectorSample m_data; };参数说明capacity按采样率 × 可视时间 × 安全系数估算。比如 100Hz 采样、显示 60 秒容量至少 6000安全系数取 1.5 就是 9000。snapshot返回的是拷贝避免绘图线程和采集线程竞争同一块内存。如果你的采集在独立线程push 和 snapshot 之间要加锁或用无锁队列这是另一个话题但环形缓冲的结构不变。3.2 坐标映射别让浮点数精度毁掉你的曲线坐标映射分两步数据坐标 → 像素坐标。QCustomPlot 和 QChart 内部都做了这件事但当你自绘或者做自定义交互时必须自己处理。常见坑是 X 轴用double存时间戳数值很大比如 Unix 时间戳 1.7e9加上小的时间增量后浮点精度不够曲线会出现台阶。解决办法是把时间戳归一化到相对起点。采集开始时记录t0之后所有点的 X 值存t - t0这样数值范围小精度足够。显示时如果需要绝对时间在坐标轴标签里做转换。// 归一化时间轴避免大数浮点精度丢失 double t0 QDateTime::currentMSecsSinceEpoch() / 1000.0; // 采集回调里 double relativeTime (QDateTime::currentMSecsSinceEpoch() / 1000.0) - t0; ringBuffer.push(relativeTime, value);这个细节在长时间运行的设备上尤其明显跑几个小时之后曲线开始“抖动”很多人以为是传感器问题其实是浮点精度。归一化之后X 轴范围始终在 0 到运行时长之间精度稳定。3.3 刷新策略什么时候重绘比怎么重绘更重要重绘频率不是越高越好。人眼对 30 FPS 以上的变化已经不敏感工业场景 20 FPS 足够。关键是把重绘和数据处理解耦采集线程只管往环形缓冲写UI 线程用定时器按固定间隔取快照并重绘。这样即使采集频率是 1kHzUI 也只按 30 FPS 刷新CPU 占用可控。QCustomPlot 的replot(rpQueuedReplot)会把重绘请求合并如果一帧内多次调用只执行最后一次。Qt Charts 没有这个机制需要自己用QTimer控制update()的调用频率。自绘方案里可以在paintEvent里直接读环形缓冲的最新快照由update()触发同样用定时器限流。注意不要在paintEvent里做数据排序或大量计算绘制线程只做渲染数据准备放在定时器回调里完成。4. 避坑与排查曲线绘制里那些“玄学”问题4.1 曲线闪烁、撕裂现象窗口缩放或数据刷新时曲线出现闪烁或者上半段和下半段对不齐。原因没有开启双缓冲或者绘制过程中数据被修改。QCustomPlot 默认开启双缓冲但如果你在paintEvent里手动调用了replot可能触发递归重绘。Qt Charts 的 QChartView 需要设置setRenderHint(QPainter::Antialiasing)否则边缘锯齿明显。解决确保所有数据修改在 UI 线程完成或者加锁。QCustomPlot 用rpQueuedReplotQt Charts 用定时器限流。自绘时在paintEvent里只读快照不写数据。4.2 数据量大了之后界面卡死现象跑了十几分钟界面越来越卡最后无响应。原因数据点无限追加绘图控件内部存储膨胀每次重绘遍历所有点。或者环形缓冲的snapshot返回了全量数据而不是窗口数据。解决环形缓冲容量固定snapshot只返回当前可视窗口内的点。QCustomPlot 用removeBeforeQt Charts 用replace批量替换。另外检查是否在每次追加后都调用了replot改成定时器统一刷新。4.3 坐标轴刻度和数据对不上现象曲线画出来了但 X 轴标签显示的时间和实际数据点的时间差了几秒。原因坐标轴范围设置和数据追加不在同一个时间基准上。比如数据用相对时间坐标轴用绝对时间或者setRange的参数顺序写反了。解决统一时间基准要么全用相对时间要么全用绝对时间。setRange(lower, upper)的第一个参数是下界第二个是上界别写反。QCustomPlot 的rescaleAxes会自动调整但实时曲线里手动设范围更可控。4.4 多曲线时颜色和图层混乱现象加了三条曲线结果颜色一样或者某条曲线被遮住。原因addGraph返回的指针没有保存后续配置作用到了错误的 graph 上。或者图层顺序没设置后添加的曲线覆盖了先添加的。解决每条曲线保存独立的QCPGraph*指针分别设置 pen 和 name。QCustomPlot 的图层用graph-setLayer调整默认都在main层按添加顺序绘制。需要某条曲线在上层把它移到overlay层或者调整添加顺序。4.5 高 DPI 屏幕上曲线模糊现象在 4K 屏或缩放 150% 的屏幕上曲线和文字发虚。原因Qt 的高 DPI 缩放没有正确启用或者绘制时用了固定像素坐标。解决在main函数里设置QApplication::setAttribute(Qt::AA_EnableHighDpiScaling)Qt 5.6 以上。QCustomPlot 需要调用setNotAntialiasedElements关闭某些元素的抗锯齿或者用QCPPainter的缩放接口。自绘时用devicePixelRatio换算像素坐标。5. 进阶技巧让曲线既快又稳的几个习惯5.1 用 OpenGL 加速大规模点集渲染QCustomPlot 支持 OpenGL 加速在pro文件里加QT opengl然后调用plot-setOpenGl(true)。实测 10 万点以上的曲线开启后帧率能从个位数回到 30 FPS 以上。但注意 OpenGL 模式下某些绘图元素比如文本标注可能渲染异常需要测试。Qt Charts 没有直接的 OpenGL 开关但可以用QOpenGLWidget作为 QChartView 的 viewport。// QCustomPlot 开启 OpenGL 加速 plot-setOpenGl(true); // 检查是否真的启用了 qDebug() OpenGL enabled: plot-openGl();参数说明setOpenGl(true)只是请求实际是否启用取决于系统 OpenGL 版本和驱动。如果返回 false说明当前环境不支持回退到软件渲染。这个开关对点数量大的场景收益明显点少的时候反而增加开销建议按数据量动态决定。5.2 数据降采样显示 1 万个点但只画 2000 个屏幕像素宽度有限1080P 屏幕上 X 轴最多也就 1920 个像素列。如果数据点比像素还密多出来的点对视觉效果没有贡献反而拖慢渲染。我一般用等间隔抽稀或者极值抽稀每 N 个点取一个或者每个像素列取最大最小值各一个。// 极值抽稀每个像素列保留最大最小值保持波形包络 QVectorQPointF downsample(const QVectorQPointF input, int pixelWidth) { if (input.size() pixelWidth) return input; QVectorQPointF out; out.reserve(pixelWidth * 2); int step input.size() / pixelWidth; for (int i 0; i input.size(); i step) { double minV input[i].y(), maxV input[i].y(); double t input[i].x(); for (int j i; j qMin(i step, input.size()); j) { minV qMin(minV, input[j].y()); maxV qMax(maxV, input[j].y()); } out.append(QPointF(t, minV)); out.append(QPointF(t, maxV)); } return out; }逻辑说明step是每个像素列对应的原始点数内层循环找该列的最大最小值输出两个点。这样波形包络不会丢失尖峰不会被抽掉。参数pixelWidth取绘图区域的像素宽度可以从plot-axisRect()-width()获取。抽稀在数据准备阶段做不影响原始环形缓冲。5.3 一个我踩过的坑定时器精度和重绘节奏早期我用QTimer的默认精度Qt 5 里是 1ms 左右但实际受事件循环影响设了 10ms 间隔想跑 100 FPS结果实际只有 30~40 FPS而且曲线一顿一顿的。后来发现是replot同步执行的时间超过了定时器间隔事件循环被阻塞。改成rpQueuedReplot之后重绘请求排队定时器回调快速返回帧率稳定在 60 FPS。另一个习惯是把定时器间隔设成目标帧率的两倍比如想要 30 FPS定时器设 60ms留出余量给系统调度。从那以后我每次做曲线绘制都强制走一遍“环形缓冲 → 快照 → 抽稀 → 队列重绘”这条链路不管用哪个库先把数据流理顺再调渲染参数。希望帮到你。本文还有配套的精品资源点击获取