WPF 上位机学习日记(六)— 按钮命令绑定 + 状态管理 + 状态颜色
今日内容
今天主要做了 3 件事:
- 按钮绑定 ICommand— Start/Stop/Pause/Emergency Stop 绑定到 ViewModel
- 状态管理 + INotifyPropertyChanged— 按钮联动 4 轴状态,UI 自动刷新
- StatusToColorConverter— 状态值自动映射到颜色(Running=绿, StandBy=灰 等)
一、遇到的坑 & 学到的知识
1. 新 .cs 文件添加后,XAML 找不到
问题:新建StatusToColorConverter.cs后,XAML 报错The name does not exist in the namespace。
原因:WPF 的 XAML 设计器依赖编译后的程序集来解析代码中的类。新加 .cs 文件后需要先编译一次。
解决:dotnet build之后再打开 XAML 文件,或者直接dotnet run运行。
2. App.xaml 命名空间声明
问题:在 App.xaml 里引用 Helpers 文件夹下的类时,需要声明对应的 XML 命名空间。
正确: xmlns:helpers="clr-namespace:UpperMachine.Helpers" <helpers:StatusToColorConverter x:Key="..."/> 错误: xmlns:local="clr-namespace:UpperMachine" ← local 只到 UpperMachine <local:StatusToColorConverter .../> ← 找不到!在 UpperMachine.Helpers 里clr-namespace:后面跟的是C# 的命名空间路径,不是文件夹路径,但通常文件夹和命名空间一致。
3. 线圈 vs 寄存器
| 类型 | 地址前缀 | 功能码 | 写方法 | 数据 |
|---|---|---|---|---|
| 线圈(Coils) | 0x | 05 | WriteSingleCoil(地址, true/false) | 1 位 ON/OFF |
| 保持寄存器(Holding Registers) | 4x | 06/16 | WriteSingleRegister(地址, 数值) | 16 位(0~65535) |
它们地址空间独立——线圈地址 0 和寄存器地址 0 是两回事。
4. 按钮状态互锁
// 状态机设计Idle → Start → Running Running → Stop → Idle Running → Pause → Idle(Pause 后回 Idle) Running → Emergency → Idle 任何状态 → Emergency → Idle(E-Stop 不检查状态)用Is_Running字段 + 方法前置条件实现:
if(Is_Running)return;// Start 防重按if(!Is_Running)return;// Stop/Pause 只有运行时有效// Emergency 不设条件——急停永远可用5. Modbus Slave 同时配线圈和寄存器
使用 Modbus Slave(Witte)时,免费版只能选一种数据类型。选 Holding Registers 就看不到 Coils 配置。
所以:
- 如果用线圈 → 在 Modbus Slave 里新建 Coils 表
- 如果用寄存器 → 改回
WriteSingleRegister,地址 88~91 - 不能两个同时用(免费版限制)
二、今天新增/修改的文件
文件 1:Models/PlcAddressMap.cs— 新增线圈地址常量
/* * 新增内容: * 4 个控制命令的线圈地址 * 每个命令占一个独立的线圈位(0~3) * WriteSingleCoil(地址, true) 发脉冲 */// =========== 线圈地址 ============// 开始publicstaticushortCoil_Start=0;// 停止publicstaticushortCoil_Stop=1;// 暂停publicstaticushortCoil_Pause=2;// 急停publicstaticushortCoil_EmergencyStop=3;文件 2:Models/AxisData.cs— 加 INotifyPropertyChanged
/* * 新增内容: * 1. AxisData 实现 INotifyPropertyChanged 接口 * 2. Status 属性改为字段+属性模式,设值时触发 OnPropertyChanged * 3. 这样 XAML 绑定 {Binding Axis1Data.Status} 才能自动刷新 */internalclassAxisData:INotifyPropertyChanged{privateAxisStatus_status;// 私有字段存真实值publicAxisStatus Status// 公开属性供 XAML 绑定{get=>_status;set{_status=value;OnPropertyChanged();}// 赋值时通知 WPF}publicAxisParamData{get;set;}=newAxisParam();// 12 个 float 参数publiceventPropertyChangedEventHandler?PropertyChanged;protectedvoidOnPropertyChanged([CallerMemberName]string?name=null)=>PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(name));}文件 3:Helpers/StatusToColorConverter.cs— 新增状态→颜色转换器
/* * 完整新增文件 * * 作用: * 把 AxisStatus 枚举转成对应的画刷颜色 * XAML 绑定:Foreground="{Binding Axis1Data.Status, Converter=...}" * * 执行流程: * Status = Running → INPC 通知 → WPF 调 Convert(Running) * → switch 匹配 → 返回 StatusRunningBrush(绿色) * → UI 上状态文字变绿 */usingSystem.Globalization;usingSystem.Windows;usingSystem.Windows.Data;usingUpperMachine.Models;namespaceUpperMachine.Helpers{publicclassStatusToColorConverter:IValueConverter{// Convert:从数据源→UI 方向的转换// value:传进来的原始值(这里是 AxisStatus 枚举)// 返回值:赋给 Foreground 属性的画刷publicobjectConvert(objectvalue,TypetargetType,objectparameter,CultureInfoculture){// value is AxisStatus status// → 判断 value 是不是 AxisStatus 类型// → 是则赋给 status 变量,否则走最后 : 后面的兜底returnvalueisAxisStatusstatus?statusswitch{AxisStatus.Running=>Application.Current.FindResource("StatusRunningBrush"),AxisStatus.StandBy=>Application.Current.FindResource("StatusStandByBrush"),AxisStatus.Stopped=>Application.Current.FindResource("StatusStoppedBrush"),AxisStatus.Error=>Application.Current.FindResource("StatusErrorBrush"),_=>Application.Current.FindResource("TextBrush")// 兜底}:Application.Current.FindResource("TextBrush");// 非 AxisStatus}// ConvertBack:UI→数据源 的逆转换(不需要,所以抛异常)publicobjectConvertBack(...)=>thrownewNotImplementedException();}}文件 4:App.xaml— 注册转换器 + 新增颜色
<!-- 新增命名空间(第 5 行) -->xmlns:helpers="clr-namespace:UpperMachine.Helpers"<!-- ↑ 让 XAML 能找到 Helpers 文件夹下的类 --><!-- 在 Resources 里添加(第 10 行) --><helpers:StatusToColorConverterx:Key="StatusToColorConverter"/><!-- ↑ 创建转换器实例,取名为 "StatusToColorConverter" --><!-- 所有 Page 都能通过 {StaticResource StatusToColorConverter} 引用它 --><!-- 新增 4 个状态颜色(第 29~32 行) --><SolidColorBrushx:Key="StatusStandByBrush"Color="#90A4AE"/><!-- 灰色:待机 --><SolidColorBrushx:Key="StatusRunningBrush"Color="#00E676"/><!-- 绿色:运行中 --><SolidColorBrushx:Key="StatusStoppedBrush"Color="#E0E0E0"/><!-- 白色:已停止 --><SolidColorBrushx:Key="StatusErrorBrush"Color="#FF1744"/><!-- 红色:错误/报警 -->文件 5:MainViewModel.cs— 4 个按钮命令 + 执行方法
/* * 新增内容: * 1. 4 个 ICommand 属性(StartCommand, StopCommand, PauseCommand, EmergencyCommand) * 2. 4 个执行方法(ExecuteStart/Stop/Pause/Emergency) * 3. 构造函数注册 RelayCommand * 4. Is_Running 状态字段防止按钮重按 * 5. 每个方法写对应线圈 + 设置 4 轴状态 */// === 私有字段 ===privateboolIs_Running;// 运行状态锁// === 公开属性(第 100~106 行) ===publicICommandStartCommand{get;}publicICommandStopCommand{get;}publicICommandPauseCommand{get;}publicICommandEmergencyCommand{get;}// === 执行方法(第 228~274 行) ===// Start:仅在 idle 状态可用// 写线圈 0(Coil_Start)+ 设 4 轴为 RunningprivatevoidExecuteStart(){if(Is_Running)return;// 已在运行 → 忽略点击if(_service==null||!IsConnected)return;// 未连接 → 忽略_service.WriteSingleCoil(PlcAddressMap.Coil_Start,true);Is_Running=true;Axis1Data.Status=AxisStatus.Running;// INPC 通知 UI 刷新Axis2Data.Status=AxisStatus.Running;Axis3Data.Status=AxisStatus.Running;Axis4Data.Status=AxisStatus.Running;}// Stop:仅在 running 状态可用// 写线圈 1(Coil_Stop)+ 设 4 轴为 StoppedprivatevoidExecuteStop(){if(!Is_Running)return;// 没运行 → 忽略if(_service==null||!IsConnected)return;_service.WriteSingleCoil(PlcAddressMap.Coil_Stop,true);Is_Running=false;Axis1Data.Status=AxisStatus.Stopped;// UI 变 Stopped(白色)// ... 其他 3 轴同理}// Pause:仅在 running 状态可用// 写线圈 2(Coil_Pause)+ 设 4 轴为 StandByprivatevoidExecutePause(){if(!Is_Running)return;if(_service==null||!IsConnected)return;_service.WriteSingleCoil(PlcAddressMap.Coil_Pause,true);Is_Running=false;Axis1Data.Status=AxisStatus.StandBy;// UI 变 StandBy(灰色)// ... 其他 3 轴同理}// Emergency:任何状态都可用(不检查 Is_Running)// 写线圈 3(Coil_EmergencyStop)+ 设 4 轴为 ErrorprivatevoidExecuteEmergency(){if(_service==null||!IsConnected)return;_service.WriteSingleCoil(PlcAddressMap.Coil_EmergencyStop,true);Is_Running=false;Axis1Data.Status=AxisStatus.Error;// UI 变 Error(红色)// ... 其他 3 轴同理}// === 构造函数(第 300~307 行) ===publicMainViewModel(){ConnectionCommand=newRelayCommand(Connect);StartCommand=newRelayCommand(ExecuteStart);StopCommand=newRelayCommand(ExecuteStop);PauseCommand=newRelayCommand(ExecutePause);EmergencyCommand=newRelayCommand(ExecuteEmergency);}文件 6:HomePage.xaml— 按钮加 Command + Status 加颜色
<!-- ===== 第 227~238 行:4 个按钮加 Command 绑定 ===== --><!-- Start 按钮:Command 绑定到 ViewModel 的 StartCommand --><ButtonGrid.Column="0"Command="{Binding StartCommand}"Content="Start"Background="{StaticResource GreenBrush}"Foreground="White"FontSize="20"BorderThickness="1"Height="45"Margin="0,0,5,0"/><!-- Stop 按钮 --><ButtonGrid.Column="1"Command="{Binding StopCommand}"Content="Stop"Background="{StaticResource RedBrush}".../><!-- Pause 按钮 --><ButtonGrid.Column="2"Command="{Binding PauseCommand}"Content="Pause".../><!-- Emergency Stop 按钮 --><ButtonCommand="{Binding EmergencyCommand}"Content="⚠ Emergency Stop".../><!-- ===== 第 39~42 行:Status 文字颜色随状态变化 ===== --><TextBlockFontSize="30"Margin="0,0,0,8"><!-- "Status:" 标签固定灰色 --><RunText="Status:"Foreground="{StaticResource LabelBrush}"/><!-- 状态值通过 Converter 转颜色: Running→绿 StandBy→灰 Stopped→白 Error→红 --><RunText="{Binding Axis1Data.Status}"Foreground="{Binding Axis1Data.Status, Converter={StaticResource StatusToColorConverter}}"/></TextBlock>三、今日踩坑汇总
| # | 问题 | 原因 | 解决 |
|---|---|---|---|
| 1 | XAML 找不到新加的 Converter 类 | 没编译,设计器看不到 .cs 中的类 | dotnet build |
| 2 | local:命名空间找不到类 | local指向UpperMachine,但类在UpperMachine.Helpers | 加xmlns:helpers="clr-namespace:UpperMachine.Helpers" |
| 3 | App.xaml 第 10 行用local:引 converter | 手误,忘改成helpers: | local:→helpers: |
| 4 | 按钮互锁失效 | Is_Running写完就设回 false | 保持Is_Running = true直到 Stop/Pause/E-Stop |
| 5 | AxisData.Status 赋值后 UI 不变 | AxisData 没实现 INotifyPropertyChanged | 加 INPC,Status 用属性+字段模式 |
| 6 | Modbus Slave 不能同时配 Coils + Registers | 免费版限制 | 根据实际 PLC 选一种 |
四、整体架构图
用户点击 Start ↓ HomePage.xaml Button.Command="{Binding StartCommand}" ↓ MainViewModel.StartCommand (RelayCommand) ↓ ExecuteStart() ├── WriteSingleCoil(0, true) ← 写 PLC 线圈 ├── Is_Running = true ← 锁住,防重按 └── Axis1.Status = AxisStatus.Running ← INPC 通知 UI ↓ AxisData.Status setter → OnPropertyChanged() ↓ WPF 收到通知 → 执行 StatusToColorConverter.Convert(Running) ↓ 返回 StatusRunningBrush(绿色 #00E676) → 赋值给 Run.Foreground ↓ 用户在 UI 上看到: Status: Running ← 绿色文字五、当前状态与下一步
已实现
| 功能 | 状态 |
|---|---|
| Start/Stop/Pause/急停 绑定 | ✅ 已完成 |
| 状态互锁(防重按) | ✅ 已完成 |
| Status INotifyPropertyChanged | ✅ 已完成 |
| Status→颜色自动转换 | ✅ 已完成 |
| 线圈写入 PLC | ✅ 已完成(需 Modbus Slave 配合测试) |
下一阶段计划
- 创建子页面(ParamSettingsPage、CommConfigPage 等)
- Return Home / Manual Adjust 按钮绑定命令
- 导航按钮跳转页面
- 恢复轮询,完善数据读取
- 报警日志模块