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. 按钮状态互锁
csharp
// 状态机设计
Idle → Start → Running
Running → Stop → Idle
Running → Pause → Idle(Pause 后回 Idle)
Running → Emergency → Idle
任何状态 → Emergency → Idle(E-Stop 不检查状态)
用 Is_Running 字段 + 方法前置条件实现:
csharp
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 --- 新增线圈地址常量
csharp
/*
* 新增内容:
* 4 个控制命令的线圈地址
* 每个命令占一个独立的线圈位(0~3)
* WriteSingleCoil(地址, true) 发脉冲
*/
// =========== 线圈地址 ============
// 开始
public static ushort Coil_Start = 0;
// 停止
public static ushort Coil_Stop = 1;
// 暂停
public static ushort Coil_Pause = 2;
// 急停
public static ushort Coil_EmergencyStop = 3;
文件 2: Models/AxisData.cs --- 加 INotifyPropertyChanged
csharp
/*
* 新增内容:
* 1. AxisData 实现 INotifyPropertyChanged 接口
* 2. Status 属性改为字段+属性模式,设值时触发 OnPropertyChanged
* 3. 这样 XAML 绑定 {Binding Axis1Data.Status} 才能自动刷新
*/
internal class AxisData : INotifyPropertyChanged
{
private AxisStatus _status; // 私有字段存真实值
public AxisStatus Status // 公开属性供 XAML 绑定
{
get => _status;
set { _status = value; OnPropertyChanged(); } // 赋值时通知 WPF
}
public AxisParam Data { get; set; } = new AxisParam(); // 12 个 float 参数
public event PropertyChangedEventHandler? PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string? name = null)
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
文件 3: Helpers/StatusToColorConverter.cs --- 新增状态→颜色转换器
csharp
/*
* 完整新增文件
*
* 作用:
* 把 AxisStatus 枚举转成对应的画刷颜色
* XAML 绑定:Foreground="{Binding Axis1Data.Status, Converter=...}"
*
* 执行流程:
* Status = Running → INPC 通知 → WPF 调 Convert(Running)
* → switch 匹配 → 返回 StatusRunningBrush(绿色)
* → UI 上状态文字变绿
*/
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using UpperMachine.Models;
namespace UpperMachine.Helpers
{
public class StatusToColorConverter : IValueConverter
{
// Convert:从数据源→UI 方向的转换
// value:传进来的原始值(这里是 AxisStatus 枚举)
// 返回值:赋给 Foreground 属性的画刷
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
// value is AxisStatus status
// → 判断 value 是不是 AxisStatus 类型
// → 是则赋给 status 变量,否则走最后 : 后面的兜底
return value is AxisStatus status ? status switch
{
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→数据源 的逆转换(不需要,所以抛异常)
public object ConvertBack(...) => throw new NotImplementedException();
}
}
文件 4: App.xaml --- 注册转换器 + 新增颜色
xml
<!-- 新增命名空间(第 5 行) -->
xmlns:helpers="clr-namespace:UpperMachine.Helpers"
<!-- ↑ 让 XAML 能找到 Helpers 文件夹下的类 -->
<!-- 在 Resources 里添加(第 10 行) -->
<helpers:StatusToColorConverter x:Key="StatusToColorConverter"/>
<!-- ↑ 创建转换器实例,取名为 "StatusToColorConverter" -->
<!-- 所有 Page 都能通过 {StaticResource StatusToColorConverter} 引用它 -->
<!-- 新增 4 个状态颜色(第 29~32 行) -->
<SolidColorBrush x:Key="StatusStandByBrush" Color="#90A4AE"/> <!-- 灰色:待机 -->
<SolidColorBrush x:Key="StatusRunningBrush" Color="#00E676"/> <!-- 绿色:运行中 -->
<SolidColorBrush x:Key="StatusStoppedBrush" Color="#E0E0E0"/> <!-- 白色:已停止 -->
<SolidColorBrush x:Key="StatusErrorBrush" Color="#FF1744"/> <!-- 红色:错误/报警 -->
文件 5: MainViewModel.cs --- 4 个按钮命令 + 执行方法
csharp
/*
* 新增内容:
* 1. 4 个 ICommand 属性(StartCommand, StopCommand, PauseCommand, EmergencyCommand)
* 2. 4 个执行方法(ExecuteStart/Stop/Pause/Emergency)
* 3. 构造函数注册 RelayCommand
* 4. Is_Running 状态字段防止按钮重按
* 5. 每个方法写对应线圈 + 设置 4 轴状态
*/
// === 私有字段 ===
private bool Is_Running; // 运行状态锁
// === 公开属性(第 100~106 行) ===
public ICommand StartCommand { get; }
public ICommand StopCommand { get; }
public ICommand PauseCommand { get; }
public ICommand EmergencyCommand { get; }
// === 执行方法(第 228~274 行) ===
// Start:仅在 idle 状态可用
// 写线圈 0(Coil_Start)+ 设 4 轴为 Running
private void ExecuteStart()
{
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 轴为 Stopped
private void ExecuteStop()
{
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 轴为 StandBy
private void ExecutePause()
{
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 轴为 Error
private void ExecuteEmergency()
{
if (_service == null || !IsConnected) return;
_service.WriteSingleCoil(PlcAddressMap.Coil_EmergencyStop, true);
Is_Running = false;
Axis1Data.Status = AxisStatus.Error; // UI 变 Error(红色)
// ... 其他 3 轴同理
}
// === 构造函数(第 300~307 行) ===
public MainViewModel()
{
ConnectionCommand = new RelayCommand(Connect);
StartCommand = new RelayCommand(ExecuteStart);
StopCommand = new RelayCommand(ExecuteStop);
PauseCommand = new RelayCommand(ExecutePause);
EmergencyCommand = new RelayCommand(ExecuteEmergency);
}
文件 6: HomePage.xaml --- 按钮加 Command + Status 加颜色
xml
<!-- ===== 第 227~238 行:4 个按钮加 Command 绑定 ===== -->
<!-- Start 按钮:Command 绑定到 ViewModel 的 StartCommand -->
<Button Grid.Column="0" Command="{Binding StartCommand}"
Content="Start" Background="{StaticResource GreenBrush}"
Foreground="White" FontSize="20"
BorderThickness="1" Height="45" Margin="0,0,5,0"/>
<!-- Stop 按钮 -->
<Button Grid.Column="1" Command="{Binding StopCommand}"
Content="Stop" Background="{StaticResource RedBrush}" .../>
<!-- Pause 按钮 -->
<Button Grid.Column="2" Command="{Binding PauseCommand}"
Content="Pause" .../>
<!-- Emergency Stop 按钮 -->
<Button Command="{Binding EmergencyCommand}"
Content="⚠ Emergency Stop" .../>
<!-- ===== 第 39~42 行:Status 文字颜色随状态变化 ===== -->
<TextBlock FontSize="30" Margin="0,0,0,8">
<!-- "Status:" 标签固定灰色 -->
<Run Text="Status:" Foreground="{StaticResource LabelBrush}"/>
<!-- 状态值通过 Converter 转颜色:
Running→绿 StandBy→灰 Stopped→白 Error→红 -->
<Run Text="{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 按钮绑定命令
- 导航按钮跳转页面
- 恢复轮询,完善数据读取
- 报警日志模块