上位机运动控制开发日志 — WPF 页面导航 + 数据绑定 + PLC 写入

上位机运动控制开发日志 --- WPF 页面导航 + 数据绑定 + PLC 写入

一、今日目标

  1. 为 "Axis Param Settings" 按钮添加页面跳转逻辑
  2. 创建 AxisParamSettingsPage 参数设置页面
  3. 实现 TextBox 与 PLC 数据的双向绑定
  4. 美化 UI:卡片式布局 + 返回按钮
  5. 添加 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 批量写入方法

九、下一步计划

  1. 处理 TextBox 验证红色边框问题
  2. 轮询策略:打开设置页时暂停写入覆盖,或仅读取实时参数
  3. 实现 Return Home / Manual Adjust 等剩余导航按钮
  4. 确认 Apply 实际写入 PLC 后的反馈