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::vectordouble computeSpectrum(const std::vectordouble waveData) { int n static_castint(waveData.size()); std::vectorkiss_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::vectordouble 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.exewindeployqt会自动把必需的Qt DLL、插件目录和平台文件拷过来。但有几个情况它不会自动处理QCustomPlot这种第三方库需要手动拷贝你自己构建的QML模块和qml目录如果不在标准路径需要手动放到部署目录OpenSSL的DLL如果用了HTTPSwindeployqt不一定自动带上要自己补libcrypto和libssl。部署完成后双击exe还会出现黑框一闪而过的话去cmd里直接跑exe看具体报错。平台插件问题、缺DLL问题、显卡驱动问题都会在命令行里打印出来比直接看弹窗有效得多。4.3 离线环境下的安装补充有些开发机不能联网或者客户现场需要离线安装开发环境。Qt官方安装器支持离线包下载完整离线安装包后直接本地安装不再需要联网验证。我在Linux环境也装过一份离线Qt安装到自定义目录后用cmake指定CMAKE_PREFIX_PATH指向这个目录即可。离线环境下还容易踩的坑是缺失系统依赖库比如Linux上缺libxcb-xinerama、libgl1-mesa-devQApplication启动时会直接崩溃或者出现平台插件错误。解决办法是按发行版的包管理器补上提示的依赖包这个没有捷径只能边试边装。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; charsetutf-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和mkspecsPOST请求无法获取数据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做类似的桌面可视化工具建议先花一晚上把数据流和对象生命周期理清楚再开始写界面后面能省下不少调试时间。本文还有配套的精品资源点击获取