新闻详情

新闻详情

首页 / 资讯中心 / 详情

Qt动态曲线绘制完整方案:QCustomPlot实时刷新与FFT频谱分析实战

发布时间:2026/9/7 7:51:26来源:尧图网络
Qt动态曲线绘制完整方案:QCustomPlot实时刷新与FFT频谱分析实战
简介这是一份基于Qt的完整示例工程适合希望掌握QCustomPlot绘图库、实现实时动态曲线可视化的中高级Qt开发者。工程将QCustomPlot作为核心绘图组件演示了结合定时器周期性刷新数据、多曲线同步绘制以及主窗口中图表交互等关键技术的实现方式从曲线绘制到重绘刷新展示了完整的动态更新链路。压缩包共9个文件其中4个cpp、3个h负责业务逻辑与绘图接口pro文件用于工程配置ui文件描述主窗口布局整体容量仅242KB轻量且结构清晰。已有446人学习下载可供读者快速研究如何集成QCustomPlot并扩展出带缩放、平移和样式定制的曲线面板。通过阅读源码还能掌握Qt事件循环与定时器在数据可视化场景下的配合技巧对实际项目开发具有直接参考价值可作为入门与进阶的实用参考。 前一阵整理了一个Qt动态曲线绘制的完整示例工程当时的目的很简单给一个做设备上位机的朋友兜个底他那边串口数据一堆一堆地来界面上的曲线动不动就卡成PPT问我有没有现成的方案。于是我干脆把从数据采集、缓冲、绘制到频谱分析、打包发布这一整套东西全都收进了一个示例工程里压缩包就叫“qt绘制动态曲线-完整示例工程.rar”今天把它拆开讲讲。这个示例工程解决的事情很直接在Qt环境里用较少的代码量实现一条高频刷新的实时曲线并且不卡界面。主线做法是用QCustomPlot做绘制控件配合定时器或串口数据信号来持续更新数据同时对数据做滑窗截断只保留当前感兴趣的一段区域。示例中还带了一个时域转频域的小模块用的kissfft画完波形再画频谱一套代码两个图适合做音频分析、振动监测、传感器波形查看这类需求。如果你正打算在项目里加动态曲线功能或者刚接触Qt不久、想找一个能直接跑的示例做参考这个工程应该能帮你省下不少时间。下面从方案选型、代码结构、核心实现到常见坑一条条说清楚。1. 动态曲线的整体思路与方案选型1.1 为什么选QCustomPlot而不是QChart绘实时曲线第一反应往往是Qt官方的QChart。我并不是说QChart不好但在“动态、高频、轻量”这个场景下它的表现并不理想。QChart模块封装得比较重内部对象树复杂实时更新时要频繁操作QLineSeries的数据点稍微不注意就会造成UI线程卡顿。而且QChart依赖Qt Charts独立模块打包部署时要额外带上工程规模整体变大对不少做工业上位机、教学演示、个人项目的朋友来说不太值得。QCustomPlot是另一条路。这个开源库本质上就是两个文件qcustomplot.h和qcustomplot.cpp直接扔进工程里编译即可不依赖额外插件。它内部用QPainter直接绘制虽然底层还是QPainter但做2D曲线、柱状图、散点图、颜色图已经完全够用。社区的成熟度也很高很多示波器类开源项目都在用。整个库持续维护接口稳定。实际项目里一条曲线塞几千个点刷新频率二三十赫兹QCustomPlot完全能撑住。1.2 示例工程的整体结构设计整个示例工程在结构上分成了这样几个层次实践下来比较清晰数据源层默认内置了一个模拟数据发生器用QTimer周期产生正弦波加噪声数据目的是一开箱就能看到动态效果。同时预留了串口接入的接口把QSerialPort收到的数据帧解析后扔进数据缓冲。数据缓冲层用一个环形缓冲区存放最近的N个点好处是数据量可控不会无限增长拖垮绘制。绘制层封装QCustomPlot提供添加曲线、更新数据、坐标轴自适应等接口。交互层界面上放“开始/暂停”“清空”“时域/频域切换”按钮方便演示和调试。这样分层之后每个模块只做一件事。数据源换成摄像头采集也好、网口通信也好只要往缓冲区里塞数据即可绘制层完全不用改。这也是我建议你自己动手扩展时保留的核心思想数据逻辑和绘制逻辑解耦。1.3 时域频域联动设计很多实际场景里光看时域波形不够还要看它的频谱特征比如电机振动、音频分析、心电信号处理。示例工程里加了一个可选的时域转频域模块思路是这样拿时域缓冲区中最近的一段数据做一次快速傅里叶变换把结果绘制在下方的频谱图上。这个模块用得比较多的是kissfft库一个轻量级的FFT实现几十个文件加进工程不像FFTW那样重适合做上位机内置的频谱分析。需要说明的是FFT计算本身可以在UI线程做也可以在子线程做。示例工程里默认放在UI线程但只对当前窗长的数据做变换点数量一般在1024或2048计算量很小实测帧率基本不受影响。如果数据长度长到上万点建议单独开个QThread去跑变换这是后话。2. 核心细节解析与实操要点2.1 QCustomPlot的初始化配置想在窗口上显示一条曲线第一步是创建QCustomPlot控件并设置坐标轴。下面是示例工程中构造函数里的初始化代码// 创建设置画布 customPlot new QCustomPlot(this); customPlot-addGraph(); // 添加一条曲线 customPlot-graph(0)-setPen(QPen(QColor(0, 120, 255))); // 曲线颜色 // 设置坐标轴名称 customPlot-xAxis-setLabel(Time (s)); customPlot-yAxis-setLabel(Value); // 坐标轴范围 customPlot-xAxis-setRange(0, 10); customPlot-yAxis-setRange(-1.5, 1.5); // 显示网格 customPlot-xAxis-grid()-setVisible(true); customPlot-yAxis-grid()-setVisible(true);这里有几个值得注意的点。addGraph()是添加一条曲线一个QCustomPlot可以同时管理多条曲线重复调用addGraph()会得到多条曲线通过graph(index)取用。初始化时设置坐标轴的初始范围很重要不然数据点会把坐标轴撑得乱跳。另外关于坐标轴范围我推荐使用setRange设置一个合理的初始值然后在数据更新时动态调整。示例里为了方便观察把Y轴设成了-1.5到1.5的范围X轴固定显示最近10秒的数据窗口。2.2 动态追加数据的核心思想动态曲线不像静态图那样一次画完而是源源不断地接收新数据点。示例工程里用了一个很简单的思路每来一个新数据点就把旧数据整体左移最右边的点移除最左边的空位放新点。这其实就是一个滑动窗口。但真的要一个个点去操作数组效率很低。更实际的做法是维护一个QVector作为数据缓冲区记录当前所有要显示的点每次更新时往尾部追加当数据超过一定数量时从头部删除同样数量的旧点然后整体重绘。QVectordouble xData, yData; void appendData(double t, double v) { // 超过最大显示点数移除旧数据 if (xData.size() maxPoints) { xData.remove(0, 1); yData.remove(0, 1); } xData.append(t); yData.append(v); }这种做法简单直观但在数据量极大时头部的删除操作会频繁移动内存产生一些开销。如果数据点很多、刷新频率很高可以考虑用环形缓冲区固定大小的QVector加上头尾索引只覆盖旧数据而不移动内存。示例工程里默认使用QVector头部删除的方式因为代码更容易读懂实际使用中几千个点的场景下不会成为瓶颈。2.3 刷新策略别让重绘吃掉所有帧率动态曲线最常栽跟头的地方是重绘频率。如果数据来得特别快每秒几百个点你也跟着每秒重绘几百次界面肯定卡。原因很简单每秒钟的绘制次数超过了显示器刷新率绘制队列堆积界面线程被拖死。示例工程里的做法是数据到达后只做“追加点”的操作不立刻重绘而是定时器以固定频率触发重绘一般2030次/秒就足够了。换句话说高频数据进缓冲低频重绘显示。// 定时器设置30Hz刷新 QTimer *refreshTimer new QTimer(this); connect(refreshTimer, QTimer::timeout, this, MainWindow::refreshPlot); refreshTimer-start(33); // 约30帧/秒再说一个QCustomPlot更新的细节官方提供了replot()和replot(QCustomPlot::rpQueuedReplot)两种方式。连续快速调用replot()会立即重绘如果调用频率过高会阻塞UI线程。而rpQueuedReplot会把多次刷新请求合并成一次等到事件循环空闲再统一绘制性能明显更好。示例工程中默认用的是customPlot-replot()因为数据频率不高但如果你的数据到达速度极快改成replot(QCustomPlot::rpQueuedReplot)是个有效的性能优化点。2.4 几个实用的绘制优化开关QCustomPlot默认开启了抗锯齿和多种特效这在静态图表里是加分项在动态刷新场景里则成了性能拖累。示例工程里做了几个值得记住的优化设置// 关闭曲线抗锯齿让绘图更快 customPlot-setNotAntialiasedElements(QCP::aeAll); // 关闭网格/坐标轴的抗锯齿可以单独控制 customPlot-xAxis-setAntialiased(false); customPlot-yAxis-setAntialiased(false); // 如果不需要图例建议关闭 customPlot-legend-setVisible(false);再一个很容易忽略的点是数据量不大时不需要每次都调用rescaleAxes()自动调整坐标轴因为该函数会遍历所有数据来计算范围数据多了是开销。更好的方法是根据数据窗口固定坐标轴范围或者在追加数据达到某个阈值时才手动调整。示例工程里默认采用固定范围加X轴滑动的方式这样视觉稳定、性能也稳。3. 从零到一搭建完整示例工程的实操过程3.1 创建工程并添加QCustomPlot以Qt 5.15.2为例我先创建了一个普通的Qt Widgets Application。工程创建好后从QCustomPlot官网下载源码包把qcustomplot.h和qcustomplot.cpp拷贝到工程目录下然后右键工程文件选择“添加现有文件”把这两个文件加进去。如果是CMake工程在CMakeLists里加两行即可。add_executable(MyPlotApp main.cpp mainwindow.cpp qcustomplot.cpp )用Qt Creator自带的qmake构建时在.pro文件里加上SOURCES main.cpp mainwindow.cpp qcustomplot.cpp HEADERS mainwindow.h qcustomplot.h这是整个示例工程最基础的一步很多新手卡在“怎么把QCustomPlot用起来”这一步其实核心点就一个把库的源码编进你的工程。这不依赖任何外部模块比用QChart省去了安装模块的步骤。3.2 界面布局设计示例工程的主窗口用了QSplitter把界面分成上下两个区域上面是时域曲线下面是频域曲线。左侧放了一列控制按钮和参数输入框方便调整数据频率、幅度、FFT窗长等参数。布局代码大致是这样// 创建上下两个绘图区 QSplitter *splitter new QSplitter(Qt::Vertical, this); timePlot new QCustomPlot(splitter); freqPlot new QCustomPlot(splitter); // 左侧控制面板 QWidget *controlPanel new QWidget(this); // ... 添加按钮、SpinBox等控件 // 把控制面板和splitter放进水平布局 QWidget *central new QWidget(this); QHBoxLayout *layout new QHBoxLayout(central); layout-addWidget(controlPanel); layout-addWidget(splitter); setCentralWidget(central);为什么用QSplitter因为它天然支持鼠标拖动调整子窗口大小方便用户调大时域图或频谱图。如果只是静态固定布局用QVBoxLayout就够但QLayout做不了拖动调节对演示类工具不太友好。3.3 核心代码模拟数据生成与FFT频谱模拟数据生成器用了QTimer每10ms产生一个点叠加正弦波和随机噪声double t 0; void generateData() { t 0.01; double value sin(2 * M_PI * 5 * t) 0.3 * sin(2 * M_PI * 13 * t) 0.1 * randn(); appendData(t, value); }重点是频域转换部分。示例用kissfft计算频谱先申请一个kiss_fft_cfg然后对时域数据做正变换kiss_fft_cfg cfg kiss_fft_alloc(nfft, 0, nullptr, nullptr); std::vectorkiss_fft_cpx fin(nfft), fout(nfft); // 填充时域数据 for (int i 0; i nfft; i) { fin[i].r (i (int)yData.size()) ? yData[i] : 0; fin[i].i 0; } // 执行FFT kiss_fft(cfg, fin.data(), fout.data()); kiss_fft_free(cfg); // 取幅值 std::vectordouble mag(nfft / 2); for (int i 0; i nfft / 2; i) { mag[i] sqrt(fout[i].r * fout[i].r fout[i].i * fout[i].i) / nfft; }有一个很常见的误区用FFT算幅值后不除以nfft就直接当真实幅值显示导致频谱图的纵轴数值巨大。正确理解是FFT结果的大小与窗长有关。幅度归一化通常要除以nfft单边谱再对非零频部分乘2这样才能得到接近时域波形幅度的数值。示例工程里做了这个归一化处理。频谱图的横轴是频率需要根据采样率计算频率分辨率double freqRes sampleRate / nfft; for (int i 0; i nfft / 2; i) { freqData.append(i * freqRes); }如果采样率设置不对频谱图的横轴就会跟实际频率对不上。比如信号是5Hz正弦波采样率设成100Hznfft为1024那么频率分辨率为100/1024≈0.0977Hz频谱上第51个点左右对应5Hz。这个换算关系在做频谱显示时一定要搞明白。3.4 串口数据接入与帧解析既然是“完整示例工程”肯定不能只有模拟数据还得接上真实的数据源。示例工程里用QSerialPort读串口配合QDataStream或手动解析帧头帧尾。void MainWindow::handleReadyRead() { QByteArray data serial-readAll(); buffer.append(data); // 假设协议格式AA 55 float(4字节) 0D 0A while (buffer.size() 8) { if ((quint8)buffer[0] 0xAA (quint8)buffer[1] 0x55) { // 解析4字节浮点 float value 0; memcpy(value, buffer.mid(2, 4).constData(), sizeof(float)); buffer.remove(0, 8); appendData(currentTime, value); } else { buffer.remove(0, 1); } } }串口解析这块的坑很多最主要的是“数据分批到达”。串口协议一帧数据不一定一次收完可能分几批到达所以必须在外部缓存buffer中做粘包处理。示例中用了最简单实用的“while循环找帧头”方式先缓存所有数据再一帧一帧从缓存里切割。如果帧头匹配不上就一个一个字节丢弃直到找到合理的帧结构。实际项目中更健壮的做法是把buffer换成QByteArray循环队列解析完的字节及时移除避免缓存无限增大。3.5 打包发布示例工程最后包含了打包发布相关的操作文档。Windows下最简单的方式是用windeployqt工具windeployqt MyPlotApp.exe这个工具会自动把Qt运行时库、平台插件platforms目录、样式插件等复制到exe同目录下。执行完windeployqt之后再把qcustomplot相关代码编译进exe不需要额外拷贝什么动态库这样整个程序就能在没有装Qt的机器上运行了。有个注意点如果你用了串口模块还需要确认Qt5SerialPort.dll被打进去。windeployqt一般情况下会识别库依赖但偶尔会漏掉插件建议打包完后手动检查platforms目录下是否有qwindows.dll串口插件是Qt内置模块一般不会被漏掉。4. 常见问题与排查技巧实录4.1 Windows下报“no qt platform plugin could be initialized”听到这句话多半是平台插件没有找到。可执行文件在运行时需要platforms/qwindows.dll如果这个动态库和exe不在正确相对路径下启动就会报这个错误。排查方式很简单确认exe同级目录下存在platforms文件夹里面有qwindows.dll确认没有混用不同版本或不同编译器的Qt动态库确认环境变量QT_QPA_PLATFORM_PLUGIN_PATH没有指向错误位置。实际开发中我在Qt Creator里运行正常双击exe却报这个错就是因为在打包时把platforms目录漏掉了重新执行windeployqt后解决。如果不想用windeployqt也可以手动把Qt安装目录/plugins/platforms整个文件夹复制到exe目录下。4.2 编译报错: error: dependent ..\..\..\allinstall\qt\5.15.2\msvc2019\include\qtw...这类报错的本质是工程文件里引用了错误的Qt库路径。常见原因是工程用到了系统中的旧版Qt路径或者清理工程不彻底导致旧的中间文件残留。如果用的是MSVC套件还要检查环境变量和Qt版本是否匹配。我遇到的情况是电脑里装了多个Qt版本qmake自动选择了其中一个版本而工程中引用的模块不在该版本里。处理办法删除工程目录下的build文件夹以及.pro.user文件重新打开工程重新选择正确的Qt套件。如果工程是从压缩包解压的尽量把文件放到一个目录层级较短的路径下比如D:/QtProjects/MyPlotApp不要放在带中文或空格过多的路径中减少不必要的路径解析错误。4.3 曲线画出来是锯齿状的出现锯齿一般是抗锯齿配置问题。QCustomPlot默认抗锯齿是开启的如果发现锯齿明显可能是在代码里关闭了全局抗锯齿。如果想美观有两种方案一是保持抗锯齿开启牺牲一点性能二是保持抗锯齿关闭但把曲线线宽增加或调整Y轴范围让视觉上不那么突兀。示例工程为了性能默认关闭了抗锯齿。如果你正好做教学演示、数据量不大更在意显示效果可以把setNotAntialiasedElements(QCP::aeAll)这行注释掉曲线会平滑很多。4.4 曲线更新卡顿CPU占用很高这是动态曲线最常见的问题。排查思路按优先级来确认是否在数据到达的槽函数里做了太多计算比如解析、滤波、FFT全堆在UI线程确认重绘频率是否过高QTimer刷新间隔过短会持续占用CPU确认数据点数量是否过大显示几千个点和几万个点的绘制开销完全不同确认是否在每次更新里调用了rescaleAxes()这是开销较大的操作。我自己遇到的一次卡顿是QTimer刷新间隔设成了5ms加上自动缩放坐标轴CPU直接拉高到80%。后来把刷新间隔调整到33ms数据点控制在2000个以内固定X轴范围CPU占用降到了12%左右曲线刷新依然很流畅。4.5 FFT频谱横坐标不对这个问题的根源几乎都是采样率和频率分辨率计算错误。比如采样率标称1000Hz实际数据从串口到达的速度却只有100Hz那么FFT算出来的频谱横坐标和真实信号频率就对不上。解决办法是统计一段时间内实际接收的数据点数反算真实采样率再用于频谱计算。另外一个容易踩的坑是FFT输入的时域数据长度不够后面用0补齐。补零能增加频率点数量但不能提高真实频率分辨率这一点要明确。如果不知道数据具体频率成分尽量在时域图中先观察波形周期估算大概的基频范围再去设置采样率和频率轴范围。4.6 发布后exe无法运行提示缺少DLL一般表现为启动时提示缺少Qt5Core.dll、Qt5Gui.dll等。这时优先用Dependency Walker或Process Explorer检查依赖但最好用的还是回到windeployqt。windeployqt --release MyPlotApp.exe发布时选择Release版本不要发布Debug版本因为带调试符号的动态库体积大很多而且目标机器上往往没有对应运行库。执行完windeployqt后把整个生成目录压缩就是一份可分发的最小运行包。如果还缺少其他运行库比如编译器运行库MSVC的vc_redist也要一并准备。在纯Windows环境里MSVC编译的Qt程序通常需要对应的Visual C Redistributable。5. 最后一个值得扩展的方向示例工程目前只实现了两条曲线的绘制一个是时域、一个是频域。如果你正对着这个工程做二次开发有几个扩展方向个人觉得很实用多通道数据的并行显示比如同时看两路传感器波形数据存储和回放把原始数据保存成CSV或二进制文件后期可以回放分析加入峰值检测和报警功能实时监控数据越界并在界面上高亮提示。从我自己的使用体验来说动态曲线的核心难点不是“画一条线”而是“保证数据链路各环节不互相拖累”。数据采集线程、解析逻辑、绘图刷新这三者一定要解耦否则再漂亮的界面也会在真实数据的冲击下问题百出。把数据缓冲和刷新机制想明白后面的扩展才会顺手很多。如果你拿到这个示例工程后跑通了建议花点时间把“模拟数据源”替换成你自己手上的真实数据源比如串口、TCP或者一个采集卡SDK。改完那一刻你会觉得Qt做小而美的上位机工具是真的顺手。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Linux C++ 手写 URL 编码与 Base64 编解码:核心原理与防坑指南 2026/9/7 8:33:33

