news 2026/9/10 0:36:58

Qt混合架构实战:Widgets+Quick实现信号采集与可视化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Qt混合架构实战:Widgets+Quick实现信号采集与可视化

简介:《QT和QT quick实战》配套源码包以 Qt 框架与 Qt Quick/QML 为主线,面向正在学习 C++ 桌面开发、希望掌握跨平台 GUI 与移动界面开发的入门及中级开发者。源码包共 535 个文件,压缩包大小为 58.53MB;其中 79 个 cpp、55 个 h 对应 Widget 与业务逻辑代码,67 个 qml、59 个 qmlproject 说明 Qt Quick 界面结构与工程组织,26 个 ui 为窗体设计文件,21 个 qrc 管理图标和资源,29 个 pro 用于 Qt Creator 项目构建;png、mp3、pdf 等素材则方便直接查看效果与原理说明。已有 2501 人学习下载。内容覆盖信号与槽机制、QML 组件自定义、动画与状态机、网络通信、数据库访问、多平台适配等模块;配合书中章节源码,可拆解主窗口、数据展示、自定义控件、多媒体应用等典型场景,边运行边修改能理解从传统 Widget 到 Qt Quick 的完整开发路径。源码目录按知识点组织,适合作为系统提升 Qt 实战能力、积累跨平台调试与排错经验的参考。 聊Qt之前先交代一下背景。我最近做了一个桌面端信号采集与可视化的小项目,界面主体用QWidget配合QCustomPlot,设备对接用QSerialPort做串口读取,另外在同一个进程里用Qt Quick写了一个3D场景展示模块,数据从C++层统一推送过去。整套工程基于Qt 5.15.2构建,中间为了把采样数据从时域波形转成频域谱图,还接入了kissfft做FFT运算,最后用windeployqt完成打包发布。

之所以写这篇,是因为几乎每次有人问“Qt到底学Widgets还是学Qt Quick”时,都得从头解释一遍。与其干讲概念,不如直接拿一个能跑通的项目源码来讲:哪些地方用Widgets更省事,哪些地方必须交给Qt Quick,数据流怎么串,打包会遇到什么坑。适合刚接触Qt的开发者、从Python界面转过来的朋友,以及需要做桌面端数据可视化方案的工程师参考。

1. 项目整体设计与技术选型

1.1 为什么选 Qt + Qt Quick 混合方案

很多人一开始会纠结:既然QML做动画和触摸交互很强,是不是所有界面都应该用Qt Quick?反过来,做工具类软件时是否就不用碰QML?

我的实际感受是,两者各管一段最舒服。传统桌面应用里的树形列表、属性表、设备配置页、菜单工具栏,用QWidget那套控件体系开发效率很高,调试也直观;像仪表盘指针动画、3D模型旋转、渐变过渡这类视觉展示,直接用QWidget去写会非常痛苦,而Qt Quick用声明式语法做这些几乎是降维打击。

所以这个项目没有二选一,而是采用混合架构:核心计算、串口读取、文件读写放在C++层,负责数据采集与业务逻辑;表格、配置面板用QWidget承载;状态监控主界面和3D视角展示用Qt Quick嵌入。这样每个模块都落在自己擅长的地方,后期维护也不至于在一堆自定义绘图代码里迷失。

1.2 源码目录与模块边界

工程结构我习惯按功能拆目录,而不是按语言拆。因为项目同时存在C++和QML,如果按“cpp目录”“qml目录”去分,一段时间后你根本不知道哪个QML文件对应哪个业务模块。下面是一个精简后的目录参考:

project/ ├── CMakeLists.txt ├── src/ │ ├── core/ │ │ ├── serialreader.h │ │ ├── serialreader.cpp │ │ ├── fftworker.h │ │ └── fftworker.cpp │ ├── ui/ │ │ ├── mainwindow.h │ │ ├── mainwindow.cpp │ │ └── plotwidget.cpp │ ├── quick/ │ │ ├── scenebridge.h │ │ ├── scenebridge.cpp │ │ └── qml/ │ │ ├── SceneView.qml │ │ └── DashboardPanel.qml │ └── main.cpp └── resources/ ├── models/watertank.glb └── icons/

