尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
QCustomPlot瀑布图绘制详解:从QCPColorMap到实时滚动实现
简介基于Qt与QCustomPlot的瀑布图实现demo主要面向需要在Qt界面中展示数据变化过程的C开发者。demo通过随机数生成数据完整演示了初始化绘图控件、配置坐标轴与刻度、创建QCPGraph对象、绘制矩形及连接线、调用replot刷新视图等关键流程并加入逐个矩形显示的动态效果与鼠标交互便于观察每个数据点的数值和正负颜色区分。复现该工程后既能掌握QCustomPlot常用API也能理解瀑布图在财务分析、项目管理等场景下的数据结构设计思路。资源共36个文件以cpp源码、h头文件、pro工程配置为主附带ui界面文件、编译产物和可执行文件压缩包仅6.08MB可直接在Qt环境下打开调试。目前已有7812人学习使用适合初学或进阶Qt自定义绘图、希望快速落地图表组件的开发者。1. 为什么用 QCustomPlot 画瀑布图先搞懂数据长什么样用 Qt 和 C 做实时数据展示瀑布图是个绕不开的需求。无论是频谱分析、声学测量还是振动监测你手里都是一帧一帧的时间序列X 轴是频率或通道Y 轴是时间或帧序号Z 轴是幅度。单纯画一条最新曲线只能看到当前瞬间把几十帧甚至几百帧叠成一张二维图用颜色表示幅度大小就能一眼看出某个频点的能量随时间怎么变化。QCustomPlot 自带 QCPColorMap做这种图不需要引入 OpenGL 或 Qt Charts一个控件就能出图。这份 demo 的核心价值就是演示数据进来之后怎么存、怎么刷、怎么让颜色和坐标轴不出错。适合刚接触 QCustomPlot 的 Qt 开发者也适合要做采集系统原型验证的工程师。2. 瀑布图的绘图模型QCPColorMap 与数据映射2.1 瀑布图不是“图”是矩阵三维数据如何塞进二维画布很多第一次接触瀑布图的人会误以为它是类似波形图的曲线堆叠。实际上瀑布图的本质是一个二维矩阵矩阵的行是时间帧或者扫描线列是频率点或者通道每个单元格存储一个幅度值。把这个矩阵画在坐标轴上再用颜色梯度映射幅度就形成视觉上的“瀑布”。QCustomPlot 里对应这个矩阵的类是 QCPColorMapData。它不保存一个QVectorQVectordouble那样的动态二维数组而是用固定尺寸的网格。这意味着你在构造瀑布图之前就要想清楚两个问题每一帧有多少个点矩阵列数以及保留多少帧历史矩阵行数。这两个参数直接决定内存占用和刷新效率。选择 QCPColorMap 而不是用QCPGraph画很多条曲线是因为瀑布图要显示的是连续变化的整体趋势。曲线堆叠到几百条时遮挡、重叠、颜色区分都很难处理而 ColorMap 天然支持从数据到颜色的查表映射性能瓶颈主要在矩阵拷贝和重绘上。这里有个关键认知ColorMap 不是散点图它要求整个矩形区域都有数据。没有填数据的单元格会变成 NaN在图上默认显示为黑色或者透明这会干扰视觉判断。所以实现瀑布图时通常要预先分配一块固定大小的“画布”新帧写入旧帧滚出保证每个单元格始终是有效数据。2.2 QCustomPlot 里的两个关键类QCPColorMap 和 QCPColorScaleQCPColorMap 负责把二维网格数据画到绘图区QCPColorScale 则是右侧那条竖着的色阶条它本质是一个额外的小坐标轴告诉你颜色深浅对应的数值大小。二者必须配合使用否则你只能看到一片颜色不知道具体幅度。创建这两个对象的典型代码如下// 创建一个色阶条并放到绘图区的右侧 QCPColorScale *colorScale new QCPColorScale(customPlot); customPlot-plotLayout()-addElement(0, 1, colorScale); // 创建颜色图绑定到主坐标轴 QCPColorMap *colorMap new QCPColorMap(customPlot-xAxis, customPlot-yAxis); colorMap-setColorScale(colorScale); // 设置网格尺寸1024 个频率点512 帧历史 colorMap-data()-setSize(1024, 512); // 设置网格对应的坐标范围 colorMap-data()-setRange(QCPRange(0, 1023), QCPRange(0, 511)); // 选择颜色梯度gpJet 是常见的彩虹色标 colorMap-setGradient(QCPColorGradient::gpJet);这里参数需要解释一下。setSize(1024, 512)的第一个参数是 key 方向单元格数量对应 X 轴第二个参数是 value 方向单元格数量对应 Y 轴。如果你把频率放 X 轴、时间放 Y 轴那么 1024 就是频率点数512 就是保留的历史帧数。setRange后面的两个 QCPRange 分别对应 key 方向和 value 方向的物理坐标范围。有个新手常犯的错误setSize(1024, 512)和setRange(QCPRange(0, 1023), QCPRange(0, 511))里的数字看起来没差多少但前一个是“单元格数量”后一个是“坐标范围”。如果后续你用setCell()填充数据cell 的索引是从 0 到 size-1。如果坐标范围和 cell 索引对不上鼠标悬停显示的实际坐标就会错位。2.3 数据格式与坐标轴把 x、y、z 对齐到网格瀑布图的核心操作是“往网格里填数据”。QCPColorMapData 提供两个填数方法setCell(int keyIndex, int valueIndex, double value)和setData(double keyCoord, double valueCoord, double value)。前者用网格下标后者用实际坐标内部会做坐标到下标的换算。实际项目中我一般用setCell因为数据帧天然是按顺序来的。假设你的采集设备每帧返回 1024 个频点存在QVectordouble frame里那么把这一帧写入颜色图的代码就是循环调用setCell。// 假设已经创建好 colorMapx 方向 1024 个点y 方向 512 帧 for (int col 0; col 1024; col) { colorMap-data()-setCell(col, rowIndex, frame[col]); }这里rowIndex是当前帧对应的行号。注意setCell的第一个参数是列key第二个参数是行value。如果你搞反了把 frame 写到了行方向画出来的图就是旋转了 90 度的错误结果。坐标轴的范围也要和网格范围对应。比如你的频率范围是 0 到 100MHz采样点 1024 个那么正确做法是colorMap-data()-setRange(QCPRange(0, 100e6), QCPRange(0, 30.0)); // 第二个参数是时间秒数 customPlot-xAxis-setRange(0, 100e6); customPlot-yAxis-setRange(0, 30.0);这样每个 cell 的中心点就能正确映射到实际频率和时间上。如果你还调用了customPlot-rescaleAxes()它会根据数据最小最大值把坐标轴强行拉到一个范围很可能破坏你对坐标轴的手动设置导致后续写入的数据“看不见”。2.4 颜色映射从数据到颜色的三种姿势颜色映射决定了瀑布图的“对比度”。默认情况下QCustomPlot 会使用当前数据的最大值和最小值作为颜色范围这在大动态范围信号下是非常糟糕的。常见做法有三种。第一种是固定范围。适合信号幅度基本不变的场景比如无线电接收机用 dBm 表示功率范围通常固定在 -120dBm 到 0dBm 之间。colorMap-setDataRange(QCPRange(-120.0, 0.0)); colorScale-setDataRange(QCPRange(-120.0, 0.0));第二种是动态范围也就是每次写入后调用rescaleDataRange()让颜色范围跟着数据走。优点是能充分利用颜色梯度缺点是某一帧出现尖峰时整体对比度会被瞬间拉低。第三种是对数范围。很多瀑布图数据跨度很大振动加速度从 0.001 到 10直接用线性范围会把小信号压成一片暗色。常见做法是先把数据取对数再填充比如20.0 * std::log10(value)然后再设固定范围。色阶条上显示的就是对数后的数值习惯了之后反而直观。3. 动手实现从空窗口到可交互瀑布图3.1 先把 demo 跑起来工程配置与编译前的三件事拿到这份 demo 后第一步不是看代码而是先确认工程能编译。这个 demo 基于 Qt Widgets 和 QCustomPlot工程文件里至少要配置三件事把qcustomplot.cpp和qcustomplot.h加进项目添加printsupport模块如果要用 OpenGL 加速再定义一个宏。.pro文件可以参考下面这样QT core gui widgets printsupport CONFIG c11 SOURCES main.cpp mainwindow.cpp qcustomplot.cpp HEADERS mainwindow.h qcustomplot.h这里printsupport不是可选项。QCustomPlot 的 QCPColorScale 内部依赖 QPrinter如果漏了编译时会报“未定义引用”或找不到 QtPrintSupport 的头文件。我见过有人为了省事去掉这一项结果花半小时排查链接错误。OpenGL 宏QCUSTOMPLOT_USE_OPENGL在 qcustomplot.cpp 里是默认启用的吗其实要看你的 Qt 版本。如果启用后编译失败最稳妥的办法是先关闭 OpenGL等瀑布图逻辑通了再考虑性能优化。对新手来说先跑通比先跑快更重要。3.2 核心代码走读每一帧数据如何滚动进瀑布图瀑布图的核心逻辑是一个“滚动窗口”。假设你固定保留 512 帧历史那么每当新帧到来就把旧数据整体往下挪一行把新帧放到最上面一行。这样在视觉上最新的数据出现在图的顶部历史数据逐渐被推出底部。完整函数如下void MainWindow::appendFrame(const QVectordouble frame) { QCPColorMapData *data m_colorMap-data(); const int cols >m_customPlot-setInteractions(QCP::iRangeDrag | QCP::iRangeZoom); m_customPlot-axisRect()-setupFullAxesBox(true);启用后鼠标拖拽可以平移视图滚轮可以缩放。对于瀑布图有个细节一旦你启用了iRangeZoom滚轮默认是以鼠标为中心缩放如果鼠标不在绘图区会缩放全部坐标轴包括色阶条的坐标轴。如果你不想让色阶条跟着缩放可以通过colorScale-axis()-setSelectable(QCP::stNone)之类的设置关掉它的交互。性能取舍方面最基础的原则是避免每来一帧都调用一次同步replot()。同步 replot 会立即重绘整个控件如果数据写入频率是 30Hz操作在 30Hz 下肉眼看不出来但如果采集设备到了 1kHz 帧率UI 线程就会卡死。常见做法是把数据写入和界面刷新解耦写入函数只更新数据然后调用replot(QCustomPlot::rpQueuedReplot)或者用 QTimer 定时刷新。4. 避坑与常见问题坐标轴、内存和刷新那点事4.1 现象新数据写进去图却不更新排查思路是先区分“数据没写进去”还是“没重绘”。我见过有人写了两百行代码结果在每次写入后没调replot()还以为是 QCustomPlot 的 bug。实际上ColorMap 不会自动重绘它只知道你改了数据必须显式要求它刷新。另一个原因是行列索引填反了。如果你在setCell里把 col 和 row 对调数据确实写了进去但画出来的是一个旋转了 90 度的图案在某些测试数据下看起来就像“没更新”。验证方法是打印几个特定位置的 cell 值比如qDebug() m_colorMap-data()-cell(0, 0);如果打印出来是新帧的第一个值说明数据写对了如果还是旧值说明写入逻辑本身有问题。还有一种隐蔽情况你在创建 QCPColorMap 之后又调用了customPlot-graph()添加了一个空曲线。这个空曲线会留在绘图区上方颜色图被它盖住。检查图层最直接的方法是customPlot-layerableAt(pos)看鼠标位置有没有被其他对象遮挡。4.2 现象瀑布图“拖影”严重颜色渐变发灰这是动态色阶的典型问题。当数据里偶尔有一个尖峰rescaleDataRange(true)会把颜色范围瞬间拉得很宽原来细节丰富的中低幅度区域全部变成相近颜色画面看起来发灰、发暗并且持续一段时间不能恢复。原因在于rescaleDataRange的逻辑是取当前所有数据的全局最小最大值对噪声敏感。解决思路是不要用全局极值而是用分位数。比如取所有帧数据的 95% 分位数作为颜色上限这样即使出现尖峰也只会把尖峰截断成纯红其余区域对比度依然正常。QVectordouble allData; // 把最近 512 帧的数据收集到 allData 里 std::sort(allData.begin(), allData.end()); double upper allData[allData.size() * 0.95]; m_colorMap-setDataRange(QCPRange(0, upper));如果不想引入排序的开销更简单的做法是固定色阶范围。对频谱设备来说dBm 范围一般是已知的直接setDataRange(QCPRange(-120, 0))压根不需要动态更新。4.3 现象内存暴涨几百帧后界面卡死这个问题的根源通常是数据存储无上限。有些实现会把每一帧都 push 到一个QVector里然后在 replot 时把所有帧重新编码到 ColorMap。这在帧数少的时候没问题但历史帧数涨到上万帧后矩阵拷贝时间和内存占用都会线性上升。解决办法是构造 ColorMap 时就固定帧数并让 appendFrame 只做固定大小的滚动拷贝不新增任何数据结构。如果你确实需要保留全部历史可以考虑把历史数据序列化到磁盘界面上只保留最近 N 帧。还有一个常见内存泄漏点QCPColorMapData::setSize被反复调用。每次调用都会重新分配内部数组。如果你的代码里一帧调用一次setSize内存碎片很快就会让程序卡死。正确方式是只在初始化调用一次 setSize之后只填充数据。4.4 现象坐标轴标签和色阶刻度对不上色阶条上显示 0 到 100而鼠标悬停在图上却显示 0 到 1000这就是数据范围和色阶范围没同步。QCPColorScale本身不会自动跟随 ColorMap 的dataRange需要显式设置。m_colorMap-setDataRange(QCPRange(-80.0, 20.0)); m_customPlot-colorScale-setDataRange(QCPRange(-80.0, 20.0)); m_customPlot-colorScale-axis()-setLabel(dBm);参数说明第一个 setDataRange 影响颜色图内部的数据到颜色映射第二个影响色阶条坐标轴的显示。两者必须用同一个范围否则图上的颜色和右侧色阶不匹配。如果你还修改了坐标轴范围注意 xAxis/yAxis 的 isRangeValid 状态。把data()-setRange和坐标轴 setRange 设成一致是最不容易出错的做法。5. 进阶玩法把瀑布图变成性能检测利器当瀑布图能跑通之后下一步是让它稳定地跑在实时系统里。这里分享一个我常用的固定模式固定帧数、固定色阶、固定刷新率。固定帧数前面已经讲过setSize(1024, 512)这个 512 要按你的实际需要来定。如果信号变化慢512 帧足够看到一个完整趋势如果变化快加大帧数会把细节涂掉反而看不清。固定色阶需要你先摸底目标信号的范围。我一般会在数据源接一个连续采集模式跑几秒钟统计一下幅度分布然后选一个能覆盖 95% 数据点的范围固定下来。这样瀑布图的颜色长期稳定不会因为某个随机尖峰忽亮忽暗。固定刷新率用 QTimer 实现而不是在数据回调里直接刷界面QTimer *timer new QTimer(this); timer-setInterval(40); // 25 FPS connect(timer, QTimer::timeout, this, [this]() { QVectordouble frame m_device-nextFrame(); appendFrame(frame); }); timer-start();这里m_device-nextFrame()模拟从采集设备拉取一帧数据。即使设备产生数据的速度不均匀界面也始终按 25FPS 稳定刷新不会忽快忽慢。最后说一个验证技巧用扫频信号做自测。把一个频率随时间线性增加的正弦波输入到瀑布图如果时间轴方向正确图上会出现一条斜的亮带如果亮带方向反了说明滚动方向反了把 appendFrame 里的行循环方向改成相反即可。从那以后我每次写完瀑布图都会先跑一遍扫频模拟数据确认方向和色阶再接真实硬件避免在联调现场手忙脚乱。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

