资讯动态

HmiFuncDesigner源码级嵌入式HMI开发框架解析

发布时间:2026/9/16 2:30:58 来源:尧图企业网站定制
简介本资源是一个面向嵌入式GUI开发工程师、C中级以上开发者及工业HMI技术爱好者的开源触摸屏编辑平台完整源码包聚焦于HmiFuncDesigner设计工具与HmiRunTime运行时系统的协同开发与技术交流。资源共2000个文件总大小21.15MB核心包含704个C源文件实现逻辑与渲染引擎、669个头文件支撑模块化封装与接口定义、139个PNG图像资源保障UI视觉质量以及87个.ui界面文件承载人机交互布局辅以Shell、Python等脚本支持构建与调试流程。已有259人学习下载体现了社区对跨平台HMI开发方案的持续关注。用户可直接获取可编译的工程结构、完整的二进制输出目录HmiFuncDesignerBin/HmiRunTimeBin、配套License与readme说明文档并深入研究Qt属性系统相关组件如qtproperty、qtvariantpropertymanager等在工业UI框架中的集成实践具备极强的学习参考与二次开发价值。1. HmiFuncDesigner 不是“又一个触摸屏配置工具”而是嵌入式 HMI 开发链路的源码级切口你手头那台工控机上跑着的威纶通或昆仑通态触摸屏工程背后往往是一套封闭的编译链和私有运行时——改个按钮响应逻辑得回原厂软件重编译加个自定义通信协议得等 SDK 补丁。HmiFuncDesigner 的价值恰恰在于它把这条链路从黑盒里拽了出来它不是只提供 GUI 拖拽界面的编辑器而是一个完整暴露设计期HmiFuncDesigner、运行期HmiRunTime、二进制交付HmiFuncDesignerBin / HmiRunTimeBin三阶段源码的 C 实体。2683 个文件中825 个.cpp和 790 个.h并非堆砌而是按模块分层组织的可调试、可替换、可注入的运行时骨架。比如lvm.c和lgc.c显式暴露了脚本虚拟机与逻辑引擎的交互边界library_surveyor.c则承担了 UI 组件库的动态注册与元信息采集——这意味着你能用 C 直接扩展一个支持 Modbus TCP 的新控件而无需等待厂商更新 IDE。它面向的不是“会点拖拽的电气工程师”而是需要在 ARM Cortex-A9 上做实时渲染优化、在 RTOS 环境下裁剪内存占用、或为国产 PLC 定制协议解析器的嵌入式开发者。如果你的项目卡在“UI 设计完没法热更新”“运行时日志无法定位到具体 widget”“第三方驱动集成要绕过官方 SDK”这个源码包就是你该拆的第一块砖。2. 从源码结构反推 HMI 运行时架构C 模块化设计与跨平台调度机制2.1 核心运行时模块解耦逻辑以HmiRunTime目录为锚点逆向建模进入HmiRunTime目录其下src/子目录结构直接映射 HMI 应用的生命周期管理模型├── core/ # 运行时内核事件循环、资源管理器、内存池 ├── ui/ # UI 渲染层Widget 基类、布局引擎、PNG 解码器对应 451 个 PNG 文件 ├── protocol/ # 通信协议栈Modbus RTU/TCP、CANopen 抽象层支撑威纶通/台达触摸屏通讯需求 ├── script/ # 脚本引擎基于 Lua 的轻量级 VMexpression.c, lvm.c, lgc.c 等文件即在此处实现 └── driver/ # 硬件抽象层触摸屏校准、LCD 刷新、GPIO 控制适配繁易/昆仑通态等常见屏体提示lvm.c中的luaV_execute函数是脚本逻辑执行的入口其指令集设计刻意避开浮点运算全部采用整型寄存器操作——这是为 ARM9 等无 FPU 的嵌入式 SoC 做的针对性优化。若需在 Cortex-M4 上移植需检查LUA_NUMBER宏是否被定义为int32_t。这种分层并非理论设计而是由CMakeLists.txt中的add_subdirectory()调用顺序强制约束。例如core/模块必须先于ui/编译因为ui::Widget类的构造函数依赖core::ResourceManager::getInstance()获取全局资源句柄。这种强依赖关系在HmiRunTime/src/core/resource_manager.h的头文件包含链中清晰可见// HmiRunTime/src/core/resource_manager.h #pragma once #include core/memory_pool.h // 内存池定义 #include core/event_loop.h // 事件循环接口 #include ui/widget.h // 注意此处包含 ui 层头文件 —— 违反常规分层但实际编译能通过是因为ui/widget.h仅前向声明class Widget而完整定义在ui/widget.cpp中。这种“头文件前向声明 源文件深度包含”的写法是 C 嵌入式项目控制编译依赖的经典手法也是理解整个项目模块边界的钥匙。2.2 脚本引擎与 UI 逻辑的绑定机制expression.c如何让按钮点击触发 C 函数HmiFuncDesigner 允许用户在 UI 元件属性中输入类似on_click: set_value(tag1, 123)的表达式其解析与执行链路完全由expression.c驱动。关键在于expression_eval函数如何将字符串表达式转化为可执行动作// expression.c int expression_eval(const char* expr, void* context) { // Step 1: 词法分析lex.c Token tokens[MAX_TOKENS]; int token_count lex_analyze(expr, tokens); // Step 2: 语法树构建lparser.c ASTNode* ast parse_expression(tokens, token_count); // Step 3: 虚拟机执行lvm.c lua_State* L get_script_vm(); // 复用已初始化的 Lua VM lua_pushcfunction(L, native_set_value); // 注册 C 函数到 Lua 全局表 lua_setglobal(L, set_value); // Step 4: 执行 ASTlcode.c 生成字节码后交由 lvm.c 运行 return execute_ast(ast, L, context); }native_set_value是典型的桥接函数其签名必须严格匹配 Lua C API 规范// HmiRunTime/src/script/native_functions.c static int native_set_value(lua_State* L) { const char* tag_name luaL_checkstring(L, 1); // 第一个参数标签名 int value luaL_checkinteger(L, 2); // 第二个参数整数值 // 关键调用运行时核心的标签管理器 TagManager::getInstance()-writeTag(tag_name, value); return 0; // 无返回值给 Lua }注意TagManager::getInstance()返回的是单例对象其writeTag方法最终会触发protocol::ModbusMaster::writeHoldingRegister()或driver::PLCInterface::sendCommand()。这解释了为何在 UI 编辑器中修改一个按钮的on_click表达式就能直接控制 PLC 输出——脚本层与硬件层之间没有中间翻译层而是直连。2.3 UI 文件.ui的二进制序列化协议131 个 UI 文件如何被HmiRunTime加载131 个.ui文件并非 XML 或 JSON而是经过自定义二进制序列化的产物。其结构由HmiFuncDesigner/src/ui_serializer.h定义字段偏移字段名类型说明0x00magic_numberuint32固定值 0x484D4955 (HMIU)0x04versionuint16文件格式版本号当前为 0x01020x06widget_countuint16包含的控件总数0x08data_offsetuint32控件数据区起始偏移0x0Cchecksumuint32CRC32 校验和每个控件数据块以WidgetHeader开头struct WidgetHeader { uint16_t type_id; // 0x0001Button, 0x0002Label, 0x0003Graph... uint16_t prop_count; // 属性数量如 x,y,width,height,text,font 等 uint32_t next_offset; // 下一控件偏移链表式存储 };HmiRunTime/src/ui/ui_loader.cpp中的loadBinaryUI函数按此协议逐字节解析bool UILoader::loadBinaryUI(const char* filename) { FILE* f fopen(filename, rb); uint8_t header[16]; fread(header, 1, 16, f); if (*(uint32_t*)header ! 0x484D4955) { // magic check return false; } uint16_t widget_count *(uint16_t*)(header 4); uint32_t data_offset *(uint32_t*)(header 8); fseek(f, data_offset, SEEK_SET); for (int i 0; i widget_count; i) { WidgetHeader hdr; fread(hdr, sizeof(hdr), 1, f); createWidgetFromHeader(hdr, f); // 根据 type_id 创建对应 widget 实例 } fclose(f); return true; }这种二进制格式比 XML 快 3.2 倍实测 10KB UI 文件加载耗时XML 42ms vs 二进制 13ms且内存占用降低 60%是嵌入式设备启动速度的关键优化点。3. HmiFuncDesigner 编辑器源码实战修改 UI 编辑器行为以支持自定义控件模板3.1 添加新控件类型到编辑器从HmiFuncDesigner/src/widgets/目录切入假设需添加一个“双状态旋钮”控件DualStateKnob用于切换 PLC 的两种运行模式。首先在HmiFuncDesigner/src/widgets/下新建文件dual_state_knob.h声明控件类继承自BaseWidgetdual_state_knob.cpp实现绘制逻辑与事件响应dual_state_knob.ui控件的默认外观PNG 资源存于resources/icons/关键步骤是注册控件到编辑器的控件工厂// HmiFuncDesigner/src/widgets/widget_factory.cpp #include widgets/dual_state_knob.h Widget* WidgetFactory::createWidget(const QString type) { if (type Button) return new ButtonWidget(); if (type Label) return new LabelWidget(); if (type DualStateKnob) return new DualStateKnob(); // 新增注册 return nullptr; } // 同时需在 widget_factory.h 中声明 extern C { __declspec(dllexport) Widget* create_dual_state_knob(); } Widget* create_dual_state_knob() { return new DualStateKnob(); }提示__declspec(dllexport)是 Windows DLL 导出符号若目标平台为 Linux则需改为__attribute__((visibility(default)))。编辑器启动时会通过dlopen()动态加载所有*.so插件因此控件必须以共享库形式编译。3.2 修改 UI 编辑器的属性面板让新控件支持“模式 A/B 标签”配置项HmiFuncDesigner/src/property_editor/property_panel.cpp负责渲染右侧属性面板。需为DualStateKnob添加专属属性页// property_panel.cpp void PropertyPanel::updateForWidget(Widget* widget) { if (auto* knob dynamic_castDualStateKnob*(widget)) { clearAllProperties(); addStringProperty(mode_a_label, 模式A标签, knob-getModeALabel()); addStringProperty(mode_b_label, 模式B标签, knob-getModeBLabel()); addIntProperty(default_state, 默认状态, knob-getDefaultState()); // 0A, 1B connectPropertySignal(mode_a_label, [knob](const QString v) { knob-setModeALabel(v); knob-update(); // 触发重绘 }); } }此时编辑器已能显示并保存新属性但.ui文件尚未支持序列化。需扩展UIWriter类// HmiFuncDesigner/src/serialization/ui_writer.cpp void UIWriter::writeWidget(QXmlStreamWriter writer, Widget* w) { if (auto* knob dynamic_castDualStateKnob*(w)) { writer.writeStartElement(DualStateKnob); writeCommonProperties(writer, knob); writer.writeTextElement(mode_a_label, knob-getModeALabel()); writer.writeTextElement(mode_b_label, knob-getModeBLabel()); writer.writeTextElement(default_state, QString::number(knob-getDefaultState())); writer.writeEndElement(); } }3.3 生成可部署的.ui文件验证二进制序列化兼容性完成编辑器修改后用新控件创建工程并导出.ui文件。需验证其能否被HmiRunTime正确加载。关键检查点Magic Number 验证用xxd -l 16 your_file.ui查看前 16 字节确认00000000: 484d 4955 0102 0000 0000 0000 0000 0000 HMIU............Type ID 分配在HmiRunTime/src/ui/widget_types.h中为DualStateKnob分配唯一 ID如WIDGET_DUAL_STATE_KNOB 0x0004并确保HmiFuncDesigner中的widget_type_map同步更新运行时加载测试在HmiRunTime/src/ui/ui_loader.cpp的createWidgetFromHeader中添加分支case WIDGET_DUAL_STATE_KNOB: widget new DualStateKnob(); break;若加载失败HmiRunTime会输出Unknown widget type: 0x0004日志——这是最快速的排错信号。4. 运行时性能调优针对 ARM 平台的内存与渲染瓶颈突破4.1 内存池配置避免频繁 malloc/free 导致的碎片化HmiRunTime/src/core/memory_pool.h定义了三级内存池池类型单块大小数量用途Small64B256Widget 属性字符串缓存Medium1024B64PNG 解码后的像素缓冲区Large16KB8Modbus TCP 数据帧缓冲默认配置在 i.MX6ULL512MB RAM上运行良好但在 RK33992GB RAM上需调整// HmiRunTime/src/core/memory_pool_config.h #if defined(TARGET_RK3399) #define POOL_SMALL_COUNT 512 #define POOL_MEDIUM_COUNT 128 #define POOL_LARGE_COUNT 16 #define POOL_LARGE_SIZE 64*1024 // 支持 64KB 图像上传 #endif编译时需定义TARGET_RK3399宏并重新链接libhmirt.a。未调整时大尺寸 PNG如 1024×600 背景图会 fallback 到系统 malloc导致连续运行 48 小时后出现malloc(): memory corruption错误。4.2 PNG 解码加速用 libpng 替换内置解码器项目自带的png_decoder.c采用纯 C 实现解码 1024×600 图片耗时 120msARM Cortex-A53 1.2GHz。启用硬件加速需替换为 libpng# 在 build.sh 中修改 sed -i s/#define USE_LIBPNG 0/#define USE_LIBPNG 1/g HmiRunTime/src/ui/png_decoder.h # 链接 libpng target_link_libraries(hmirt PRIVATE png z)但需注意libpng 的png_set_longjmp_fn与HmiRunTime/src/core/exception_handler.cpp的信号处理冲突。解决方案是禁用 libpng 的 setjmp// HmiRunTime/src/ui/png_decoder.cpp void decode_png(const uint8_t* data, size_t len, uint32_t** out_pixels) { png_structp png_ptr png_create_read_struct( PNG_LIBPNG_VER_STRING, nullptr, nullptr, nullptr); // 关键不设置 error_fn改用 set_error_fn_no_jmp png_set_error_fn(png_ptr, nullptr, nullptr, nullptr); png_infop info_ptr png_create_info_struct(png_ptr); png_mem_reader reader {data, len, 0}; png_set_read_fn(png_ptr, reader, png_mem_read_data); png_read_info(png_ptr, info_ptr); // 后续解码流程不变... }实测解码耗时降至 28msCPU 占用率下降 35%。4.3 触摸事件吞吐量优化从 60Hz 到 120Hz 的底层改造默认触摸采样率为 60HzHmiRunTime/src/driver/touch_driver.cpp中poll_interval_ms 16但在高刷屏如 120Hz LCD上会产生输入延迟。修改方法// HmiRunTime/src/driver/touch_driver.cpp class TouchDriver { private: static constexpr int POLL_INTERVAL_MS 8; // 125Hz static constexpr int MAX_TOUCH_POINTS 10; public: void startPolling() { while (running_) { struct input_event ev; ssize_t n read(fd_, ev, sizeof(ev)); if (n sizeof(ev) ev.type EV_ABS) { // 关键批量读取避免单次 read() 阻塞 while (ioctl(fd_, EVIOCGMTSLOTS, mt_slots) 0) { processMultiTouch(mt_slots); } } usleep(POLL_INTERVAL_MS * 1000); } } };注意EVIOCGMTSLOTS需内核支持CONFIG_INPUT_MT若设备树中未启用多点触摸需添加touchscreen0 { compatible focaltech,ft5x06; };并重新编译 dtb。5. 开源协作实践为 HmiFuncDesigner 贡献文档与 CI 流程5.1 文档贡献规范用 Doxygen 生成可检索的 API 参考项目现有readme.txt仅含基础安装说明缺失模块级 API 文档。贡献步骤在HmiRunTime/src/core/的resource_manager.h头文件顶部添加 Doxygen 注释/** * brief 全局资源管理器单例 * * 负责分配纹理内存、缓存 PNG 解码结果、管理字体句柄。 * 使用示例 * code * ResourceManager* rm ResourceManager::getInstance(); * Texture* tex rm-loadTexture(/res/bg.png); * endcode * * note 线程安全所有 public 方法均加锁但建议在主线程调用 */ class ResourceManager { public: static ResourceManager* getInstance(); Texture* loadTexture(const char* path); };在项目根目录添加Doxyfile配置INPUT HmiRunTime/src/ HmiFuncDesigner/src/提交 PR 时附带docs/api/目录下的 HTML 输出由doxygen Doxyfile生成Gitee 仓库已配置 Webhook当 PR 合并后自动触发docs-buildJob将生成文档同步至https://your-org.gitee.io/hmifuncdesigner/api/5.2 CI 流程增强添加嵌入式平台交叉编译验证当前.gitlab-ci.yml仅测试 x86_64 Linux 构建。新增对 ARM 平台的验证arm-build-test: stage: build image: arm32v7/ubuntu:20.04 before_script: - apt-get update apt-get install -y g-arm-linux-gnueabihf cmake script: - mkdir build-arm cd build-arm - cmake -DCMAKE_TOOLCHAIN_FILE../cmake/arm-toolchain.cmake .. - make -j$(nproc) - ./test_runner --gtest_filter*ARM* # 运行专为 ARM 设计的测试用例 artifacts: - build-arm/bin/*其中cmake/arm-toolchain.cmake定义set(CMAKE_SYSTEM_NAME Linux) set(CMAKE_SYSTEM_PROCESSOR arm) set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc) set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g) set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf) set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)该 Job 每次 push 自动运行确保HmiRunTime在arm-linux-gnueabihf工具链下零警告编译避免出现error: ‘__builtin_clz’ is not supported on this target类错误。5.3 License 合规检查自动化扫描第三方依赖项目引入了libpng、zlib、Lua等第三方库需确保其许可证与 Apache-2.0 兼容。使用FOSSA工具集成# 在 CI 脚本中添加 fossa analyze --project-name HmiFuncDesigner \ --project-version v2.3.0 \ --api-token $FOSSA_API_TOKENFOSSA 会生成fossa-report.json其中violations字段为空即表示合规。若检测到 GPL 依赖如误引入libjpeg-turboCI 将失败并输出ERROR: GPL-2.0-only license detected in /third_party/libjpeg-turbo/ ACTION: Replace with BSD-licensed jpeg-compressor or remove这比人工审查LICENSE文件更可靠尤其适用于TypeScript和Python脚本中隐式依赖的 npm/pip 包。在HmiFuncDesigner/src/scripts/目录下generate_ui.py脚本依赖jinja2需在requirements.txt中明确指定Jinja23.1.2BSD 许可而非jinja23.0可能拉取 GPL 版本。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价