简介:这是一份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++、界面放 QML
2.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 对照一次,可以省掉不少「代码明明是最新却编不过」的排查时间。
本文还有配套的精品资源,点击获取