公共文化场馆携程评论爬取实战:反爬应对与文旅数据应用

公共文化场馆携程评论爬取实战:反爬应对与文旅数据应用

1. 项目概述:为什么一个城市图书馆的评论数据值得被系统性采集?“广州图书馆——携程评论爬取”这个标题乍看有点违和——图书馆不是政务服务平台或公共文化场馆吗?怎么会出现在携程这样的旅游预订平台上?但正是这种“反常识”的组…

📅 2026/10/16 12:49:20
用Go实现GitLab pre-receive钩子:强制规范commit message

用Go实现GitLab pre-receive钩子:强制规范commit message

简介:这是一份采用 Go 语言编写的 GitLab pre-receive(预接收)钩子示例资源,面向 GitLab 管理员、DevOps 工程师及对服务端钩子机制感兴趣的开发者。它演示了如何在代码推送前拦截提交,通过检查最新提交消息是否包含指…

📅 2026/10/16 12:49:20
PySOT评估必装pysot-toolkit:Ubuntu 16.04编译安装与评测原理详解

PySOT评估必装pysot-toolkit:Ubuntu 16.04编译安装与评测原理详解

1. 为什么必须单独安装 pysot-toolkit:PySOT 主库与评估工具的职责边界 很多人在跑通 PySOT 训练流程后,会自然地认为“模型训出来了,接下来直接 eval 就行”——结果在终端敲下 python tools/test.py 时,十有八九会遇到 Modul…

