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 里的行循环方向改成相反即可。从那以后我每次写完瀑布图都会先跑一遍扫频模拟数据确认方向和色阶再接真实硬件避免在联调现场手忙脚乱。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

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

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

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

2026/10/16 12:49:27 阅读更多 →
用Go实现GitLab pre-receive钩子:强制规范commit message

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

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

2026/10/16 12:49:27 阅读更多 →
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:27 阅读更多 →

最新新闻

DeepSeek提示词实战指南:50个模板解锁职场、内容与编程效率

DeepSeek提示词实战指南:50个模板解锁职场、内容与编程效率

简介:覆盖职场、自媒体、电商、编程与个人成长五大场景的50个DeepSeek实用提示词,以单份PDF文档形式提供,文件大小约972KB,适合初次接触DeepSeek或其他自然语言处理工具的入门者,也适合希望用提示词提升写作、运营与开…

2026/10/16 16:01:31 阅读更多 →
NLP算法岗面试指南:高频考点、大模型与项目复盘全解析

NLP算法岗面试指南:高频考点、大模型与项目复盘全解析

直接开讲。NLP算法岗面试,到底在面什么?我把自己被问过的问题都倒出来了先说个大实话:NLP算法岗面试,这两年变化比前五年加起来都大。我自己面了十几场,也作为面试官坐在桌子另一边问过别人。最大的感受是——面试考察…

2026/10/16 16:01:31 阅读更多 →
GPT技术路线全解析:从生成式预训练到工程落地

GPT技术路线全解析:从生成式预训练到工程落地

GPT 这个缩写放到今天,已经很难说清楚它到底是“模型名”还是“技术路线”了。把它完整展开成 Generative Pre-Training,其实是一整套训练范式的浓缩:先用海量文本把模型喂成一个“懂很多常识的说话者”,再在具体任务上做轻量调整…

2026/10/16 16:01:31 阅读更多 →
VOC数据集mAP计算详解:两代标准差异与手工推算方法

VOC数据集mAP计算详解:两代标准差异与手工推算方法

调试目标检测模型的那几个月,我对voc数据集里的mAP计算方式印象格外深。前阵子某朋友跑完一个检测模型,拿着结果跟我说mAP到了0.79,我拿同一份预测文件复算了一遍,却只有0.74。两个数字都声称是“VOC的mAP”,差的这0.0…

2026/10/16 16:01:31 阅读更多 →
华为云Stack架构方案实战:从HCIE备考到政企云落地

华为云Stack架构方案实战:从HCIE备考到政企云落地

简介:这是一份面向HCIE-Cloud Computing V3.0认证的华为官方培训教材,适合正在备考华为云计算HCIE的工程师,也适合需要系统理解华为云Stack方案的解决方案架构师。教材从传统IT与企业数字化转型的大背景切入,梳理数字经济、国家数…

2026/10/16 16:01:31 阅读更多 →
零基础学漏洞挖掘:从靶场练手到报告撰写的避坑指南

零基础学漏洞挖掘:从靶场练手到报告撰写的避坑指南

零基础写“挖洞”这个词,这几年在网安圈已经快被说烂了。但说实话,我见过太多人把这个词理解偏了。大多数新手一上来就想要平台清单,想直接拿别人的业务练手,结果要么学不进去,要么在违法的边缘反复横跳。我做了十多年…

2026/10/16 16:00:29 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/15 16:18:54 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/16 14:23:36 阅读更多 →