四轴运动控制第一篇 - WPF MVVM 基础框架搭建

四轴运动控制第一篇 - WPF MVVM 基础框架搭建

本文基于 .NET 10 + WPF + MVVM 架构,从零搭建一个四轴运动控制系统的 UI 外壳。包含项目结构、全局资源、ViewModel 绑定、DataTrigger 状态切换等核心知识点,逐行注释,适合 WPF 入门学习参考。


目录

  • 一、项目背景与架构
  • 二、项目结构总览
  • [三、App.xaml - 全局资源定义](#三、App.xaml - 全局资源定义)
  • [四、MainViewModel.cs - 数据驱动核心](#四、MainViewModel.cs - 数据驱动核心)
  • [五、MainWindow.xaml.cs - 绑定 ViewModel](#五、MainWindow.xaml.cs - 绑定 ViewModel)
  • [六、MainWindow.xaml - 界面布局详解](#六、MainWindow.xaml - 界面布局详解)
    • [6.1 整体 Grid 三行布局](#6.1 整体 Grid 三行布局)
    • [6.2 顶栏 - 左侧标题区域](#6.2 顶栏 - 左侧标题区域)
    • [6.3 顶栏 - 右侧 IP + 连接状态](#6.3 顶栏 - 右侧 IP + 连接状态)
    • [6.4 中间 Frame - 页面容器](#6.4 中间 Frame - 页面容器)
    • [6.5 底栏 - 四轴状态显示](#6.5 底栏 - 四轴状态显示)
  • 七、核心机制详解
    • [7.1 DataContext - 绑定的「目标对象」](#7.1 DataContext - 绑定的「目标对象」)
    • [7.2 Binding - 声明式数据连接](#7.2 Binding - 声明式数据连接)
    • [7.3 INotifyPropertyChanged + CallerMemberName](#7.3 INotifyPropertyChanged + CallerMemberName)
    • [7.4 DataTrigger - 条件触发覆盖](#7.4 DataTrigger - 条件触发覆盖)
  • 八、启动与运行流程
  • 九、总结与下一步

一、项目背景与架构

本项目的目标是开发一套四轴运动控制系统的上位机软件,采用 WPF + MVVM 设计模式,与 PLC 通过 Modbus TCP 协议通信,实现四个运动轴的实时监控与控制。

技术栈

技术 用途
.NET 10 + WPF 桌面 UI 框架
MVVM 模式 界面与数据逻辑分离
INotifyPropertyChanged 数据变更通知 UI
DataTrigger / Binding 声明式 UI 数据驱动
Frame + Page 导航 多页面切换

设计目标

  • 顶栏:显示 IP 地址 + 连接状态指示灯
  • 中间区域:Frame 容器,用于加载不同的功能页面
  • 底栏:显示四个轴当前的运行状态

二、项目结构总览

复制代码
UpperMachine/
├── App.xaml              # 全局资源(颜色画刷)
├── App.xaml.cs           # 应用入口
├── MainWindow.xaml       # 主窗口 UI 布局
├── MainWindow.xaml.cs    # 主窗口绑定 ViewModel
├── ViewModels/
│   └── MainViewModel.cs  # 数据模型(IP、连接状态、轴状态)
├── Models/               # 后续存放数据模型
├── View/                 # 后续存放功能页面
└── UpperMachine.csproj   # 项目文件

三、App.xaml - 全局资源定义

作用:定义整个应用程序中所有窗口和控件都可以引用的全局资源。在这里我们定义了 14 个颜色画刷,实现统一的暗色主题风格。

xml 复制代码
<Application x:Class="UpperMachine.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             StartupUri="MainWindow.xaml">

    <Application.Resources>
        <!-- 背景色 - 深蓝黑 -->
        <SolidColorBrush x:Key="BackgroundBrush" Color="#1B1B2F"/>
        <!-- 面板色 - 深蓝 -->
        <SolidColorBrush x:Key="PanelBrush" Color="#162447"/>
        <!-- 卡片色 - 中蓝 -->
        <SolidColorBrush x:Key="CardBrush" Color="#1F4068"/>
        <!-- 顶栏/底栏色 - 最暗 -->
        <SolidColorBrush x:Key="HeaderBrush" Color="#0F0F23"/>
        <!-- 边框色 -->
        <SolidColorBrush x:Key="BorderBrush" Color="#2D2D44"/>
        <!-- 强调色 - 青色 -->
        <SolidColorBrush x:Key="AccentBrush" Color="#00D2FF"/>
        <!-- 主要文字色 - 浅灰 -->
        <SolidColorBrush x:Key="TextBrush" Color="#E0E0E0"/>
        <!-- 标签文字色 - 灰蓝 -->
        <SolidColorBrush x:Key="LabelBrush" Color="#90A4AE"/>
        <!-- 绿色 - 连接成功/运行中 -->
        <SolidColorBrush x:Key="GreenBrush" Color="#00E676"/>
        <!-- 红色 - 断开/报警 -->
        <SolidColorBrush x:Key="RedBrush" Color="#FF1744"/>
        <!-- 橙色 - 警告 -->
        <SolidColorBrush x:Key="OrangeBrush" Color="#FF9100"/>
        <!-- 黄色 - 注意 -->
        <SolidColorBrush x:Key="YellowBrush" Color="#FFD740"/>
        <!-- 蓝色 - 信息 -->
        <SolidColorBrush x:Key="BlueBrush" Color="#448AFF"/>
        <!-- 纯白 - 特殊强调 -->
        <SolidColorBrush x:Key="WhiteBrush" Color="#FFFFFF"/>
    </Application.Resources>
</Application>

重点理解

  • x:Key :每个资源都有一个唯一的键名,XAML 中通过 {StaticResource 键名} 引用。
  • StaticResource :编译时解析的资源引用,资源必须在引用前定义。比 DynamicResource 性能更好。
  • StartupUri="MainWindow.xaml":应用启动后自动打开 MainWindow。
xml 复制代码
<!-- 在控件中使用:-->
Background="{StaticResource BackgroundBrush}"
Foreground="{StaticResource TextBrush}"
Fill="{StaticResource GreenBrush}"

四、MainViewModel.cs - 数据驱动核心

作用:作为 View 和 Model 之间的桥梁,负责存储 UI 需要显示的所有数据,并在数据变化时通知 WPF 刷新界面。

csharp 复制代码
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace UpperMachine.ViewModels
{
    // 实现 INotifyPropertyChanged 接口,让 WPF 能监听属性变化
    class MainViewModel : INotifyPropertyChanged
    {
        // ===== 私有字段(实际存储数据的地方) =====
        private string _ipAddress = "192.168.0.10:502";  // PLC IP 地址
        private bool _isConnected = true;                  // 连接状态
        private string _axis1Status = "Axis1 : StandBy";   // 轴1 状态
        private string _axis2Status = "Axis2 : StandBy";   // 轴2 状态
        private string _axis3Status = "Axis3 : StandBy";   // 轴3 状态
        private string _axis4Status = "Axis4 : StandBy";   // 轴4 状态

        // ===== 公有属性(XAML 绑定目标) =====
        // getter 返回字段值,setter 更新字段 + 触发通知

        public string IpAddress
        {
            get => _ipAddress;
            set { _ipAddress = value; OnPropertyChanged(); }
        }

        public bool IsConnected
        {
            get => _isConnected;
            set { _isConnected = value; OnPropertyChanged(); }
        }

        public string Axis1Status
        {
            get => _axis1Status;
            set { _axis1Status = value; OnPropertyChanged(); }
        }

        public string Axis2Status
        {
            get => _axis2Status;
            set { _axis2Status = value; OnPropertyChanged(); }
        }

        public string Axis3Status
        {
            get => _axis3Status;
            set { _axis3Status = value; OnPropertyChanged(); }
        }

        public string Axis4Status
        {
            get => _axis4Status;
            set { _axis4Status = value; OnPropertyChanged(); }
        }

        // ===== INotifyPropertyChanged 接口实现 =====
        // 事件:WPF 内部会订阅这个事件
        public event PropertyChangedEventHandler? PropertyChanged;

        // 通知方法:属性值变化时调用,WPF 收到通知后刷新 UI
        // [CallerMemberName] 自动填入调用者的属性名
        protected void OnPropertyChanged([CallerMemberName] string? name = null)
            => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}

核心设计思路

ViewModel 只负责数据,不关心 UI 长什么样。UI 只负责展示,不关心数据从哪来。

例如:

csharp 复制代码
// 后续写 PLC 通信代码时,只需改属性值:
IsConnected = true;   // → UI 自动变绿 + 显示 "Connected"
Axis1Status = "Axis1 : Running";  // → UI 自动更新轴状态文字

[CallerMemberName] 的作用

编译器自动将调用者的方法名作为参数传入。例如:

csharp 复制代码
set { _ipAddress = value; OnPropertyChanged(); }
// 编译器自动翻译为:
set { _ipAddress = value; OnPropertyChanged("IpAddress"); }

好处:不改写死字符串,重构时不会出现拼写错误。

?.Invoke 的含义

csharp 复制代码
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
  • ?:如果 PropertyChanged 为 null(没有监听者),则不执行
  • this:告诉 WPF 是当前 ViewModel 实例的属性变了
  • PropertyChangedEventArgs(name):告诉 WPF 哪个属性变了

五、MainWindow.xaml.cs - 绑定 ViewModel

作用 :在 MainWindow 初始化时创建 MainViewModel 实例,并赋值给 DataContextDataContext 是 WPF 绑定的核心------所有子控件的 {Binding} 都从这个对象上查找属性。

csharp 复制代码
using System.Windows;

namespace UpperMachine
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            // 创建 ViewModel 实例并设为 DataContext
            // 从此,所有子控件的 {Binding} 都指向这个 MainViewModel 对象
            DataContext = new ViewModels.MainViewModel();
        }
    }
}

为什么只有这一行代码就够了?

因为所有逻辑(属性定义、通知机制)都已经写在 MainViewModel.cs 里了。下面这个图清晰展示了数据流:

复制代码
用户操作 / PLC 数据
        ↓
ViewModel.IsConnected = true
        ↓
OnPropertyChanged("IsConnected")
        ↓
WPF 收到通知 → DataTrigger 检测到 true
        ↓
Ellipse.Fill = GreenBrush  (UI 自动更新)
TextBlock.Text = "Connected"(UI 自动更新)

六、MainWindow.xaml - 界面布局详解

6.1 整体 Grid 三行布局

xml 复制代码
<Window x:Class="UpperMachine.MainWindow"
        Title="四轴运动控制系统" Height="850" Width="1280"
        MinHeight="700" MinWidth="1024"
        WindowStartupLocation="CenterScreen"
        Background="{StaticResource BackgroundBrush}"
        FontFamily="Microsoft Himalaya">

    <!-- 根 Grid,分成三行 -->
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="60"/>    <!-- 顶栏:固定 60px -->
            <RowDefinition Height="*"/>    <!-- 中间:自动撑满 -->
            <RowDefinition Height="50"/>   <!-- 底栏:固定 50px -->
        </Grid.RowDefinitions>

        <!-- 顶栏(Row 0)-->
        <!-- 中间 Frame(Row 1)-->
        <!-- 底栏(Row 2)-->
    </Grid>
</Window>
高度 用途
Row 0 60px 顶栏:标题 + IP + 连接状态
Row 1 *(剩余) Frame 容器:加载功能页面
Row 2 50px 底栏:四轴状态信息

6.2 顶栏 - 左侧标题区域

xml 复制代码
<!-- 顶栏 Border -->
<Border Grid.Row="0"
        Background="{StaticResource HeaderBrush}"
        BorderBrush="{StaticResource BorderBrush}"
        BorderThickness="0,0,0,1">
    <Grid Margin="20,0">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>  <!-- 左侧:标题 -->
            <ColumnDefinition Width="*"/>     <!-- 中间:弹性空间 -->
            <ColumnDefinition Width="Auto"/>  <!-- 右侧:IP + 状态 -->
        </Grid.ColumnDefinitions>

        <!-- 左侧:青色圆点 + 系统标题 -->
        <StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center">
            <Ellipse Width="14" Height="14"
                     Fill="{StaticResource AccentBrush}"
                     Margin="0,-5,12,0"/>
            <TextBlock Text="4-Axis Motion System" FontSize="22" FontWeight="Bold"
                       Foreground="{StaticResource TextBrush}"/>
        </StackPanel>

        <!-- 右侧:IP + 连接状态(见 6.3)-->
    </Grid>
</Border>

布局技巧Grid.ColumnDefinitions 使用 Auto / * / Auto 三列布局(左中右),这是 WPF 实现左右对齐的经典模式。


6.3 顶栏 - 右侧 IP + 连接状态

这部分是最复杂的绑定逻辑,包含了属性绑定、样式、触发器。

xml 复制代码
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">

    <!-- 1. IP 地址 -->
    <!-- {Binding IpAddress} → 从 DataContext 的 IpAddress 属性读值 -->
    <TextBlock Text="{Binding IpAddress}"
               Foreground="{StaticResource LabelBrush}"
               FontSize="30" Margin="0,0,15,0"
               VerticalAlignment="Center"/>

    <!-- 2. 连接指示灯 -->
    <Ellipse Width="10" Height="10" Margin="0,-10,8,0" VerticalAlignment="Center">
        <Ellipse.Style>
            <Style TargetType="Ellipse">
                <!-- 默认填充红色(断开状态) -->
                <Setter Property="Fill" Value="Red"/>
                <Style.Triggers>
                    <!-- 当 IsConnected=True 时,覆盖为绿色 -->
                    <DataTrigger Binding="{Binding IsConnected}" Value="True">
                        <Setter Property="Fill" Value="{StaticResource GreenBrush}"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </Ellipse.Style>
    </Ellipse>

    <!-- 3. 连接状态文字 -->
    <TextBlock VerticalAlignment="Center" FontSize="25"
               Foreground="{StaticResource TextBrush}">
        <TextBlock.Style>
            <Style TargetType="TextBlock">
                <!-- 默认显示 "DisConnect" -->
                <Setter Property="Text" Value="DisConnect"/>
                <Style.Triggers>
                    <!-- 当 IsConnected=True 时,显示 "Connected" -->
                    <DataTrigger Binding="{Binding IsConnected}" Value="True">
                        <Setter Property="Text" Value="Connected"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </TextBlock.Style>
    </TextBlock>

</StackPanel>
为什么不直接用绑定?
xml 复制代码
<!-- 这样写不行------Fill 需要 Brush 类型,IsConnected 是 bool 类型 -->
<Ellipse Fill="{Binding IsConnected}" />

因为类型不匹配Fill 期望一个 Brush 对象(如 RedGreenBrush),而 IsConnectedboolDataTrigger 的作用就是条件判断

IsConnected 值 DataTrigger 指示灯颜色 文字
false(默认) 不触发 🔴 Red DisConnect
true 触发覆盖 🟢 GreenBrush Connected

DataTrigger 的自动回退机制 :当 true 变回 false 时,DataTrigger 不再匹配,自动移除覆盖效果,恢复默认值(Red + "DisConnect")。


6.4 中间 Frame - 页面容器

xml 复制代码
<!-- Row 1:Frame 页面容器 -->
<Frame x:Name="MainFrame" Grid.Row="1"
       NavigationUIVisibility="Hidden" Focusable="False"/>

后续加载页面:

csharp 复制代码
MainFrame.Navigate(new SomePage());

6.5 底栏 - 四轴状态显示

xml 复制代码
<!-- Row 2:底栏 -->
<Border Grid.Row="2"
        Background="{StaticResource HeaderBrush}"
        BorderBrush="{StaticResource BorderBrush}"
        BorderThickness="0,1,0,0"
        Padding="15,0">
    <Grid HorizontalAlignment="Center">  <!-- 整组水平居中 -->
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>  <!-- Axis1: 文字 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis1: 指示灯 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis2: 文字 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis2: 指示灯 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis3: 文字 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis3: 指示灯 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis4: 文字 -->
            <ColumnDefinition Width="Auto"/>  <!-- Axis4: 指示灯 -->
        </Grid.ColumnDefinitions>

        <!-- Axis 1 -->
        <TextBlock Grid.Column="0" Text="{Binding Axis1Status}"
                   FontSize="28" VerticalAlignment="Center"
                   Foreground="{StaticResource LabelBrush}"/>
        <Ellipse Grid.Column="1" Width="8" Height="8"
                 Fill="{StaticResource GreenBrush}"
                 Margin="10,-5,80,0" VerticalAlignment="Center"/>

        <!-- Axis 2 (同 Axis 1 结构) -->
        <TextBlock Grid.Column="2" Text="{Binding Axis2Status}"
                   FontSize="28" VerticalAlignment="Center"
                   Foreground="{StaticResource LabelBrush}"/>
        <Ellipse Grid.Column="3" Width="8" Height="8"
                 Fill="{StaticResource GreenBrush}"
                 Margin="10,-5,80,0" VerticalAlignment="Center"/>

        <!-- Axis 3 -->
        <TextBlock Grid.Column="4" Text="{Binding Axis3Status}"
                   FontSize="28" VerticalAlignment="Center"
                   Foreground="{StaticResource LabelBrush}"/>
        <Ellipse Grid.Column="5" Width="8" Height="8"
                 Fill="{StaticResource GreenBrush}"
                 Margin="10,-5,80,0" VerticalAlignment="Center"/>

        <!-- Axis 4 (最后一个不加右 Margin) -->
        <TextBlock Grid.Column="6" Text="{Binding Axis4Status}"
                   FontSize="28" VerticalAlignment="Center"
                   Foreground="{StaticResource LabelBrush}"/>
        <Ellipse Grid.Column="7" Width="8" Height="8"
                 Fill="{StaticResource GreenBrush}"
                 Margin="10,-5,8,0" VerticalAlignment="Center"/>
    </Grid>
</Border>

HorizontalAlignment="Center" 为什么加在 Grid 上?

底栏的 Grid 有 8 列,全部是 Width="Auto"(宽度=内容宽度)。如果 Grid 默认 Stretch(填满整行),则 8 列内容会贴在左侧

复制代码
默认 Stretch:                   加了 HorizontalAlignment="Center":
[Axis1][●]  [Axis2][●] ...           [Axis1][●]  [Axis2][●] ...
^ 左对齐                              ^^^^^^^ 整组居中 ^^^^^^^

关键理解Auto 列的子元素没有多余空间可以「水平居中」------列宽就等于内容宽度。要控制整组位置,必须加在容器(Grid)上


七、核心机制详解

7.1 DataContext - 绑定的「目标对象」

csharp 复制代码
// MainWindow.xaml.cs
public MainWindow()
{
    InitializeComponent();
    DataContext = new ViewModels.MainViewModel();
}
  • DataContextDependencyProperty会沿视觉树自动向下传递

  • 所有子控件的 {Binding} 都从这个对象查找属性

  • 一个窗口只能有一个 DataContext(但不同区域可以覆盖)

    Window.DataContext = MainViewModel
    ├── Border (继承 DataContext = MainViewModel)
    │ ├── StackPanel (继承 DataContext = MainViewModel)
    │ │ ├── TextBlock {Binding IpAddress} → 去 MainViewModel.IpAddress 读值
    │ │ ├── Ellipse {Binding IsConnected} → 去 MainViewModel.IsConnected 读值
    │ │ └── ...
    │ └── ...
    ├── Frame (继承 DataContext = MainViewModel)
    └── Border (继承 DataContext = MainViewModel)
    └── Grid (继承 DataContext = MainViewModel)
    ├── TextBlock {Binding Axis1Status} → 去 MainViewModel.Axis1Status 读值
    └── ...


7.2 Binding - 声明式数据连接

xml 复制代码
<TextBlock Text="{Binding IpAddress}" />

等价于:

csharp 复制代码
// TextBlock 的 Text 属性 = DataContext.IpAddress 的当前值
// 如果 IpAddress 变了 → OnPropertyChanged 通知 → WPF 刷新 TextBlock

7.3 INotifyPropertyChanged + CallerMemberName

完整的数据通知链条:

csharp 复制代码
// 1. ViewModel 实现 INotifyPropertyChanged
class MainViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    // 2. 通知方法,[CallerMemberName] 自动填入属性名
    protected void OnPropertyChanged([CallerMemberName] string? name = null)
        => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    
    // 3. 属性 setter 中调用通知
    private string _ipAddress = "192.168.0.10:502";
    public string IpAddress
    {
        get => _ipAddress;
        set
        {
            _ipAddress = value;
            OnPropertyChanged();  // 编译器自动补为 OnPropertyChanged("IpAddress")
        }
    }
}

执行流程:

复制代码
属性值变化 → setter → OnPropertyChanged("IpAddress")
    ↓
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("IpAddress"))
    ↓
WPF 收到通知 → 找到绑定了 "IpAddress" 的所有控件
    ↓
重新读取 IpAddress 属性的 getter
    ↓
更新 TextBlock.Text 显示新值

7.4 DataTrigger - 条件触发覆盖

xml 复制代码
<Ellipse Width="10" Height="10">
    <Ellipse.Style>
        <Style TargetType="Ellipse">
            <!-- 默认值(始终生效的兜底) -->
            <Setter Property="Fill" Value="Red"/>

            <Style.Triggers>
                <!-- 当 IsConnected = true 时,覆盖 Fill -->
                <DataTrigger Binding="{Binding IsConnected}" Value="True">
                    <Setter Property="Fill" Value="{StaticResource GreenBrush}"/>
                </DataTrigger>
            </Style.Triggers>
        </Style>
    </Ellipse.Style>
</Ellipse>

重点理解

  1. 默认值<Setter Property="Fill" Value="Red"/> 是「兜底」,条件不匹配时使用

  2. 只匹配 true :DataTrigger 只有匹配条件,没有 elsetrue 时覆盖,false 时自动恢复默认

  3. 动态回退true → false 时,DataTrigger 自动解除,恢复为 Red

    时间线:
    false ───── true ───── false
    红色 绿色 红色
    (默认) (触发覆盖) (自动恢复默认)


八、启动与运行流程

复制代码
1. App.xaml → StartupUri="MainWindow.xaml"
        ↓
2. MainWindow 构造函数
   ├── InitializeComponent()       ← 解析 XAML 创建控件树
   │   ├── 解析 Grid 三行布局
   │   ├── 解析顶栏 Border → 标题 / IP / 状态
   │   ├── 解析 Frame
   │   └── 解析底栏 Border → 四轴状态
   │
   └── DataContext = new MainViewModel()
       │
       ├── WPF 发现 {Binding IpAddress}
       │   → 自动读取 MainViewModel.IpAddress = "192.168.0.10:502"
       │   → 显示在 TextBlock 上
       │
       ├── WPF 发现 {Binding IsConnected} 的 DataTrigger
       │   → 读取 IsConnected = true
       │   → DataTrigger 匹配 → 指示灯变绿 + "Connected"
       │
       └── WPF 发现 {Binding Axis1Status~Axis4Status}
           → 自动读取 ViewModel 对应的属性值
           → 显示在底栏四个 TextBlock 上
                ↓
3. 窗口显示:顶栏(标题 | IP + 绿灯 + Connected) | Frame(空白) | 底栏(四轴状态)

九、总结与下一步

本博客已完成的内容

模块 功能 涉及技术
App.xaml 全局 14 色画刷 StaticResource
MainViewModel IP、连接状态、4 轴状态 INotifyPropertyChanged[CallerMemberName]
MainWindow.xaml.cs 绑定 ViewModel DataContext
MainWindow.xaml - 顶栏 标题 + IP + 指示 BindingDataTriggerStyle
MainWindow.xaml - 底栏 四轴状态 Grid 居中、Auto 列布局

下一步计划

阶段 内容
第二篇 Models 数据层 + PLC 通信接口设计
第三篇 Page 功能页面编写 + Frame 导航
第四篇 串口/NModbus4 通信集成
第五篇 数据绑定深化 + 多轴实时监控

本文基于 .NET 10 + WPF 编写,所有代码均已通过 dotnet build && dotnet run 验证。如果有任何问题或建议,欢迎交流讨论。