上位机运动控制开发日志 --- WPF 页面导航 + 数据绑定 + PLC 写入
一、今日目标
- 为 "Axis Param Settings" 按钮添加页面跳转逻辑
- 创建 AxisParamSettingsPage 参数设置页面
- 实现 TextBox 与 PLC 数据的双向绑定
- 美化 UI:卡片式布局 + 返回按钮
- 添加 Apply(写入 PLC)按钮逻辑
二、页面跳转架构
2.1 整体设计
采用 MVVM + 导航委托 模式:
用户点击按钮 ↓ HomePage.xaml: Command="{Binding NavigateToAxisParamCommand}" ↓ MainViewModel.ExecuteNavigateToAxesParam() → new AxisParamSettingsPage() → page.DataContext = this(同一个 MainViewModel) → NavigateToPage?.Invoke(page) ↓ MainWindow.xaml.cs 注入的委托 → MainFrame.Navigate(page)
2.2 核心代码
MainViewModel.cs --- 导航委托属性:
csharp public Action<Page>? NavigateToPage { get; set; }
MainViewModel.cs --- 执行导航:
csharp public void ExecuteNavigateToAxesParam() { var page = new AxisParamSettingsPage(); page.DataContext = this; NavigateToPage?.Invoke(page); }
MainWindow.xaml.cs --- 注入导航逻辑:
csharp var vm = new ViewModels.MainViewModel(); DataContext = vm; vm.NavigateToPage = (page) => MainFrame.Navigate(page); var homePage = new HomePage(); homePage.DataContext = vm; MainFrame.Navigate(homePage);
原理: ViewModel 不直接引用 UI(MainFrame),而是通过 Action? 委托声明"我需要导航能力",由 MainWindow 在启动时注入真正的 MainFrame.Navigate()。保持了 MVVM 的纯净。
三、AxisParamSettingsPage 页面设计
3.1 布局结构
Page 背景:BackgroundBrush ┌─ 标题:Axis Parameter Settings(居中) ─┐ ├─ Card: Axis 1 ───────────────────────────┤ │ ┌─────────────────────────────────────┐ │ │ │ AbsoPos:[____] AbsoVel:[____] ... │ │ │ └─────────────────────────────────────┘ │ ├─ Card: Axis 2 ───────────────────────────┤ │ ┌─────────────────────────────────────┐ │ │ │ AbsoPos:[____] AbsoVel:[____] ... │ │ │ └─────────────────────────────────────┘ │ ├─ Card: Axis 3 ───────────────────────────┤ ├─ Card: Axis 4 ───────────────────────────┤ │ [Apply Settings] │ │ [←] │ └──────────────────────────────────────────┘
3.2 每轴卡片内部布局(8列 Grid)
Col 0 (Auto) Col 1 (*) Col 2 (Auto) Col 3 (*) [TextBox] AbsoPos: [TextBox] AbsoVel:
- TextBox:Width="90",绑定 AxisNData.Data.AbsoPos,StringFormat=N2
- TextBlock:HorizontalAlignment="Center",显示参数名
- 整体 Grid:HorizontalAlignment="Left" 靠左排列
3.3 数据绑定
TextBox 绑定见 MainViewModel 的轴属性:
xml <TextBox Text="{Binding Axis1Data.Data.AbsoPos, StringFormat=N2}" Width="90" FontSize="17" Margin="3" VerticalAlignment="Center"/>
数据流:
PLC → PollAllAxes() → AxisParam.AbsoPos setter → OnPropertyChanged() → WPF 绑定引擎 → TextBox 自动刷新
四、Apply Settings --- PLC 写入逻辑
4.1 添加批量写入方法
在 ModbusServiceBase.cs 添加 WriteMultipleRegisters(功能码 0x10):
csharp public void WriteMultipleRegisters(ushort startAddress, ushort[] values) { if (!_connected()) throw new InvalidOperationException("未连接"); Master!.WriteMultipleRegisters(Config.SlaveId, startAddress, values); }
4.2 在 MainViewModel 添加 Apply 命令
`csharp
public ICommand ApplySettingsCommand { get; }
// 构造函数
ApplySettingsCommand = new RelayCommand(ExecuteSettingAbso);
`
写入方法: 遍历 4 个轴,每个轴写入 4 个参数,每个参数用 ModbusHelper.ConvertFloatToPlc() 转为 2 个寄存器:
`csharp
private void ExecuteSettingAbso()
{
if (_service == null || !IsConnected) return;
var Axes = new\[\] { Axis1Data, Axis2Data, Axis3Data, Axis4Data };
for (int i = 0; i < Axes.Length; i++)
{
var data = Axesi.Data;
int baseAddr = PlcAddressMap.PerAxisStartAddressi;
var (h, l) = ModbusHelper.ConvertFloatToPlc(data.AbsoPos);
_service.WriteMultipleRegisters((ushort)(baseAddr + 6), new ushort[] { h, l });
(h, l) = ModbusHelper.ConvertFloatToPlc(data.AbsoVel);
_service.WriteMultipleRegisters((ushort)(baseAddr + 8), new ushort[] { h, l });
(h, l) = ModbusHelper.ConvertFloatToPlc(data.AbsoAccel);
_service.WriteMultipleRegisters((ushort)(baseAddr + 10), new ushort[] { h, l });
(h, l) = ModbusHelper.ConvertFloatToPlc(data.AbsoDecel);
_service.WriteMultipleRegisters((ushort)(baseAddr + 12), new ushort[] { h, l });
}
}
`
4.3 地址映射
| 参数 | 偏移(相对于轴起始地址) | 对应寄存器索引 |
|---|---|---|
| CurrentPos | 0 | regs0, regs1 |
| CurrentVel | 2 | regs2, regs3 |
| CurrentTorque | 4 | regs4, regs5 |
| AbsoPos | 6 | regs6, regs7 |
| AbsoVel | 8 | regs8, regs9 |
| AbsoAccel | 10 | regs10, regs11 |
| AbsoDecel | 12 | regs12, regs13 |
五、返回按钮设计
5.1 XAML --- Path 箭头图标
xml <Button Height="45" Width="45" Click="BackButton_Click" Background="{StaticResource CardBrush}" BorderBrush="{StaticResource BorderBrush}" BorderThickness="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="20,10,0,0"> <Path Data="M18,2 L4,20 L18,38" Stroke="{StaticResource AccentBrush}" StrokeThickness="4" StrokeStartLineCap="Round" StrokeEndLineCap="Round"/> </Button>
5.2 Code-behind 事件
csharp private void BackButton_Click(object sender, RoutedEventArgs e) { var homePage = new HomePage(); homePage.DataContext = DataContext; NavigationService.Navigate(homePage); }
使用 NavigationService.Navigate() 跳回主页,同时沿用同一个 MainViewModel DataContext。
六、遇到的小问题
6.1 class 类名不匹配
AxisParamSettingsPage.xaml 的 x:Class 和 .xaml.cs 的类名不一致,导致编译错误。
6.2 缺少 partial 和 InitializeComponent
WPF Page 的 code-behind 必须有 partial class 和构造函数调用 InitializeComponent()。
6.3 XAML 绑定语法
`xml
Command="ApplySettingsCommand"
Command="{Binding ApplySettingsCommand}"
`
缺少 {Binding} 时 WPF 当作字符串处理,按钮无效。
6.4 TextBox 的 Validation 红色边框
当输入不完整数字(如 1.)时,WPF 默认验证失败并显示红色边框。后续可通过 ValidatesOnExceptions=False 或自定义 ErrorTemplate 解决。
6.5 HorizontalAlignment="Left" 与 * 列的冲突
Grid 设了 HorizontalAlignment="Left" 时,* 列宽度变为 0,需要去掉或调整布局策略。
七、PLC 读写的字节序说明
使用大端模式(Big-Endian):
- 低地址存放高 16 位(high word)
- 高地址存放低 16 位(low word)
`csharp
// PLC → float
public static float ConvertPlcToFloat(ushort high, ushort low)
{
uint raw = (uint)(high << 16 | low);
return BitConverter.Int32BitsToSingle((int)raw);
}
// float → PLC
public static (ushort low, ushort high) ConvertFloatToPlc(float v)
{
int bits = BitConverter.SingleToInt32Bits(v);
return ((ushort)(bits >> 16), (ushort)(bits & 0xFFFF));
}
`
八、今日文件改动清单
| 文件 | 改动内容 |
|---|---|
| View/AxisParamSettingsPage.xaml | 新建:卡片式 4 轴参数页面 + 返回/Apply 按钮 |
| View/AxisParamSettingsPage.xaml.cs | 新建:构造函数 + BackButton 点击事件 |
| ViewModels/MainViewModel.cs | 添加导航属性/命令 + Apply 命令 + 写入方法 |
| MainWindow.xaml.cs | 注入导航委托 NavigateToPage = (page) => MainFrame.Navigate(page) |
| View/HomePage.xaml | 按钮绑定 NavigateToAxisParamCommand |
| Services/ModbusServiceBase.cs | 添加 WriteMultipleRegisters 批量写入方法 |
九、下一步计划
- 处理 TextBox 验证红色边框问题
- 轮询策略:打开设置页时暂停写入覆盖,或仅读取实时参数
- 实现 Return Home / Manual Adjust 等剩余导航按钮
- 确认 Apply 实际写入 PLC 后的反馈