ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用CuteHMI在Qt上搭建开源HMI:从QML界面到C++通信实践

用CuteHMI在Qt上搭建开源HMI:从QML界面到C++通信实践 简介这是一份CuteHMI开源人机界面HMI框架的完整镜像源码包。CuteHMI以Qt与C/QML为核心借助Qbs构建系统将库、插件与可执行程序组合成扩展和工具适用于工业物联网、SCADA、树莓派及多系统桌面环境为具备C/Qt基础的开发者提供了灵活的自定义HMI开发框架可学习如何构建模块化的跨平台人机交互应用。资源包共1517个文件压缩后3.38MB核心内容包括cpp/hpp源码、qml界面文件、qbs工程规则、svg/png图标素材以及sql脚本、md/txt说明文档和Makefile等构建辅助文件目录结构清晰便于按模块阅读和编译。目前已有2312人学习/下载在开源HMI开发者中有一定参考价值。从资源实体来看读者可获得完整的源码树、构建配置、许可声明与分支迭代说明实际项目中可借鉴其扩展机制、Qt与QML的分层设计以及SQL、GPIO等能力的接入方式适合用于学习开源HMI架构和二次开发。1. 用 CuteHMI 在 Qt 上自己搭 HMI比想象中更接近生产环境HMI人机界面在工业现场往往和组态软件绑定在一起授权费、运行库、封闭的数据模型这三件事让很多项目在原型阶段就被拖住。CuteHMI 走的是另一条路以 Qt 库作为框架界面用 QML 声明式编写协议处理和逻辑放在 C 侧整个项目开源GitHub 上的仓库作为镜像同步。这意味着你可以在不购买工控组态授权的前提下用一套成熟的 Qt 技术栈完成设备上位机或产线看板的界面开发。这套方案适合两类人。一类是做设备上位机和产线看板的工程师需要频繁定制画面、对接 PLC 或串口设备另一类是 Qt 开发者想看看 QML 在工业场景里怎么组织状态和事件。接下来的内容按做这类项目时的常见顺序展开先明确 C 与 QML 的边界再跑通一个最小工程然后处理事件、状态和自绘控件最后收在发布与排错上。2. CuteHMI 的技术构成为什么逻辑放 C、界面放 QML2.1 CuteHMI 的模块边界与界面刷新模型CuteHMI 这类开源 HMI 方案的常见组织方式是把项目拆成数据采集、数据模型、界面渲染三层。C 侧负责串口、TCP、现场总线的通信把来自下位机的原始数据统一成带名称、数值、单位和报警状态的变量QML 侧只负责把这些变量画出来不直接碰协议。界面刷新不是靠定时器轮询而是靠 Qt 的属性绑定机制。C 对象通过 Q_PROPERTY 暴露属性属性变化时发出 NOTIFY 信号QML 里所有引用该属性的绑定表达式会自动重算。画面上的颜色、位置、数值因此天然同步不需要手动调用 update()。这套模型和 HMI 的需求高度匹配一个温度值变化可能同时影响文本、进度条颜色和报警灯绑定表达式让这些联动关系直接声明在 QML 里。为什么不干脆用 C 的 QWidget 写界面QWidget 当然也能做但工业画面有大量状态刷色、数字动画、多语言场景QWidget 的刷新粒度较粗改一次样式就得重新编译。QML 的 State、Transition 和 Behavior 让界面逻辑更接近视觉本身调试时可以热重载这对现场频繁改画面的场景非常实用。2.2 从空目录跑通一个 CuteHMI 风格的最小工程搭建环境时我一般选择 Qt 6.5 以上版本配合 CMake编译器用 MSVC 或 GCC 均可。先准备三个文件这是一个不依赖任何外部库也能直接运行的最小骨架后面要接 CuteHMI 时只需要在 target_link_libraries 里追加对应组件。cmake_minimum_required(VERSION 3.16) project(cutehmi_demo VERSION 0.1 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) find_package(Qt6 REQUIRED COMPONENTS Quick QuickControls2) qt_add_executable(cutehmi_demo main.cpp ) qt_add_qml_module(cutehmi_demo URI Demo VERSION 1.0 QML_FILES main.qml ) target_link_libraries(cutehmi_demo PRIVATE Qt6::Quick Qt6::QuickControls2 )#include QGuiApplication #include QQmlApplicationEngine int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QQmlApplicationEngine engine; // CuteHMI 这类库通常在这里完成 QML 模块注册。 // 常见的做法是把通信和数据模型对象注册成 qml 类型 // 供 QML 侧直接 import 后使用。 engine.loadFromModule(Demo, Main); return app.exec(); }import QtQuick import QtQuick.Controls ApplicationWindow { width: 800 height: 480 visible: true title: CuteHMI 最小面板 Rectangle { anchors.centerIn: parent width: 200 height: 80 color: #2E7D32 radius: 4 Text { anchors.centerIn: parent color: white text: qsTr(设备运行中) font.pixelSize: 20 } } }构建顺序是常规的 CMake 流程cmake -S . -B build cmake --build build ./build/cutehmi_demo这里有两个容易忽略的参数。qt_add_qml_module里的URI Demo决定了 QML 侧的 import 名称VERSION 1.0是模块版本engine.loadFromModule(Demo, Main)中的Main对应当前目录下的main.qml文件名首字母大写是 Qt 的约定。如果在 QML 里写了import Demo就必须保证这个 URI 与 CMake 中一致否则运行时报module not found这种错误经常在拷贝旧工程时出现。2.3 裸 Qt 与 CuteHMI 的边界什么时候值得引入下面表格对比了直接用 Qt 写 HMI 与引入 CuteHMI 这类框架的差异方便判断你所在项目的切入点。关注点直接用 Qt 手写使用 CuteHMI 这类方案数据模型自己写 QObject 子类手动管理属性与信号提供统一的数据点与报警对象变化自动通知工程结构画面一多需自己搭导航和状态机按模块组织页面与逻辑QML 与 C 解耦现场通信自己封装串口/TCP 的重连、粘包与解析通信层独立常见协议可替换实现发布与授权全部自有代码无额外授权问题开源许可需要确认但远轻于商业组态我的判断标准很简单如果项目只有一块屏、几个固定页面直接用 QML 写完全够用引入框架反而增加学习成本。当第二个画面出现或者界面状态和通信状态经常对不上时才值得把数据层抽出来。CuteHMI 的价值不是省写代码而是让「数据从下位机到界面显示」这条链路有统一的路径可循。3. 把静态面板变成可操作的 HMI事件、状态与自定义控件3.1 用 C 模型驱动 QML 刷新一个最小数据点一个典型的数据点对象长这样。注意 Q_PROPERTY 的声明方式这是 QML 绑定的基础。#include QObject class DataPoint : public QObject { Q_OBJECT Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(double value READ value WRITE setValue NOTIFY valueChanged) Q_PROPERTY(bool alarm READ alarm NOTIFY alarmChanged) public: explicit DataPoint(const QString name, QObject *parent nullptr) : QObject(parent), m_name(name) {} QString name() const { return m_name; } double value() const { return m_value; } bool alarm() const { return m_alarm; } void setValue(double v) { if (qFuzzyCompare(m_value, v)) return; m_value v; emit valueChanged(); bool newAlarm (v 80.0); if (newAlarm ! m_alarm) { m_alarm newAlarm; emit alarmChanged(); } } signals: void valueChanged(); void alarmChanged(); private: QString m_name; double m_value 0.0; bool m_alarm false; };QML 侧的消费方式很直白Text { text: point.value.toFixed(1) color: point.alarm ? #E53935 : #212121 }当point.value变化时text 自动重算当point.alarm变化时color 自动切换。WRITE setValue让 QML 也能写值但真正的写入通常来自 C 的通信回调而非界面事件。这里的关键是NOTIFY信号没有它QML 引擎不知道属性何时变化绑定就不会刷新这是新手最容易漏掉的一环。3.2 QML 控件点击事件报错后的界面状态恢复QML 里最常见的一类故障是onClicked中访问了尚未创建的属性或对象引擎抛出 TypeError之后界面状态卡死。QML 的脚本异常不会像 C 那样直接崩溃但它可能让绑定失效表现就是按钮点了没反应、数值不再刷新。我习惯把「报错之后的恢复」做成显式的状态复位而不是依赖异常处理。下面的写法把启动动作和复位路径放在一起Button { id: startBtn property bool busy: false onClicked: { if (busy) return busy true enabled false backend.start() // C 侧异步执行 } function reset() { busy false enabled true } Connections { target: backend function onStartFailed(reason) { startBtn.reset() statusText.text 启动失败: reason } function onStarted() { startBtn.reset() statusText.text 已启动 } } }这个模式的要点是把「忙」状态和按钮的 enabled 绑定在一起失败时由信号驱动复位函数。QML 里虽然可以写 try/catch 捕获脚本异常但异常之后的绑定状态未必能完整恢复所以更可靠的做法是用 State 或 Loader 把界面切到安全态。实际现场中「点击报错之后如何恢复」的答案是不要在 onClicked 里堆业务逻辑把它拆成发起动作、等待回调、显式复位三个步骤。3.3 自定义工业控件带报警色的进度条与无边框窗口工业面板上最常见的自绘控件就是进度条加状态色。Qt 自带 ProgressBar 够用但需要同时显示报警色和动画过渡时自绘往往更直接。Rectangle { id: bar width: 300 height: 22 color: #333333 radius: 4 property real progress: 0.0 property bool alarm: false Rectangle { width: bar.width * bar.progress height: parent.height color: bar.alarm ? #E53935 : #43A047 radius: 4 Behavior on width { NumberAnimation { duration: 200 } } } }Behavior on width让进度变化带有 200ms 的平滑过渡这在数值频繁跳变的工业界面上比生硬跳变更容易观察趋势。alarm 属性由 C 侧报警状态驱动颜色切换是绑定的不需要额外代码。如果现场设备需要全屏显示去掉系统边框是常见需求QML 里只需设置flags: Qt.FramelessWindowHint再用一个 MouseArea 记录拖动偏移即可注意不要在拖动时阻塞绑定刷新。3.4 与下位机通信的 C 接入骨架界面就绪后下一步是接数据。下面的骨架描述了一个典型的接收路径通信线程收到字节流解析后以信号发出。class DeviceLink : public QObject { Q_OBJECT public: void onBytes(const QByteArray data) { // 以行协议为例按换行符切包防止粘包 m_buffer.append(data); while (m_buffer.contains(\n)) { int idx m_buffer.indexOf(\n); parseLine(m_buffer.left(idx)); m_buffer.remove(0, idx 1); } } signals: void valueUpdated(const QString key, double value); void linkDown(const QString reason); private: void parseLine(const QByteArray line); QByteArray m_buffer; };QML 侧通过 Connections 接收 valueUpdated更新对应的 DataPoint。注意这里的核心约束字节接收不能阻塞 GUI 线程串口和 TCP 的 read 应该放在工作线程中解析完成后通过信号队列回到主线程更新属性。很多人在这一步直接把 readAll 放在 GUI 线程结果就是快速通信时界面卡顿。现代 Qt 里可以用 QThread 加 moveToThread或者 QtConcurrent 跑收包循环两者都能接受。这个骨架是协议无关的Modbus、CAN 或私有协议都可以套同样的「切包、解析、发信号」流程。4. 发布、验证与三个高频坑4.1 用 windeployqt 收集 QML 运行时处理插件路径开发机上能跑换一台机器就跑不起来这是 Qt 发布最常见的问题。CuteHMI 这类带 QML 模块的项目尤其严重因为 QML import 的模块不会自动打进 exe。Windows 下标准做法是用 windeployqt 收集运行时windeployqt --qmldir . --release build/cutehmi_demo.exe--qmldir指向 QML 源码根目录工具会扫描所有 import 语句并递归复制对应模块--release确保不混入 debug 库。部署后如果目标机仍报qt_qpa_platform_plugin_path相关的 plugin not found 错误先确认 exe 同级目录下是否存在platforms文件夹且包含qwindows.dll。正规做法是让 windeployqt 在 exe 旁生成qt.conf该文件指定 Qt 库和插件目录比手动设置环境变量可靠得多。4.2 用 qmltest 给界面做冒烟验证界面逻辑的回归测试可以用 Qt 自带的 QML Test 框架完成它不属于 CuteHMI但适合验证绑定没有断掉。一个最小冒烟测试import QtQuick import QtQuick.Controls import QtTest Item { Button { id: startBtn text: 启动 } TestCase { name: PanelSmoke function test_start_button() { compare(startBtn.enabled, true) startBtn.clicked() // 这里可以对比 C 侧状态是否被置为 busy } } }运行命令为qmltestrunner -input .也可以挂到 CTest 里作为构建后检查。这个框架对「点击报错之后如何恢复」这类回归很有用先模拟点击再断言控件状态回到预期值能及早发现绑定失效而不是等现场反馈。4.3 镜像仓库、国际化与 QML 模块版本三个坑GitHub 上是镜像仓库意味着同步与上游存在时间差。看 release 和 tag 时以镜像为准问题不大但判断项目是否活跃不要只看镜像的最后提交时间应去上游确认。发布代码时也要注意上游流程不是所有镜像都接受 pull request。国际化方面QML 里的qsTr()依赖翻译文件。带参数的长字符串要拆成多段组合避免整句无法翻译每次新增界面文案后需要重新运行 lupdate 生成新的翻译条目否则现场切语言时会出现大段英文漏网。第三个坑是 QML 模块版本与本机 Qt 版本不一致典型报错是module not found或qml 编译错误。出现这种问题时先检查 qt.conf、QML import 路径以及构建机器与运行机器的 Qt 版本号是否完全一致而不是急于改代码。镜像仓库同步可能有几小时延迟发布前用上游 tag 与镜像 tag 对照一次可以省掉不少「代码明明是最新却编不过」的排查时间。本文还有配套的精品资源点击获取
返回列表