十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CuteHMI实战:基于Qt/QML构建工业HMI界面与部署要点

CuteHMI实战:基于Qt/QML构建工业HMI界面与部署要点 简介这是一份CuteHMI开源人机界面框架的完整源码包适用于工业物联网、SCADA监控、树莓派嵌入式设备等开发场景。CuteHMI使用Qt作为基础框架以C和QML混合编写通过Qbs组件将库、插件和可执行程序灵活组合扩展出可复用的HMI模块并支持Windows、Linux、Android等多平台运行。源码包共1517个文件压缩后仅3.38MB其中包含225个C头文件、199个C源代码文件、125个QML界面定义、112个Qbs工程脚本同时还有丰富的SVG图形资源、SQL数据库脚本、许可证与开发文档目录结构清晰便于按功能模块检索帮助理解大型Qt项目的构建流程和模块依赖关系。目前已有2312人学习此资源。借助这套代码开发者可以学习如何自定义工业HMI界面、对接GPIO与SQL数据库并在多个平台完成部署涵盖底层硬件控制到上层界面显示的完整链路对想进入IIoT和SCADA领域的C工程师很有参考价值。1. CuteHMI 是什么在 Qt 生态里做 HMI 为什么绕不开它做设备的人都有一种焦虑PLC 选型定了通信协议定了最后卡在“人机界面”上。买商业组态软件一台一套授权费用高用 QWidget 硬写现场屏幕小、分辨率乱做出来又总带着一股桌面软件的土味。CuteHMI 给出了另一条路它把 Qt 现成的 C 业务能力和 QML 的渲染能力重新组织成一套面向工业 HMI 的框架。你不必再自己造“设备变量→画面控件”之间的轮子而是把精力放在状态机、告警、量程映射这些真正影响生产安全的事情上。这篇文章直接从 CuteHMI 的架构讲起然后带你在本地把最小工程拉起来最后给出界面交互和发布部署时最容易踩的坑。适合已经用过 Qt、想进入 HMI/SCADA 方向的 C 工程师以及正在选型替代商业组态软件的技术负责人。2. CuteHMI 的核心架构C 负责逻辑QML 负责画面2.1 CuteHMI 的模块划分从 CuteHMI::Base 到 CuteHMI::QMLCuteHMI 不是一个“双击安装”的软件而是一组可以按需链接的 Qt 库。你得先从模块图里找到自己在哪一层干活。按照常见划分它至少有四个层次数据基础层、设备通信层、界面表现层和应用脚手架。CuteHMI::Base负责定义数据标签Tag和值类型一个 Tag 代表一个可读可写的数值点后续会绑定到 PLC 寄存器或者 Modbus 地址。CuteHMI::Modbus这类模块处理具体协议轮询和字节序转换对上层隐藏“发报文、收响应”的细节。CuteHMI::QML则负责把 Tag 注册到 QML 运行时让前端脚本能以很自然的方式读写数值。最外面通常还有一个应用启动器负责加载主界面、初始化模块和退出清理。常见模块职责你会接触到的 APICuteHMI::Base数据标签、值类型、错误码Tag::value、Tag::setValueCuteHMI::Modbus协议轮询、寄存器映射ModbusDevice、Tag地址配置CuteHMI::QML向 QML 暴露数据对象QMLContext、Tag注册应用脚手架启动、关闭、异常捕获CuteHMI::Application这种分层的直接收益是设备未到位时你可以先用一个“模拟数据源”把界面和交互做完。真到了现场换掉数据源模块QML 完全不用改。2.2 C 对象如何暴露给 QML 使用HMI 的第一个核心动作是“把设备数据变成界面属性”。CuteHMI 的做法和我们写普通 Qt/QML 混合工程一致先定义一个带Q_OBJECT的类用Q_PROPERTY声明可被 QML 访问的属性然后用 NOTIFY 信号通知前端值变了。下面是一个最小数据类简单模拟电压和电流两个模拟量#include QObject #include QtMath class MachineData : public QObject { Q_OBJECT Q_PROPERTY(double voltage READ voltage WRITE setVoltage NOTIFY voltageChanged) Q_PROPERTY(double current READ current WRITE setCurrent NOTIFY currentChanged) public: explicit MachineData(QObject *parent nullptr) : QObject(parent) {} double voltage() const { return m_voltage; } double current() const { return m_current; } public slots: void setVoltage(double v) { if (qFuzzyCompare(m_voltage, v)) return; m_voltage v; emit voltageChanged(); // 触发 QML 重新求值 } void setCurrent(double c) { if (qFuzzyCompare(m_current, c)) return; m_current c; emit currentChanged(); } signals: void voltageChanged(); void currentChanged(); private: double m_voltage 0.0; double m_current 0.0; };注意setVoltage里先做了相等判断值没变就不发信号。这是 QML 性能优化的关键如果你每个周期无条件 emit界面里的动画和绑定表达式都会跟着空转工业触控屏的 GPU 能力并不强。接下来在main.cpp里把这个对象注入 QML 上下文#include QGuiApplication #include QQmlApplicationEngine #include MachineData.h int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); qmlRegisterTypeMachineData(me.zhang.machine, 1, 0, MachineData); QQmlApplicationEngine engine; MachineData data; engine.rootContext()-setContextProperty(machine, data); engine.load(QUrl(QStringLiteral(qrc:/main.qml))); return app.exec(); }这段代码展示了两种暴露方式qmlRegisterType允许 QML 里直接用MachineData {}创建对象setContextProperty则是把已经存在的 C 实例塞给 QML 全局访问。HMI 工程里绝大部分数据对象应该由 C 持有生命周期所以后者更常见。需要特别注意的是data必须在engine.load()之前创建否则 QML 绑定machine.voltage时会得到 undefined。2.3 属性绑定的数据流设计在 HMI 项目里最忌讳的做法是在 QML 里放一个Timer每 500ms 去读一次 C 属性。这样既浪费时间又会让界面在值未变化时重复刷新。正确路径是C 侧收到新值后更新属性发送 NOTIFY 信号QML 中所有绑定了该属性的表达式自动重新求值。import me.zhang.machine 1.0 Text { text: 当前电压: machine.voltage.toFixed(1) V }如果现场的设备数据来自 Modbus 轮询线程拿到新值后不要在子线程里直接调用setVoltage。要么用QMetaObject::invokeMethod把赋值转到主线程要么通过信号槽连接让槽函数执行在主线程。否则 QML 引擎访问的MachineData对象处于不一致状态轻则界面卡死重则直接崩溃。还有一个容易忽略的点如果同一个槽函数里连续更新voltage和currentQML 里两个 Text 会分别重绘两次。当数据点很多时这一帧的开销会翻倍。常见做法是引入一个“批量提交”接口先修改内部值最后统一 emit 通知或者把两个属性合成一个QVector2D/QPointF减少一次通知。3. 在本地跑通 CuteHMI 的最小构建流程3.1 依赖准备Qt 版本与编译器选择CuteHMI 的根目录依赖 Qt但又不像普通 Qt Widgets 工程那样一个qtbase就能跑。HMI 开发需要 QML 渲染所以至少要安装 Qt Quick 和 Qt Quick Controls 2。如果还要走 Modbus 通信那 Qt SerialBus 模块也必须勾选。推荐使用 Qt 5.15 LTS配合 MSVC2019 64 位Windows或 GCC 9Linux。Qt 6 也可以尝试但 CuteHMI 某些旧版本代码还依赖QtQuick.Controls 2的老写法迁移成本要提前评估。依赖组件用途缺失时的现象Qt QuickQML 引擎和场景图工程无法创建QQuickViewQt Quick Controls 2按钮、进度条、对话框import QtQuick.Controls 2 失败Qt SerialBusModbus 通信栈链接.lib时找不到符号Qt QML Debug界面调试与 ProfilerQML 断点不可用Windows 上比较隐蔽的一个坑是安装 Qt 时没选 QML 调试器运行示例时明明代码没错控制台却报qml: module is not installed。这种情况往往是 Qt 安装目录里缺少qml/QtQuick插件重新补装一次即可。3.2 获取源码并编译 CuteHMI 库CuteHMI 的 GitHub 仓库维护了一份构建脚本也经常有人以镜像方式同步所以拉代码时建议先克隆一份到自己可控的目录git clone https://github.com/CuteHMI/cutehmi.git cd cutehmi git submodule update --init --recursive注意不要直接改仓库里的配置先观察根目录有哪些构建文件。有的 tag 使用 CMake有的使用 QBS有的还保留了.pro。在已安装 QBS 的前提下常见构建命令是qbs setup-toolchains --detect qbs profile:default qbs build -j 4如果不想引入 QBS也可以尝试用 Qt 自带的 qmake 构建但一定要先看 README不同发布形态之间构建方式差别很大。编译完成后输出的动态库和 QML 模块通常在build/install目录。运行示例前需要设置QML2_IMPORT_PATH否则 CuteHMI 的 QML 模块找不到export QML2_IMPORT_PATH$PWD/build/install/qml ./build/install/bin/your_example提示不要在解释器路径上省事。很多人的module not found不是代码问题而是忘了 export 这一行。3.3 用 qmake 或 CMake 创建一个最小 HMI 工程编译整个 CuteHMI 工程只是为了拿到库和 QML 模块。实际写项目时我一般会直接建一个独立 Qt 工程把 CuteHMI 的头文件和库路径指向刚编译好的目录。下面是一个最小.pro文件QT quick quickcontrols2 CONFIG c11 TEMPLATE app TARGET CuteHmiDemo INCLUDEPATH ../cutehmi/SDK/include LIBS -L../cutehmi/build/install/lib -lCuteHMI.Base -lCuteHMI.QML SOURCES main.cpp MachineData.cpp HEADERS MachineData.h RESOURCES qml.qrc构造顺序上LIBS里的库要把最底层的CuteHMI.Base放在最后因为 GCC 链接器按出现顺序解析符号。如果把CuteHMI.QML写在CuteHMI.Base后面链接时大概率报 undefined reference。编译并运行qmake make -j$(nproc) ./CuteHmiDemo如果运行后 QML 报错可以对照排查报错现象原因处理方式Cannot find -lCuteHMI.Base库路径或库名不对检查LIBS路径和实际动态库名称module CuteHMI.QML is not installedQML2_IMPORT_PATH未设置设置环境变量或在 qrc 中导入QT_QPA_PLATFORM_PLUGIN_PATH警告找不到qwindows.dll发布时拷贝 platform 插件4. 用 QML 搭建 HMI 界面控件、状态与设备交互4.1 用 Qt Quick Controls 2 做基础面板HMI 界面不像手机 App 那样追求视觉复杂度它要求的是“状态清晰、操作防呆”。例如“启动”按钮在设备运行时要置灰“停止”按钮必须在安全条件下才可点击。这些逻辑如果写在 C 侧QML 只做渲染代码会非常清晰。下面是一个简单控制面板import QtQuick 2.15 import QtQuick.Controls 2.15 ApplicationWindow { id: root width: 800; height: 480 visible: true title: qsTr(设备控制面板) Column { spacing: 12 anchors.centerIn: parent ProgressBar { id: speedProgress width: 320 from: 0 to: 1000 value: machine.voltage * 10 // 示例映射真实场景请用设备转换函数 } Row { spacing: 20 Button { text: qsTr(启动) enabled: !machine.isRunning onClicked: machine.start() } Button { text: qsTr(停止) enabled: machine.isRunning onClicked: machine.stop() } } } }控件本身并不复杂难点在绑定关系。value: machine.voltage * 10只是一种演示写法真实项目里量程转换应该在 C 侧完成比如把变送器电流 4mA~20mA 映射成 0~1000 的整形值。这样 QML 里就只有显示不掺算法。控件工业场景关键绑定Button启停、复位enabled绑设备状态ProgressBar转速、料位value绑模拟量ComboBox配方选择currentIndex绑配方编号现场经常遇到的问题是一个按钮点击后设备没反应但不报错。先别急着查 QML正确做法是让machine.start()返回一个请求结果然后通过信号传给 QML 弹窗。不要在onClicked里假设调用成功。4.2 用状态机表达设备启停与告警很多 HMI 代码一开始只用bool isRunning后来发现设备还有“正在启动”“正在停止”“故障复位中”这些中间状态。等到告警和连锁条件加进来if/else已经没法维护了。正确做法是用显式状态机。下面是一个 QML 内的状态机示例import QtQuick 2.15 Item { id: machine state: STOPPED states: [ State { name: STOPPED; PropertyChanges { target: indicator; color: gray } }, State { name: RUNNING; PropertyChanges { target: indicator; color: green } }, State { name: ALARM; PropertyChanges { target: indicator; color: red } } ] Rectangle { id: indicator width: 48; height: 48 radius: 24 } function setState(s) { if (s RUNNING machine.state ! RUNNING) { machine.state RUNNING } else if (s ALARM) { machine.state ALARM } } }这里把颜色和状态绑在一起外部只需要调用setState(RUNNING)。状态切换之间的进入/退出动作可以继续加在State的PropertyChanges里。更复杂的情况建议直接在 C 中使用QStateMachine把“启动条件不满足”这类逻辑放在转换的守卫条件中QML 只负责接收当前状态名称。4.3 用 QML 动画提升信息感知度设备告警时工人很可能没有盯着界面所以视觉上的闪烁和颜色变化比数值本身更重要。Qt Quick 里的SequentialAnimation很适合做这种效果。Rectangle { id: alarmLamp width: 60; height: 30 radius: 6 color: machine.alarm ? red : transparent SequentialAnimation on color { loops: Animation.Infinite running: machine.alarm ColorAnimation { from: red; to: darkred; duration: 400 } ColorAnimation { from: darkred; to: red; duration: 400 } } }running: machine.alarm保证了动画只在告警时运行。使用 QML Profiler 可以观察到动画关闭时这一帧的渲染时间明显更短。HMI 设备常年开机任何不必要的动画都会降低整机寿命。4.4 无边框窗口拖动缩放的工业 HMI 适配不少触控屏会要求隐藏系统标题栏把整个窗口铺满屏幕。做法是设置flags: Qt.FramelessWindowHint然后自己画标题栏。注意如果鼠标事件处理不当会导致按钮点击失效。通常只让标题栏区域的MouseArea参与拖动ApplicationWindow { id: root flags: Qt.FramelessWindowHint width: 1024; height: 600 visible: true Rectangle { id: titleBar height: 40 width: parent.width color: #333 MouseArea { anchors.fill: parent property point lastPoint: Qt.point(0, 0) onPressed: lastPoint Qt.point(mouseX, mouseY) onPositionChanged: { root.x mouseX - lastPoint.x root.y mouseY - lastPoint.y } } } }代码中使用lastPoint记录按下位置每次移动只加偏移量。如果不保存上一次坐标而是直接用mouse.x赋值窗口会越拖越快。触摸屏的坐标事件规律和鼠标不完全一样调试时最好在真机上测。5. 给 CuteHMI 做国际化与部署收尾5.1 用 qsTr 和 lupdate 提取文案HMI 设备经常要同时支持中文、英文界面。CuteHMI 工程同样是标准 Qt 工程国际化流程可以复用整套工具。界面文本统一用qsTr()包裹然后在工程根目录执行lupdate -recursive . -ts qml_zh_CN.ts lrelease qml_zh_CN.ts -qm qml_zh_CN.qmlupdate会扫描 QML 和 C 源码中的翻译条目生成.ts文件。用 Qt Linguist 打开它逐条翻译再lrelease编译成.qm。在main.cpp里加载QTranslator translator; if (translator.load(qml_zh_CN.qm, QApplication::applicationDirPath())) { app.installTranslator(translator); }加载顺序很重要installTranslator必须先于 QML 引擎创建否则界面已用默认语言实例化切换翻译不会立即生效。不要在 C 中拼接用户可见字符串而应用tr()否则无法被提取。5.2 发布时把 QML 插件和 Qt 库带全工控机现场通常没有 Qt 开发环境所以发布时必须拷贝运行库、QML 插件和 CuteHMI 自己的模块。Windows 下使用windeployqtwindeployqt.exe CuteHmiDemo.exe --qml-dir ./qml --release--qml-dir用于指定源 QML 目录让工具扫描 import 依赖。CuteHMI 自定义的 QML 模块比如CuteHMI.QML不会被自动识别需要手动把build/install/qml下的 CuteHMI 目录整体复制到发布目录的qml子目录中。验证发布包时不要只看 exe 能不能启动至少检查platforms、styles和qml三个目录是否存在。缺少platforms/qwindows.dll时会直接报could not find or load the Qt platform plugin缺少styles会导致使用 Qt Quick Controls 2 时窗口无边框。用dumpbin /dependents检查动态库依赖比逐个双击试错更快。本文还有配套的精品资源点击获取
返回列表