这里有一个容易忽略的点:C++和QML的桥接类我单独放到quick目录,而不是塞进core。因为bridge类里有大量Q_PROPERTY和Q_INVOKABLE,它的生命周期与QML引擎强相关,分离出来后,core模块可以保持纯C++,测试和复用都很方便。

2. 数据采集与时频域变换落地

2.1 串口数据怎么接进工程

设备端通过串口持续输出采样值,格式是若干通道的浮点数文本,以逗号分隔。我在serialreader里使用QSerialPort的异步读取模式,而不是开线程同步读:

m_serial = new QSerialPort(this); m_serial->setPortName("COM3"); m_serial->setBaudRate(QSerialPort::BaudRate::Baud115200); m_serial->setDataBits(QSerialPort::DataBits::Data8); m_serial->setParity(QSerialPort::Parity::NoParity); m_serial->setStopBits(QSerialPort::StopBits::OneStop); if (m_serial->open(QIODevice::ReadWrite)) { connect(m_serial, &QSerialPort::readyRead, this, &SerialReader::onReadyRead); }

关键点在于onReadyRead里不要直接解析整块数据——串口数据会分帧到达,一包数据可能只收到一半。正确的做法是维护一个QByteArray缓冲区,每次都把新数据追加进去,然后按换行符切出完整数据行去处理,不完整的数据留在缓冲区等下一批。

还有一个细节:115200波特率下,如果主界面做重绘或者FFT计算,QTcpSocket或者QSerialPort的回调仍然在主线程触发,计算耗时会造成界面卡顿甚至数据丢失。我的处理是把FFT运算放到一个QThread子线程,串口回调只做缓存和信号通知,线程拿到数据后再处理。

2.2 kissfft做时域到频域转换

频谱显示采样的是FFT,我选择了kissfft而不是FFTW。原因很实际:kissfft是单个头文件加少量源码文件,静态编译体积小,也没有FFTW那种license和动态库的烦恼。

使用流程很简单,先把时域波形数据填充到复数数组,实部是采样值,虚部置零,然后调用kiss_fft得到频域复数结果,最后对每个频率点取模并做归一化:

#include "kiss_fft.h" #include <vector> #include <cmath> std::vector<double> computeSpectrum(const std::vector<double>& waveData) { int n = static_cast<int>(waveData.size()); std::vector<kiss_fft_cpx> in(n), out(n); for (int i = 0; i < n; ++i) { in[i].r = waveData[i]; in[i].i = 0.0; } kiss_fft_cfg cfg = kiss_fft_alloc(n, 0, nullptr, nullptr); kiss_fft(cfg, in.data(), out.data()); kiss_fft_free(cfg); std::vector<double> amplitude(n / 2); for (int i = 0; i < n / 2; ++i) { amplitude[i] = 2.0 * std::hypot(out[i].r, out[i].i) / n; } return amplitude; }

这里有个新手最容易踩的坑:FFT的输入点数n必须是2的幂。实际采样时,每秒来的点数不固定,所以我会在FFT前设置一个固定大小的环形缓冲区,比如1024点,每次取最近1024个采样点做变换。如果缓冲区还没填满就提示“等待数据积累”,不要硬算,否则频谱图会非常难看。

功率谱和幅度谱取哪个也取决于业务。我做的是振动信号特征频率识别,用的是幅度谱;如果你要对比噪声能量,建议只用实部平方加虚部平方再取平均,得到功率谱密度,才不会因为FFT点数变化导致幅度波动。

2.3 QCustomPlot实时波形与频谱显示

绘图这块我最终选了QCustomPlot而不是Qt Charts。不是因为QChart不好,而是QCustomPlot在大量动态数据刷新时更灵活,而且它允许直接操作QCPGraph的数据指针,不需要每次都复制一份容器。

实时刷新的核心就两句话:更新数据再重绘:

plotWidget->graph(0)->setData(xData, yData); plotWidget->graph(0)->rescaleValueAxis(false); plotWidget->replot();

但实际使用中必须控制刷新频率。如果串口每10ms来一批数据,直接把所有点扔给QCustomPlot重绘,CPU会飙到很高。我的做法是用一个QTimer定时50ms触发刷新,期间把所有新采样点缓存下来,到点一次性更新到曲线上。这样既保证了画面平滑,又把CPU占用降下来了。

