ARTICLE DETAIL

资讯详情

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

我们写了一个专注工业现场的 C++ 界面库,然后把它开源了

我们写了一个专注工业现场的 C++ 界面库,然后把它开源了 做上位机的人大概都在某个深夜跟同一个东西较过劲。大家有空可以去看看我的博客园全系列 renyi - 博客园不是逻辑不是通讯协议是界面。客户的机柜里塞着一台工控机屏幕分辨率古怪显卡是集成的甚至没有独显Windows 版本停留在某个你不敢升级的补丁而你要在上面画出一个操作员能盯着看八小时不出错的画面。你翻出 Qt写完打包然后发现光是windeployqt拖出来的那一堆 DLL 就比你自己的程序大好几倍。更麻烦的还在后面——授权。你的产品要卖给客户静态链接 Qt 就得面对 GPL/LGPL 那套东西要么给客户留一份重新链接的义务要么掏钱买商业授权按人头按年一直付。这篇文章介绍的库就是我们在这条路上走了几年之后决定自己造的那个轮子。它叫GeeyoouUIMIT 协议专门为工业 HMI 和上位机操作台设计。开源地址在这里先放着你可以边看边翻GitHub - sj7suren/GeeyoouUI: GeeyoouUI: a modern C GUI framework, lightweight alternative to Qt. Cross-platform (Win, Linux, macOS). Offers widgets, layouts, signals, declarative UI, fast rendering. Modular, extensible. For desktop and embedded. · GitHub我不打算把它吹成 Qt 杀手。它不是后面会老老实实讲它现在还做不到什么。但如果你交付的是 Windows 上的工控画面而挡在你面前的正好是那纸授权那这篇值得你读完。先说清楚它是什么一句话:一个 C20 写的、屏幕上每个像素都自己画的控件库。自己画这三个字是理解它的钥匙。它不调用任何 Windows 原生控件,不套系统主题,一个按钮的圆角、一盏指示灯的光晕、一条趋势曲线的抗锯齿,全都是它用 CPU 一个像素一个像素算出来的。底层的光栅化交给 Blend2D——一个带 JIT 的软件渲染器,配上 AsmJit,两个依赖,都是 Zlib 协议,宽松到你可以直接静态链进闭源产品,不欠任何人任何东西。这么做的直接后果,是两条工业现场最在乎的性质:一是跨机器像素一致。同一份画面装到五十台机器上,长得一模一样。不会出现这台机器上按钮怎么变宽了、那台的字怎么糊了这种交付时才发现、现场才能复现的破事。因为它压根不问操作系统要外观。二是不挑显卡。它全程不碰 GPU。这不是什么技术洁癖,是硬需求——工控机十有八九没有能用的独显,一个需要 OpenGL/DirectX 上下文才能启动的界面库,在现场就是装不上。连后面要讲的三维设备视图,都是纯软件渲染的。还有一个你写第一行代码就会注意到的细节:没有 moc。用过 Qt 的都知道那个Q_OBJECT宏和它背后的元对象编译器,构建流程里必须先跑一遍代码生成。GeeyoouUI 的信号槽是拿 C20 的模板加std::function实现的,没有任何代码生成步骤。你和编译器之间是干净的,没有中间人。g-valueChanged.connect([](double v) { // 就这样。没有宏,没有 moc,没有 .pro 文件 });代价当然有——你失去了 Qt 那套按名字取属性的运行时反射,也就没法做 QML 那种动态绑定。但一个工业 HMI 需要 QML 吗?我的答案是不需要,而且这个取舍换来的构建简洁度,天天都在受益。三十行看个大概光说不练没意思。一个带量程、带预警区的仪表,加一扇窗,是这样:#include geeyoou/hmi/Gauge.hpp #include geeyoou/platform/Platform.hpp #include geeyoou/widget/AppWindow.hpp using namespace geeyoou; int main() { AppWindow win(温度监控, 400, 340); // 无边框窗口 自绘标题栏 win.header()-setIcon(Icon::Info); auto* g win.content()-addGauge(); g-setGeometry({20, 20, 360, 260}); g-setRange(0, 200); g-setBands(150, 180); // 预警 150、报警 180 g-setTitle(釜内温度); g-setUnit(°C); g-setValue(163.4); win.show(); return platform().runEventLoop(); }跑起来是一个无边框窗口,标题栏是库自己画的。但注意——它仍然是个正经的顶层窗口,贴边分屏、双击最大化、AltTab 缩略图全都在,因为拖动区是作为窗口标题区上报给系统的。你一行拖动逻辑都不用写。setBands(150, 180)这行我想单独点一下,因为它体现了这个库的一个设计取向:阈值是控件的一等公民,不是你画上去的装饰。设了这行,表盘外圈自动分成正常/预警/报警三段颜色,指针和中间那个数字会跟着当前值所在的区间自动变色。上面这段代码里温度是 163.4,越过了 150,所以你不用写任何if,那个数字自己就是橙色的。这个值现在危险吗——在工业界面里,这个判断出现的频率高到你不该每次都手写。它到底装了些什么三十多个控件,我不逐个念,那样这篇就成了 API 手册。挑几组你在别的轻量库里不太容易凑齐的:领域控件。这是它跟通用 GUI 库拉开距离的地方。仪表(Gauge)、状态指示灯(StatusLed,Off/Ok/Warn/Alarm 四态,报警可闪烁)、多通道实时趋势(TrendChart)、报警列表(AlarmList,带等级配色和确认状态),外加一个DataHub负责把采集线程和 UI 线程隔开。这些不是拿通用控件拼出来的,是照着工业现场的语义直接实现的。数据表格。一个TableView,每个单元格都是画出来的——序号、勾选框、开关、进度条、状态标签、操作链接、树形展开,全部原生绘制,还能行内编辑。它支持冻结列、合并行、三态列排序,以及一个我特别想让你试的东西:二十万行虚拟滚动,而模型里一行数据都不存。拉取式模型,视图只向模型要看得见的那二十来行,拖到哪画到哪,不卡。这一块我会单独写一篇讲透。下拉选择族。单选、可搜索(多字段匹配)、多选、树形、级联、日期。看着平平无奇,但里面藏着一个做过界面的人都踩过的坑:下拉弹层怎么冲出它所在的GroupBox和外层ScrollArea的双重裁剪?这个也值得单开一篇。三维设备视图。View3D,软件渲染,左键旋转、滚轮缩放、Shift 拖拽平移,单击能选中部件。而且拾取算法复用的是当帧已经排好序的面表,所以你点到的一定是你看得见的那个部件,不会点穿。部件颜色能由实时工艺值驱动——温度高了对应的设备就变色。全程无 GPU,这在工控机上是能落地的三维,不是演示品。布局,或者刻意不用布局。它有BoxLayout和GridLayout,支持 stretch 权重、min/max 钳制、跨列。但它同时把绝对坐标当成一等公民而不是历史包袱。这个取舍我后面专门讲,因为它最容易被误读成这库不完整。换肤。皮肤注册表加一个主题色带动整套配色,再加一层类 QSS 的样式表,运行时热切换,不用重新编译。演示程序本身还能在标题栏一键切中英文,整个界面即时重建。图标。38 个内置矢量图标,全代码绘制,任意缩放不失真。而且Icon不是封闭枚举,是个句柄——你可以把 Lucide、Feather 这些图标集的 SVG 路径直接注册进来,因为它们的作画网格(24×24、2 单位描边)正好和这个库一致,d属性抄过来就能用。工业现场要的泵、阀、反应釜这些符号,这么加进去成本极低。为什么是工业两个字通用 GUI 库多如牛毛,为什么还要造一个专门给工业的?因为工业界面的约束是反常的,通用库的默认取向常常正好拧着来。通用库假设你的程序开一会儿就关,工业上位机要在无人值守的机房里连跑几个月。所以这个库的热路径零内存分配——实时数据走固定容量的环形缓冲,第 901 个点进来覆盖第 1 个点,不申请一寸新内存。一个每秒分配一点点的设计,在跑三个月的场景里最后都会变成运维事故。通用库假设界面大部分时间在变,所以整控件重绘。工业画面 90% 的像素是静止的——背景的管道图、不动的标签、没报警的那些灯。所以这个库用脏矩形增量重绘,只画真正变了的那一小块。这不是优化,是让一块老旧工控机的 CPU 不至于常年空转在重绘上。通用库假设开发者控制运行环境。工业交付里你什么都控制不了——分辨率、缩放、主题、显卡。所以它自绘一切、不碰 GPU、把画面按固定坐标组态。还有一条最要命的,是线程模型。工业数据来自采集线程,而这个库有一条铁律:采集线程绝对不能碰任何控件,它唯一能调的是hub.push()。所有界面更新都发生在 UI 线程排空队列的那一刻。这不是建议。一个后台线程直接setValue()的程序也能跑,跑上三个月开始随机崩溃,崩溃现场和真正的原因隔着十万八千里,你排查到怀疑人生。这类坑,这个库从架构层面帮你堵死了。说到坑,顺便讲个真事,因为它最能说明这个项目对待质量的态度。前阵子给演示程序加中英文切换,一个估工时按小时算的功能。切语言得销毁并重建所有页面。可页面此前从来没被销毁过——导航只是把它藏起来。结果这个动作,当场引爆了两处潜伏了几个月的 use-after-free:有两个地方从页面外部持有着指向页面内部控件的指针,页面一直不死,它们就一直安全。页面能死的那一刻,它们全成了悬空指针。这两处 bug,写的时候没有任何错。它们依赖的那个页面永不销毁的隐式约定,只活在作者脑子里。这类代码没错,但依赖的前提被别处改掉了的缺陷,你 review 当前改动是看不出来的,因为改动本身是对的。是 AddressSanitizer 在几秒钟里把它们揪了出来——而这套 ASan 门禁,是 CI 每次提交都要跑的三条腿之一。我讲这个,不是炫耀修了个 bug。是想让你知道:这个库的每一次提交,都要在 Release、Debug、ASan 三套构建下把 276 个测试跑绿,外加一个专门检查每一处候选缺陷点是不是都被守住了的静态 lint。任何一条腿红了,门就是关的。对一个要被静态链进设备、跑在别人机柜里的库来说,这不是讲究,是底线。它现在还做不到什么到了这一节,你大概能猜到我要说什么。一个对你诚实的介绍,得把短板摊开。只有 Windows。这是现在最大的限制,没有之一。平台层是 21 个纯虚函数,分属两个接口,X11 和 Cocoa 的范围都划清了,但代码还没写。好消息是——平台层以上完全没有 Windows 依赖,渲染走 Blend2D 的 CPU 光栅化,没有一处平台绘图调用。所以移植是一件边界清晰、有 Win32 实现可照抄的活。这也正好接上下一节。没有无障碍支持(UIA)。领域专用库,优先级排在后面。IME 没做内联预编辑。中文能输入,候选窗跟随光标,但组合中的拼音是系统 IME 窗口画的,不是内联在输入框里。能用,不够精致。pre-1.0。别拿它去扛你明天要交付的关键项目而不做评估。它有扎实的测试和架构文档,但它还年轻。把这些列出来,不是为了显得谦虚。是因为我自己找库的时候,最烦的就是 README 一片光明,clone 下来才发现坑。我宁可你在这一节就掉头,也不想浪费你半天。接下来往哪走路线图上最靠前、也最需要人的,就是那个Linux 后端。前面说了,平台层 21 个纯虚函数,大半是一行包装(show()就是XMapWindow)。真正要动脑的集中在四处:事件循环、脏矩形呈现、DPI 换算、无边框窗口怎么在 X11 上要回窗口管理器的拖动行为。渲染层、控件层、主题、三维,一行都不用改。这是那种边界清楚、价值巨大、不需要读懂整个代码库的活,开源项目梦寐以求。如果你写过 X11,这是一两个周末的事;没写过,这是学它的好由头,因为靶子小、验收标准是机械的:测试跑绿、演示程序能跑。macOS(Cocoa)是同一套接口的另一个实现。往后还有无障碍、IME 内联这些打磨项。但方向上有几条是钉死的,不会变:文本栈继续用 Blend2D,不引入 FreeType/HarfBuzz;跨平台外观 100% 一致是硬约束,永远不用平台原生控件;无 moc 这条更是这个库区别于 Qt 的核心,不会为了任何便利放弃。换句话说,以后有了 Linux,你在 Windows 上写的画面,搬过去还是像素一致的同一个东西。这个系列还会写什么这篇是总纲。前面几次提到单开一篇的,我会一篇篇补上,每一篇都带能跑的代码和真实配图(配图是代码自己离屏渲染出来的,不会和代码对不上):表格篇:二十万行,模型里一行不存。拉取式模型到底是怎么运转的,冻结列、合并行、异步子节点三态是怎么回事。三维篇:没有 GPU,怎么做一个能旋转、能点选、能按工艺值着色的设备视图。下拉篇:一个下拉弹层,怎么同时冲出GroupBox和ScrollArea的双重裁剪。布局篇:为什么一个 2024 年的界面库,会把绝对坐标当成一等公民而不是历史遗留。想动手的,不用等我写完。仓库里有一篇 《30 分钟做一个反应釜监控画面》,从空窗口一步步做到仪表加状态灯加实时趋势的完整画面,每步都有配图,代码边读边编译。如果这个库帮你省下一份 Qt 授权,或者一个下午的windeployqt,去 GitHub 点个 star 是最省事的表态。它也是我们判断这条路要不要继续走下去的信号。GitHub - sj7suren/GeeyoouUI: GeeyoouUI: a modern C GUI framework, lightweight alternative to Qt. Cross-platform (Win, Linux, macOS). Offers widgets, layouts, signals, declarative UI, fast rendering. Modular, extensible. For desktop and embedded. · GitHub
返回列表