Linux C++ 手写 URL 编码与 Base64 编解码:核心原理与防坑指南

简介:面向Linux平台C开发者,提供URL与Base64编解码的轻量实现,解决自研Web服务器或网络通信模块中的请求转义、参数编码及数据转换问题。压缩包共6个文件、约45KB,包含两个cpp实现文件、一个公共头文件、一个makefile构建脚本&…

阅读更多 →
13.2米双体船S439技术拆解:从参数建模到CCS认证全流程 2026/9/7 8:33:33

13.2米双体船S439技术拆解:从参数建模到CCS认证全流程

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

阅读更多 →
爱迪尔9200t智能锁管理软件实操指南:从连接初始化到故障排查 2026/9/7 8:33:33

爱迪尔9200t智能锁管理软件实操指南:从连接初始化到故障排查

简介:爱迪尔门锁9200t总台制卡软件在酒店、公寓等场景中广泛用于前台发卡。这份恢复资源专门解决因硬盘损坏导致原程序丢失、无法补制门卡的问题,适合门锁系统运维或前台管理人员使用。压缩包共19个文件、约4.94MB,内含主安装程序、9个系统驱…

阅读更多 →
Claude Code插件精选:9款实战验证的效率增强工具 2026/9/7 8:33:33

Claude Code插件精选:9款实战验证的效率增强工具

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

阅读更多 →
Video2X:一条命令把低清视频放大到 4K 的完整指南 2026/9/7 8:33:33

Video2X:一条命令把低清视频放大到 4K 的完整指南

Video2X:一条命令把低清视频放大到 4K 的完整指南 【免费下载链接】video2x A machine learning-based video super resolution and frame interpolation framework. Est. Hack the Valley II, 2018. 项目地址: https://gitcode.com/GitHub_Trending/vi/video2x …

阅读更多 →
基于MyEMS与LSTM的电力负荷预测实战:从数据清洗到95%准确率 2026/9/7 8:30:32

基于MyEMS与LSTM的电力负荷预测实战:从数据清洗到95%准确率

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

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