时域和频域我开了两个graph,一个显示原始波形,一个显示归一化频谱。频谱图的Y轴用对数刻度会看得更清楚,QCustomPlot里设置如下:

plotSpectrum->yAxis->setScaleType(QCPAxis::stLogarithmic); plotSpectrum->yAxis->setScaleLogBase(10);

连续刷新时建议锁住x轴范围,只让y轴自动缩放,否则曲线会左右乱跳。比如时域图固定显示最近2000个点,频谱图固定显示0到采样率一半的频率范围。

关于绘图方案选择,我做过一个对比,供参考:

方案适合场景性能与维护
QCustomPlot高频实时曲线、自定义交互、需要深度定制轻量,数据量大时仍需按帧刷新
Qt Charts与QWidget快速集成、简单饼图/柱状图API友好,但动态刷新性能一般
Qt Quick Canvas与QML界面统一、动画丰富灵活,但要自己管理渲染与交互

3. 把 Qt Quick 接进 Desktop 应用

3.1 用 QQuickWidget 嵌入 QML

这个项目里,3D场景和状态仪表板都是用Qt Quick写的。嵌入方式我用了QQuickWidget而不是直接在main.cpp里用QQmlApplicationEngine加载整个界面。

原因是我还需要保留QMainWindow的菜单栏、工具栏和传统对话框,Qt Quick整体接管界面反而麻烦。QQuickWidget可以理解成QWidget家族里专门用来托管QML内容的容器,把它setCentralWidget或者塞进布局都行。

auto *quickWidget = new QQuickWidget; quickWidget->setSource(QUrl("qrc:/quick/qml/SceneView.qml")); quickWidget->setResizeMode(QQuickWidget::SizeRootObjectToView); setCentralWidget(quickWidget);

setResizeMode很关键。如果不设置,QML的根对象不会跟随窗口变化,拉伸窗口时内容就会变形或者留白。SizeRootObjectToView的意思是让QML根对象始终填满容器,配合anchors.fill可以让内部元素自适应。

3.2 C++ 对象暴露给 QML 的推荐姿势

C++数据要进QML,常规做法是通过qmlRegisterType注册一个可实例化类型,或者用setContextProperty注入一个单例对象。两者我都试过,实际项目更推荐qmlRegisterType配Q_PROPERTY,因为它在QML里可以多实例化,还能在某个窗口只绑定自己需要的实例。

我在scenebridge里暴露了一个Q_PROPERTY用于显示当前采样率和旋转角度:

class SceneBridge : public QObject { Q_OBJECT Q_PROPERTY(double sampleRate READ sampleRate NOTIFY sampleRateChanged) Q_PROPERTY(bool isRunning READ isRunning NOTIFY isRunningChanged) public: Q_INVOKABLE void start(); Q_INVOKABLE void stop(); signals: void newFrequencyData(QVariantList points); ... };

QML里直接这样用:

import QtQml 2.15 Text { text: bridge.sampleRate.toFixed(1) + " Hz" color: bridge.isRunning ? "#33cc66" : "#aaaaaa" } Button { onClicked: bridge.start() }

注意QVariantList在跨线程传大量浮点数据时性能不理想,如果需要每帧传几千个点,建议换成QVector 并注册为Q_DECLARE_METATYPE,或者直接传递QByteArray再在QML侧用TypedArray解析。我最初偷懒用QVariantList,刷新频率一高,QML侧明显卡顿,换成QByteArray后基本没再出现这个问题。

3.3 Qt Quick 3D 展示 3D 曲线的场景

Qt 5.15里的QtQuick3D模块还处于技术预览阶段,但已经能做一些比较实用的3D展示。我在这里加载了一个glb格式的水箱模型,再用几条曲线表示传感器测点位置。

在5.15中使用QtQuick3D需要在pro/CMake里加模块,并且QML开头要写:

import QtQuick3D 1.15

加载模型和摆放视角用View3D,最简单的代码如下:

View3D { anchors.fill: parent camera: PerspectiveCamera { id: camera3d position: Qt.vector3d(400, 300, 600) clipNear: 1.0 clipFar: 10000.0 } environment: SceneEnvironment { backgroundMode: SceneEnvironment.Color clearColor: "#121212" } Model { source: "qrc:/models/watertank.glb" scale: Qt.vector3d(1, 1, 1) position: Qt.vector3d(0, 0, 0) } }