📅 2026/10/16 12:49:20
MORE NEWS

更多资讯

📰

Django电信资费管理系统:资费模型与出账流程设计实践

前阵子整理旧项目,翻出一个用于内部业务演练的电信资费管理系统,路径名带了一串自动生成的随机后缀,项目代号就不提了,核心内容是典型的"Python Django"。当时做完这个模拟项目X挺感慨的:电信资费管理听起来…

📰

MFC网格控件CGridCtrl实战:接入、配置与避坑指南

简介:面向MFC界面开发者的可直接使用的表格控件资源,提供全套源码且无需注册即可集成到项目中。相比MSFlexGrid在大数据量时容易报错,该控件内置虚拟缓冲区模式,实测可流畅显示20002000规模的虚拟表格,适合数据报表、管…

📰

大模型落地指南:从模型选型到Agent工程化与评测实践

1. 本周行业观察:大模型竞赛进入“冷静高效期”这周翻遍各家的动态,我最大的感受是:行业节奏变了。前几年大家比的是谁把模型参数堆得更高、谁先发布新版本,而到了2026年10月这个时间窗口,头部玩家明显都把精力从“造发…

📰

C++粒子滤波实战:实时嵌入式定位的内存优化与传感器接入

简介:本资源是一份基于C实现的粒子滤波算法完整源码包,面向机器人定位、目标跟踪与传感器融合等领域的算法工程师及高年级本科生,解决非线性非高斯动态系统状态估计的实际编程落地问题。压缩包共15个文件,含7个头文件(…

📰

MATLAB实现跳频信号调制解调:从原理到硬件部署

简介:本资源是一份面向通信工程专业学生、无线通信方向初学者及MATLAB实践者的跳频通信系统仿真教学材料,聚焦于跳频信号调制与解调的核心原理与代码实现。资源提供完整的MATLAB可运行脚本(.m文件),涵盖数据生成、伪随…

📰

微信支付v3退款接口实战:Spring Boot集成与AES-GCM解密

简介:面向Java后端开发者的微信退款接口对接示例包,解决商户处理用户退款请求时与微信服务器安全交互的完整流程,适合需要快速接入微信支付退款功能的电商、ERP及第三方支付系统开发者。压缩包共29个文件,大小1.92MB,其…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