自定义控件(二):阀门控件家族——气体阀、水阀、MFC 阀
这是「从零搭建半导体设备上位机」系列第8篇。上一篇讲状态灯,这一篇讲阀门。阀门控件是这套界面里被点得最多的东西,也是最容易出安全事故的地方。
从一条 git 记录说起
翻提交历史,有一条特别短:
55fa2640 气体阀 添加 透明按钮用于点击
“添加透明按钮用于点击”——这句话本身就说明出了问题:原来的点击区域不能点了。
事实是:气体阀控件用的是三张素材图(关闭态、打开态、中间态),切换时换 Image.Source。某次换图之后,三张图的画布尺寸不一致(有的 120×80,有的 124×82),Image 按 Stretch="Uniform" 缩放,导致视觉上的阀门位置发生了几个像素的偏移。
偏移不影响看,但点击热区是按控件边界算的,操作员觉得"我明明点在阀门上",实际点在了旁边的空白处。
修法很土也很有效:在 Image 上面盖一个 Background="Transparent" 的 Button,尺寸和控件一致,专门负责接收点击。
<Grid>
<Image Source="{Binding ...}" Stretch="Uniform" />
<Button Background="Transparent" BorderThickness="0"
Command="{Binding ...}" />
</Grid>
Transparent 和 null 的区别:Background="Transparent" 能接收鼠标事件,Background="{x:Null}" 不能。这是 WPF 里一个很经典的坑。
一、三种阀,三个状态机
界面上的"阀"看起来都差不多,实际状态机完全不同:
| 控件 | 状态 | 额外信息 | 数量级 |
|---|---|---|---|
| 气体阀 | 开 / 关 / 中间态 | 到位信号 | 几十个 |
| 水阀(PCW) | 开 / 关 | 流量值 | 几个 |
| MFC(质量流量控制器) | 设定值 + 实际值 | 上下限校验 | 每个腔几个 |
- 气体阀是纯开关,但"中间态"很重要——气动阀从收到指令到真正到位有几十毫秒延迟,界面上必须能看出"正在动"。
- 水阀只有开关,但要看流量。流量掉了说明管路有问题。
- MFC 不是开关,是"设一个值,等它跟上"。它的状态是连续的,还要做上下限校验。