Qt Quick 3D的细节不多,但有一个坑值得提:在Qt 5.15上,首次加载glb模型可能不显示,常见原因是没有正确导入材质贴图。glb是自包含格式,理论上不需要外部贴图文件,但部分从Blender导出的glb会引用外部纹理,此时需要手动把纹理拷到资源目录或者在建模导出时勾选“嵌入纹理”。

4. 编译打包与发布

4.1 装好 Qt 5.15.2 之后编译器怎么选

这个项目用的是Qt 5.15.2,属于比较稳妥的LTS版本。安装时除了选择Qt本体,还要选择对应编译器的套件,比如MSVC 2019 64-bit或者MinGW 8.1.0 64-bit。区别很简单:MSVC套件配合Visual Studio使用,能直接调试、性能也不错;MinGW套件则是GCC的Windows版本,轻量,适合用命令行和VSCode混合开发。

我最终选了MSVC 2019,因为项目要用的QCustomPlot和串口库在MSVC下的二进制包最好找,部署到客户机器也方便。如果你前期用MinGW写好代码,后期又要换成MSVC重新编译,经常会在第三方库链接阶段报一堆依赖错误,所以最好一开始就定下来。

下载安装时有条件就选个镜像源,速度会快不少,别非要在官方主源上干等。安装完成后再检查一下是否有安装Qt Quick 3D相关组件,因为默认安装可能不会勾选。

4.2 windeployqt 打包和平台插件坑

项目开发完,第一件事就是打包。Qt发布不能直接把exe拷给客户,那样100%会运行时报“no Qt platform plugin could be initialized”。我第一次看到这个提示时还以为是环境坏了,其实就是缺少platforms目录下的qwindows.dll,以及一系列Qt运行库。

发布流程用windeployqt一把梭:

mkdir deploy copy build\release\yourApp.exe deploy\ cd deploy "C:\Qt\5.15.2\msvc2019_64\bin\windeployqt.exe" yourApp.exe

windeployqt会自动把必需的Qt DLL、插件目录和平台文件拷过来。但有几个情况它不会自动处理:QCustomPlot这种第三方库需要手动拷贝;你自己构建的QML模块和qml目录如果不在标准路径,需要手动放到部署目录;OpenSSL的DLL如果用了HTTPS,windeployqt不一定自动带上,要自己补libcrypto和libssl。

部署完成后,双击exe还会出现黑框一闪而过的话,去cmd里直接跑exe,看具体报错。平台插件问题、缺DLL问题、显卡驱动问题都会在命令行里打印出来,比直接看弹窗有效得多。

4.3 离线环境下的安装补充

有些开发机不能联网,或者客户现场需要离线安装开发环境。Qt官方安装器支持离线包,下载完整离线安装包后直接本地安装,不再需要联网验证。我在Linux环境也装过一份离线Qt,安装到自定义目录后,用cmake指定CMAKE_PREFIX_PATH指向这个目录即可。

离线环境下还容易踩的坑是缺失系统依赖库,比如Linux上缺libxcb-xinerama、libgl1-mesa-dev,QApplication启动时会直接崩溃或者出现平台插件错误。解决办法是按发行版的包管理器补上提示的依赖包,这个没有捷径,只能边试边装。

5. 高频报错排查实录

5.1 平台插件与依赖缺失

打包后运行时崩溃,“could not be initialized”这一类问题,九成都是因为platforms目录缺失,或者Qt安装路径写死在代码里。解决方式很直接:用windeployqt重新部署,并把Qt平台插件目录整个拷过去。

如果你自己写了一个插件,或者团队内部开发了自己平台插件,需要在部署时额外指定路径。windeployqt的参数可以加--qmldir指定QML模块所在目录,这样它能连带把QML依赖的插件也打包进去。否则在客户机器上,QML窗口可能白屏,什么错误提示都没有。

5.2 POST 请求拿不到数据

另一个非常高频的问题是QNetworkAccessManager发送POST请求,服务端一直返回“request method 'post' not supported”,或者客户端怎么都拿不到数据。

这个不一定是Qt的锅,但有一个普遍原因:服务端只接收特定Content-Type,而Qt默认发的可能是application/x-www-form-urlencoded,接口要求application/json或者multipart/form-data没对上。代码里的内容类型要和服务端约好,比如服务端是JSON就把请求头改掉:

request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json; charset=utf-8")); request.setRawHeader("Accept", "application/json");

另一种常见情况是请求被服务端重定向了,重定向后POST被变成GET。QLocalSocket、QtWebApp这些自己搭建的测试服务容易默认301/302重导到另一个URL,导致POST失效。这时抓包看一下响应状态码,比盲改代码快得多。

我之前调试时还发现,POST包在同步阻塞模式下发送,如果网络异常会导致界面假死几秒。更合理的方式是保持异步,用QNetworkReply的finished信号做回调,不要在GUI线程里等待。

5.3 VSCode + Qt Designer 的日常坑

最后聊一下开发工具链路。我主力IDE是Qt Creator,但有时候要在VSCode里改点界面或跑脚本,所以也配了Qt Designer插件。这里有个坑:Qt Designer集成到VSCode后,生成的.ui文件由uic编译,编译时如果找不到头文件,会报类似“dependent '....\allinstall\qt\5.15.2\msvc2019\include\qtwidgets/qapplication' error”的错误。这是include路径配置不全导致的,不是代码本身有问题。

解决办法是在tasks.json或编译命令里显式加上-I指向Qt的include目录,并把mkspecs目录也加上。如果只加了bin目录,编译器还是找不到Qt头文件。另外把.ui文件转成ui_xxx.h这一步,Qt Creator会在左下角自动做,VSCode则需要配置好uic的构建任务,否则改完界面不重新生成,代码里永远用的旧UI。

为了便于快速定位,我整理了一个排查速查表:

错误现象可能原因解决办法
no Qt platform plugin could be initialized缺platforms目录或qwindows.dllwindeployqt重新部署
dependent ‘...\include\qtwidgets...’ errorinclude路径未配置在编译命令加上-I和mkspecs
POST请求无法获取数据Content-Type与接口不匹配按接口要求设置请求头
request method 'post' not supported服务端只支持GET或重定向检查路由、抓包看状态码
程序运行后瞬间崩溃空指针或对象提前释放用QPointer保护、确认父对象
串口数据帧错乱缓冲区没有按完整帧切分按换行符切割后再解析
Qt Quick窗口白屏QML模块没有打包或插件缺失windeployqt加--qmldir

我实际遇到最坑的一次,是QML的ContextProperty在窗口关闭后还被C++侧访问,导致应用随机崩溃。后面我改成在窗口销毁前先移除ContextProperty,并给纯C++对象设置QObject父对象,让生命周期和QML引擎保持一致,崩溃再没出现过。

这个项目做完后,我个人的体会是Qt这套东西其实不难,难的是把Widgets、Qt Quick、第三方库、打包发布整合成一条稳定可复用流水线。如果只学界面语法,不去处理线程调度、数据缓冲和部署细节,写出来的Demo永远停在“能打开一个小窗口”的阶段。源码结构上还有一个很实用的扩展方向:把FFT模块单独抽成动态库,后续接其他信号处理算法时可以直接替换数据源,界面层完全不用动。如果你也在用Qt做类似的桌面可视化工具,建议先花一晚上把数据流和对象生命周期理清楚,再开始写界面,后面能省下不少调试时间。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/10 0:32:19

实时信号处理库架构设计与流式算法工程实践

1. 项目定位与整体设计思路 做实时信号处理库这件事&#xff0c;说白了就是解决一个核心矛盾&#xff1a; 信号采进来的速度和处理它的速度必须匹配&#xff0c;否则数据就会堆积、丢帧&#xff0c;整个系统就失去“实时”的意义 。我自己在做振动监测项目时被这个问题卡过很…

作者头像 李华
网站建设 2026/9/10 0:28:24

COSCon‘25 RISC-V开源论坛深度解读:软件生态加速落地

各位做架构、做编译器、做系统软件的同行&#xff0c;还有关注指令集和开源社区的朋友们&#xff0c;这几天圈里讨论度最高的消息之一&#xff0c;应该就是 COSCon‘25 的 RISC-V 开源论坛议程正式放出来了。作为从 ARM 时代一路看到 RISC-V 在国内落地的人&#xff0c;我第一时…

作者头像 李华