从零写一个工控多协议通讯库(三):数据与界面分离后,显示层变成“即插即用的构件”
上一篇做了件狠事:把 area/寄存器/站号这些“协议味”彻底赶出框架,界面连协议都不认识,靠一份 JSON 配置就能驱动。当时结尾说下一篇去接第二个协议——但我先插一篇,因为这次分离结出了一个太顺手的果子,值得单独说:显示层可以做成“即插即用的构件”了。
本篇让一个仪表盘构件上线,并用一段配置拼出一块可弹出、可投大屏的看板。项目代号 talkplc,纯净室开发,只依据公开协议规范,个人时间与设备。
红利从哪来:数据是一份“按索引寻址的快照”
回顾一下上两篇搭出来的数据流:采集线程每一拍产出一份点位快照——一个按下标排列的数组,每个元素是 { 值, 有效, 状态 }。界面(或任何前端)只做一件事:按索引取值。
关键就在这“按索引”三个字。它意味着:任何想显示数据的东西,只需要知道两件事——绑定哪个下标、拿到快照后取出那个值。 至于这个值是 Modbus 保持寄存器、还是以后的西门子 DB 块,显示端一概不关心。
1 | 采集线程 ──每拍──▶ 快照 tag[](按索引:值/有效/状态) |
于是“加一种显示方式”这件事,成本被压到了极低。
构件的契约,只有一个方法
我给所有显示构件定了一个极小的基类。它的全部约定就是:绑定一个点位索引 + 每拍收到快照时自己去取值刷新。
1 | /* 所有显示构件("构件")的基类。整个契约就这么大: |
就这些。写一个新构件 = 继承它、实现 updateFromSnapshot 这一个方法。 趋势曲线、LED 状态灯、数字牌、可回写的开关……都从这里长出来,主程序一行不用改。
第一个构件:仪表盘
第一个落地的构件是圆弧仪表盘。它做的事很朴素:把值映射到一段弧上,中间显示数值+单位,下面写标签;点位无效或读失败就整体变红。
绑定与刷新加起来就几行——它只跟“索引”和“值”打交道:
1 | void GaugeWidget::updateFromSnapshot(const tp_tag_t *snap, int n) { |
弧怎么画是纯 Qt 的绘图细节(一段 270° 的背景弧 + 一段按比例填充的前景弧),这里不展开。
看板 = 一堆构件 + 同一份快照
把若干构件摆进一个网格,就是一块看板。我把它做成独立的弹出窗口:可以多开、可以拖到第二块屏、可以直接甩到车间大屏上。主窗口什么都不用懂,只需每一拍把同一份快照转交给每个打开着的看板:
1 | 主窗口 onUiTick(): |
看板本身也对协议一无所知,它只是个“把快照分发给旗下构件”的容器。
连布局都写进配置
既然界面已经配置化了,看板布局自然也该是数据而不是写死的代码。配置文件里加一段 dashboard 就行——声明要哪些构件、各自绑哪个点(按名字)、量程、单位:
1 | "dashboard": { |
界面按名字把构件绑到点位表里对应的下标,之后就靠这个下标从快照取值。换一块看板 = 换一段配置,代码一行不动。
跑起来
一段完整流程——主界面 →「加载配置」选中那份 JSON → 点位表按配置自动填好并连上 → 点「仪表板」弹出看板,三个仪表盘随采集值实时跳动,而这一切显示代码都不知道底下是什么协议:

单独看弹出的那块看板——三个仪表盘的布局、绑定的点位、量程、单位全部来自配置文件:

这一步换来什么
- 加显示方式零成本:新构件只继承基类、实现一个方法,框架 / 协议 / 采集全都不动。
- 看板即配置:要哪些表、绑哪个点、量程多少,全在 JSON 里;改看板不改代码,可整份下发。
- 能上大屏:看板是独立窗口,可多开、可投车间监视屏。
- 前端仍可换:构件只依赖“索引 + 值”这份纯 C 快照,所以 Qt 能画仪表盘,以后 LVGL 也能在嵌入式屏上画同样的构件——共用同一套内核。
顺着这条路,接下来能长出的构件很自然:趋势曲线(把历史值缓一段画折线)、LED / 状态灯(bool 点位)、数字牌(大字报警值)、开关按钮(绑可写点位,点一下回写)。每一个都只是 DataWidget 的一个子类。
接下来
显示这条线先探到这儿。下一篇把欠的债还上——回到协议:给框架接第二种协议(Modbus 从站,或者直接上西门子 S7),拿它真正异构的地址(DB1.DBD0 之类)狠狠拷问一下上一篇立起来的“零协议概念”接口。抽象立不立得住,接第二个协议时见真章。