三个控件不能合成一个。早期试过做一个"通用阀门控件"用枚举区分类型,结果是每个类型都要在控件内部写一堆 if,最后比三个独立控件还难维护。
二、PressToggleButton:最底层的那块砖
整个家族的地基是这个:64 行 XAML + 79 行代码,四个依赖属性。
| 属性 | 用途 |
|---|---|
ButtonContent | 按钮上显示的文字/图标 |
IsButtonChecked | 开关状态(双向绑定到 ViewModel) |
Command | 点击命令 |
CommandParameter | 命令参数(通常是点位名) |
为什么不用原生的 ToggleButton
原生 ToggleButton 有三个问题:
- 它的
IsChecked是bool?(可空三态)。工控里只要"开/关"两态,三态会多出一个"未确定",处理起来很烦。 - 样式要重写。原生
ToggleButton的默认外观是一个凸起的方按钮,和"阀门"的视觉差太远,得整套ControlTemplate重写。 Command和IsChecked的关系要自己管。点一下,是先执行命令还是先改状态?如果先改状态,命令失败了状态就错了。
所以封装了一层:IsButtonChecked 是 bool,Command 先执行,命令成功之后 ViewModel 才回写 IsButtonChecked。
这条顺序很重要:界面上的状态永远跟着设备真实状态走,不是跟着点击走。点了但没成功,界面上的阀门不能变成"开"。
三、ValveCtrlStatus:14 个依赖属性
这个控件 338 行 XAML + 193 行代码,14 个依赖属性:
| 属性组 | 属性 | 说明 |
|---|---|---|
| 基础 | ButtonContent、IsButtonChecked | 同 PressToggleButton |
| 开条件 | OpenCondition、OpenConditionVisibility | 满足开阀条件时显示 |
| 关条件 | CloseCondition、CloseConditionVisibility | 满足关阀条件时显示 |
| 运行条件 | RunCondition、RunConditionVisibility | 工艺运行中时显示 |
| 运行结果 | RunResult、RunResultVisibility | 上次动作的结果 |
| 到位 | IsDown、IsDownVisibility | 到位信号 |
| 命令 | Command、CommandParameter | 同 PressToggleButton |
一个阀门按钮,为什么要显示这么多状态?
因为现场真的会问:“这个阀我点了,为什么没动?”
原因可能有好几种:
- 不满足开阀条件(真空没到位、互锁没解除)
- 工艺正在跑,禁止手动干预
- 上次动作失败了(指令发了但没到位)
- 阀门卡住了(指令发了,到位信号没来)
如果界面上只有一个"开/关"按钮,操作员只能叫工程师。把这四个判断结果做成四个小指示器挂在按钮旁边,操作员自己就能看出来"是真空没到位",去处理真空,而不是报"阀门坏了"。
这 14 个属性的本质,是把"为什么不能操作"这件事可视化。
代价是这个控件很重:14 个依赖属性、338 行 XAML,一屏放二十个就是 6,760 行渲染工作量。所以只在关键阀门上用,普通点位还是用 PressToggleButton。
四、AutoManuButton:194 + 321
这个是"自动/手动"模式切换按钮,194 行 XAML、321 行代码。
代码比 XAML 长,说明逻辑重。它管的是:切换模式前的检查(设备必须在空闲态、不能有未完成的动作)、切换时的确认弹窗、切换失败的回滚。
321 行代码里,真正画界面的不到 30 行,剩下全是状态校验。
这个控件后来成了整个项目里"最不该是控件"的东西——它其实是一个业务流程,被塞进了一个按钮里。第 10 篇讲导航时会再碰到它。
踩坑记录
坑一:Background="Transparent" vs {x:Null}
前面说的透明按钮。补充一点:Transparent 是有画刷但全透明,能命中测试;{x:Null} 是没有画刷,鼠标事件直接穿透。
同理适用于 Border、Grid、Rectangle。凡是"看不见但要能点"的地方,一律 Transparent。
坑二:重命名引发的连锁修改
有一条提交:
15eeebf9 气体阀重命名为 GasValveHdps
看起来只是改个名字,实际改了三处:控件类名、XAML 里的 x:Class、所有引用它的页面。
漏了第四处:ResourceDictionary 里如果有以控件类型为 key 的样式,改名之后样式静默失效——不报错,只是样式不生效了。
改名之后那个阀门按钮突然变回了默认样式(灰色方块),查了半天才发现是 TargetType 里的类型名没跟着改。
重命名控件的检查清单:类名、x:Class、引用处、TargetType、DataTemplate DataType、依赖属性的 typeof()。
坑三:Hdps 这个后缀是怎么来的
GasValveHdps、PcwValveHdps、ChamberHpds——一堆控件带着 Hdps 后缀。
它来自早期的一个设备型号名。后来设备换了,型号变了,但后缀留下来了。
后果是:新人看到 GasValveHdps 会以为"这是气体阀的一个特殊版本,是不是还有个 GasValve?"——实际上没有,GasValveHdps 就是气体阀。
有一条提交叫 c9f8fae4 UI阀分类整理,就是专门收拾这个的。但只整理了一部分,后缀还是留着。
命名里的型号/项目代号,一旦扩散到类名,就很难收回来。 这类信息应该在配置里,不该在标识符里——这一点和脱敏是同一个道理:型号是会变的,语义不会。
本篇小结
- 阀门控件不是"画个阀门",它的复杂度在于把"为什么不能操作"可视化——
ValveCtrlStatus的 14 个属性就是干这个的。 Transparent能接收点击,{x:Null}不能,这是 WPF 命中测试的基础,也是"加透明按钮"那个提交的由来。- 界面状态跟设备真实状态走,不跟点击走——命令成功后才回写
IsChecked。 - 控件的命名里不要带型号/项目代号,会永久留下来。
下一篇讲拖拽:晶圆从一个位置拖到另一个位置,Adorner 做跟随视觉,Drop 时怎么保证数据不丢。
下期预告
第9篇:拖拽与交互:Adorner、DragDrop 与晶圆拖放信息
本文详解半导体设备上位机中气体阀、水阀和MFC阀三类自定义WPF控件的实现。重点包括:基于状态机的差异化行为建模、依赖属性驱动的故障可视化(14个属性揭示开阀失败原因)、透明按钮解决点击热区偏移问题、命令执行与状态更新的严格时序控制(先执行后回写),以及工业场景下命名规范与坑点规避(如重命名连锁影响、HitTest穿透机制)。所有设计均服务于工控安全与操作可诊断性。
1万+

被折叠的 条评论
为什么被折叠?



