工业用C 前端框架对标QT UI,30 分钟做一个反应釜监控画面整个我写了博客园 大家也可以去关注下 https://www.cnblogs.com/renyitech用 GeeyoouUI 从零写一个真正像样的工控上位机画面三个仪表、一排设备状态灯、一条多通道实时趋势曲线数据来自后台采集线程。做完你会得到这个—— 而且它是能跑的不是效果图 width“880” alt“完成后的反应釜监控画面三个仪表、设备状态面板、实时趋势曲线和操作按钮”这张图和本文所有配图都是代码画出来的。它们由examples/tutorial/shots.cpp离屏渲染生成走的是和真实窗口完全相同的绘制路径。所以图不可能和代码对不上 —— 教程配图最常见的腐烂方式就是代码改了而截图没跟着改。全部代码 100 行出头。没有.ui文件没有moc没有代码生成步骤没有 Qt 授权。读者定位会 C、要做工控 HMI / 上位机的人。不需要任何 GUI 框架经验。目录环境准备3 分钟第一个窗口第一个仪表三个仪表把参数抽成表设备状态灯实时趋势曲线让它动起来接入采集线程换肤一行代码完整代码与下一步1. 环境准备3 分钟只需要装了 C 工作负载的 Visual Studio。CMake、Ninja、MSVC 全部取自 VS 自带不需要任何东西在PATH上。git clone https://github.com/sj7suren/GeeyoouUI.git cd GeeyoouUI build.bat首次构建会通过 CMakeFetchContent拉取 Blend2D 和 AsmJit已钉死 commit可复现大约 2 分钟。VS 如果装在别处改build.bat里的VSROOT。跑一下本教程的成品build\bin\tutorial.exe顺便也可以看看控件全集build\bin\showcase.exe依赖只有两个而且都是 Zlib 协议Blend2DCPU 光栅化和 AsmJit。整条技术栈保持宽松可以静态链进闭源产品没有任何义务。这也是这个库存在的理由。2. 第一个窗口#includegeeyoou/platform/Platform.hpp#includegeeyoou/widget/AppWindow.hppusingnamespacegeeyoou;intmain(){AppWindowwin(反应釜监控,1140,700);win.header()-setIcon(Icon::Settings);win.header()-setTitle(反应釜监控);win.header()-setSubtitle(2# 反应釜 · 在线);win.show();returnplatform().runEventLoop();}跑起来是一个无边框窗口Windows 不再画标题栏、边框和主题色那条标题栏是WindowHeader用和其它控件同一套Painter/Theme画出来的。但它仍然是一个正常的顶层窗口 ——贴边分屏、双击标题栏最大化、AltTab 缩略图、右键系统菜单、拖边框改大小全都还在。因为拖动区是作为「窗口标题区」上报给系统的你一行拖动逻辑都不用写。为什么工控画面要自绘标题栏因为同一套画面要装在几十上百台机器上而那些机器的 Windows 主题、缩放、配色你控制不了。自绘意味着每台机器上像素一致交付时不会出现这台机器上按钮怎么变宽了。3. 第一个仪表工控画面上最常见的东西一个带量程、带预警区的弧形仪表。#includegeeyoou/hmi/Gauge.hppauto*gcontent-addGauge();g-setGeometry({0,0,250,214});g-setRange(0,100);// 量程g-setBands(75,90);// 预警 75、报警 90g-setTitle(进料流量);g-setUnit(m³/h);g-setValue(74.9);注意setBands(75, 90)这一行。阈值是控件的一等公民不是你自己画上去的装饰表盘外圈自动分成 正常 / 预警 / 报警 三段颜色指针和中间那个数字会跟着当前值所处的区间自动变色也就是说这个值现在危险吗这件事不需要你写任何if。下一节的图里温度 163.4 越过了 150 的预警线你会看到数字自己变成橙色。addT()是什么父控件创建并拥有子控件返回裸指针给你用。生命周期跟着父节点走你不需要delete也不该把它塞进unique_ptr。整棵控件树是一个所有权树。4. 三个仪表把参数抽成表流量、温度、压力。与其复制粘贴三遍不如把差异抽出来structGaugeSpec{constchar*title;constchar*unit;doublelo,hi;// 量程doublewarn,alarm;// 阈值doubledemo;};constGaugeSpec kGauges[3]{{进料流量,m³/h,0,100,75,90,74.9},{釜内温度,°C,0,200,150,180,163.4},{系统压力,MPa,0,10,7,8.5,6.1},};for(inti0;i3;i){constGaugeSpecskGauges[i];auto*gcontent-addGauge();g-setGeometry({float(i)*266.0f,0,250,214});// 横向排开g-setRange(s.lo,s.hi);g-setBands(s.warn,s.alarm);g-setTitle(s.title);g-setUnit(s.unit);g-setValue(s.demo);}温度那个仪表现在是橙色的 —— 163.4 越过了 150 的预警线仪表自己做的判断。等一下坐标是写死的是的而且这是故意的。组态画面PID、罐区图上泵画在阀左边 40 像素是工艺语义不是排版。一个把它们重新流式排列的布局引擎等于把信息毁掉了。做过组态的人都知道画面是按固定分辨率作图的。但如果你要的是能自适应的参数表单、设置页GeeyoouUI也有布局引擎BoxLayout/GridLayout支持 stretch 权重、min/max 钳制、跨列。两种写法都是一等公民用哪种取决于这块界面的性质。详见项目 README 的「布局」一节。5. 设备状态灯一个带标题的容器装五盏指示灯#includegeeyoou/hmi/StatusLed.hpp#includegeeyoou/widget/GroupBox.hppauto*panelcontent-addGroupBox();panel-setGeometry({798,0,274,214});panel-setTitle(设备状态);structLedSpec{constchar*caption;StatusLed::State state;};constLedSpec kLeds[5]{{进料泵 P-101,StatusLed::State::Ok},{加热器 H-201,StatusLed::State::Warn},// 温度已超预警线{泄压阀 V-303,StatusLed::State::Off},{超压报警,StatusLed::State::Off},{Modbus 通讯,StatusLed::State::Ok},};for(inti0;i5;i){auto*ledpanel-addStatusLed();led-setGeometry({14,44.0ffloat(i)*32.0f,246,28});led-setCaption(kLeds[i].caption);led-setState(kLeds[i].state);}StatusLed只有四个状态Off/Ok/Warn/Alarm。报警灯还可以setBlinkOnAlarm(true)让它在报警时闪烁。报警色永远不跟着主题走。GeeyoouUI 支持换肤一个主题色能带动整套配色。但ok/warn/alarm这三个 token被刻意排除在外绿色永远是正常红色永远是报警。换个皮肤就把报警变成蓝色这在控制室里不是审美问题是安全问题。GroupBox还有一个很实用的性质禁用它就禁用整棵子树。做未登录/无权限时整块参数区置灰这类联锁一行就够了。6. 实时趋势曲线#includegeeyoou/hmi/TrendChart.hpp#includegeeyoou/render/Theme.hppconstThemethTheme::current();auto*chartcontent-addTrendChart();chart-setGeometry({0,230,1072,330});chart-setTitle(实时趋势);chart-setYRange(0,200);chart-setGridDivisions(8,4);chart-addChannel(流量,th.accent,900);// 900 保留最近 900 点chart-addChannel(温度,th.warn,900);chart-addChannel(压力×20,th.ok,900);之后每来一组数据就推一次constfloatv[3]{float(flow),float(temp),float(press*20.0)};chart-pushAll(v,3);再加上两个操作按钮和一个状态栏完整画面就成型了auto*startcontent-addPushButton();start-setGeometry({0,576,120,38});start-setText(启动);start-setVariant(ButtonVariant::Success);// 绿色auto*stopcontent-addPushButton();stop-setGeometry({132,576,120,38});stop-setText(停止);stop-setVariant(ButtonVariant::Danger);// 红色addChannel的第三个参数 900 是容量不是上限。每个通道是一个固定容量的环形缓冲第 901 个点进来时第 1 个点被覆盖不发生任何内存分配。这在工控场景不是优化癖好是硬需求。一个上位机要在工控机上无人值守跑几个月任何每秒分配一点点的设计最后都会变成运维事故。库里所有热路径都遵守这条。7. 让它动起来接入采集线程到这里画面是静态的。真实系统里数据来自 Modbus / OPC / 串口的采集线程。GeeyoouUI 的答案是DataHub#includegeeyoou/hmi/DataHub.hppDataHub hub{4096};// 有界队列constintchFlowhub.addChannel(进料流量,m³/h);constintchTemphub.addChannel(釜内温度,°C);constintchPresshub.addChannel(系统压力,MPa);采集线程只做一件事 —— 往里推std::threadworker([]{while(running.load(std::memory_order_relaxed)){conststd::uint64_ttsnowMs();hub.push(chFlow,readModbus(40020),ts);hub.push(chTemp,readModbus(40001),ts);hub.push(chPress,readModbus(40010),ts);std::this_thread::sleep_for(std::chrono::milliseconds(20));}});UI 线程定时把队列排空然后刷新控件win.enableAnimations(30);// 30 fps 动画时钟platform().startTimer(66,[]{hub.drain();// 把采集线程推进来的数据取出constdoubleflowhub.lastValue(chFlow);constdoubletemphub.lastValue(chTemp);constdoublepresshub.lastValue(chPress);flowGauge-setValue(flow);tempGauge-setValue(temp);pressGauge-setValue(press);// 阈值联动温度超 150 加热器转预警压力超 8 触发报警灯ledHeater-setState(temp150.0?StatusLed::State::Warn:StatusLed::State::Ok);ledAlarm-setState(press8.0?StatusLed::State::Alarm:StatusLed::State::Off);constfloatv[3]{float(flow),float(temp),float(press*20.0)};chart-pushAll(v,3);});8. 换肤一行代码#includegeeyoou/render/Skin.hppskins().apply(light);// 内置dark / light / contrast / amberdarklight同一份代码一个控件都没有收到通知。因为库里所有控件的取色都是每次绘制时现取Theme::current()所以换肤不需要逐控件刷新整窗重绘一次就完事了。还可以只换主题色skins().setAccent(Color::rgb(0x12,0xC2,0xC2));// 一个颜色带动整套配色它只动派生自品牌色的 tokenaccent / primary / 焦点框 / 选区底色以及填充按钮上的字色 —— 按亮度自动选黑或白。ok/warn/alarm不动理由见第 5 节。如果 token 表达不了 —— 比如「这个急停键要方角而且要它自己的红」—— 还有一层类 QSS 的样式表#emergencyStop { accent: #FF3B30; border-radius: 2; border-width: 2; } PushButton:hover { border-color: accent; } /* token 引用当前主题 */9. 完整代码与下一步本教程的完整可运行代码在仓库里examples/tutorial/ tutorial.cpp 成品程序就是你上面跑的 tutorial.exe ReactorScreen.cpp 四个步骤的画面构建代码 shots.cpp 本文所有配图的生成器build\bin\tutorial.exe :: 跑成品接下来可以看什么想了解去哪全部 30 控件长什么样build\bin\showcase.exe18 个页面表格冻结列 / 合并行 / 20 万行虚拟滚动showcase 的「表格」分组三维设备视图软件渲染无 GPUshowcase 的「设备三维视图」为什么这样设计docs/architecture.md想参与CONTRIBUTING.md目前的限制先说清楚只有 Windows。平台层是 21 个纯虚函数分属两个接口X11 / Cocoa 范围划清了但还没开工。平台层以上完全没有 Windows 依赖 —— 如果你想做这个移植这是个边界清晰、有参考实现可抄的活。没有无障碍UIA。IME 没做内联预编辑候选窗跟随光标能用但不够精致。pre-1.0。如果这个教程帮你省下了一份 Qt 授权⭐ 给项目点个 star—— 这是最省事的表态方式。github.com/sj7suren/GeeyoouUI