WPF 上位机学习日记(五)— 右侧面板 UI 布局全记录

WPF 上位机学习日记(五)--- 右侧面板 UI 布局全记录

日期: 2026-06-23

项目: UpperMachine(四轴运动控制)


今日内容

今天没有写任何 C# 后台代码,一整天全部花在 XAML UI 布局上。完成了 HomePage 主页面右侧操作面板 + 导航面板的设计,以及相关的英文命名校正。


一、右侧面板整体结构

复制代码
HomePage Grid (2行 × 3列)
├── Row 0, Col 0 → Axis 1 数据卡片
├── Row 0, Col 1 → Axis 2 数据卡片
├── Row 1, Col 0 → Axis 3 数据卡片
├── Row 1, Col 1 → Axis 4 数据卡片
└── Row 0, Col 2, RowSpan=2 → 右侧面板 (280px宽)
    └── 内层 Grid (3行)
        ├── Row 0 (Auto) → 操作按钮区(Control Panel)
        ├── Row 1 (Auto) → 分隔线(Rectangle)
        └── Row 2 (*)    → 功能导航区(Navigation)

关键设计决策

决策 原因
右侧面板用 RowSpan="2" 跨两行高度,顶部到轴卡平齐,底部也与轴卡平齐
Row 0 用 Height="Auto" 操作按钮内容高度不定,让内容自己撑开
Row 1 用 Height="Auto" 分隔线只有 3px 高
Row 2 用 Height="*" 导航区吃掉剩余所有高度,自动填满
内层用 Grid 而非 StackPanel 做 3 行 Grid 的 * 行能撑满,StackPanel 不能

二、遇到的问题 & 学到的知识点

1. 按钮看不见/被裁剪

现象: 内层 Grid Row 0 固定 Height="80" 时,按钮不显示。

原因: Row 0 固定 80px,但内容(标题 + 分割线 + 操作按钮 + Margin)合计约 350px,按钮被裁剪在 80px 范围外。

解决: Height="80"Height="Auto",让内容自己决定行高。

2. StackPanel 不拉伸导航区

现象: 导航区边框只占一小块,下面空白。

原因: StackPanel 的特性是"子元素要多少给多少",不会把剩余空间分配给内部的 Border。

解决: Row 2 的外层容器从 StackPanel 改成 Grid,设置 RowDefinition Height="Auto"(标题)+ RowDefinition Height="*"(Border 撑满)。

复制代码
错误:StackPanel → Border 只在内容高度处,下面空白
正确:Grid (Auto + *) → Border 自动填满剩余空间

3. Margin 的四个值

WPF 的 Margin 是 左、上、右、下

复制代码
Margin="10"      → 左10 上10 右10 下10(四边统一)
Margin="10,20"   → 左10 上20 右10 下20(水平/垂直统一)
Margin="0,10,5,0" → 左0 上10 右5 下0(逐边指定)

4. Grid 不写 ColumnDefinitions 默认为 1 列

删掉 ColumnDefinitions 但保留 Grid.Column="1" → 按钮重叠。

5. Button 不支持 CornerRadius

WPF Button 默认没有圆角属性,需要用 Border 包裹 + ControlTemplate 重写 或者 外层 Border + 内部透明按钮 来实现圆角。


三、最终 HomePage.xaml 完整代码(逐行注释)

xml 复制代码
<!-- ==========================================
     HomePage.xaml --- 四轴运动控制主页面
     包含:4个轴数据卡片 + 右侧操作面板 + 导航面板
     ========================================== -->

<Page x:Class="UpperMachine.View.HomePage"
      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      mc:Ignorable="d"
      d:DesignHeight="600" d:DesignWidth="800"
      Title="HomePage"
      Background="{StaticResource BackgroundBrush}">

    <!-- ===== 外层 Grid:2行 × 3列 ===== -->
    <!-- 第0-1行放4个轴卡片,第2列280px做右侧面板 -->
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="280"/>
        </Grid.ColumnDefinitions>

        <!-- ====================== 轴卡片 Axis 1 ====================== -->
        <!-- CardBrush 为深蓝色背景,CornerRadius 10px 圆角 -->
        <Border Grid.Row="0" Grid.Column="0"
                Background="{StaticResource CardBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1"
                CornerRadius="10" Margin="5">
            <!-- StackPanel 垂直排列数据行,30px内边距 -->
            <StackPanel VerticalAlignment="Top" Margin="30">
                <!-- 标题行:青色圆点 + "Axis 1" 粗体文字 -->
                <StackPanel Orientation="Horizontal" Margin="0,0,0,15">
                    <Ellipse Width="12" Height="12"
                             Fill="{StaticResource AccentBrush}"
                             Margin="0,0,10,0" VerticalAlignment="Center"/>
                    <TextBlock Text="Axis 1" FontSize="30" FontWeight="Bold"
                              Foreground="{StaticResource TextBrush}"/>
                </StackPanel>
                <!-- Status 状态显示,绑定 ViewModel 的 Axis1Data.Status -->
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="Status:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis1Data.Status}" Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <!-- CurrentPos 当前位置,N2格式保留2位小数 -->
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="CurrentPos:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis1Data.Data.CurrentPos, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <!-- CurrentVel 当前速度 -->
                <TextBlock FontSize="30">
                    <Run Text="CurrentVel:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis1Data.Data.CurrentVel, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <!-- CurrentTorque 当前扭矩 -->
                <TextBlock FontSize="30">
                    <Run Text="CurrentTorque:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis1Data.Data.CurrentTorque,StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
            </StackPanel>
        </Border>

        <!-- ====================== 轴卡片 Axis 2 ====================== -->
        <Border Grid.Row="0" Grid.Column="1"
                Background="{StaticResource CardBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1"
                CornerRadius="10" Margin="5">
            <StackPanel VerticalAlignment="Top" Margin="30">
                <StackPanel Orientation="Horizontal" Margin="0,0,0,15">
                    <Ellipse Width="12" Height="12"
                             Fill="{StaticResource AccentBrush}"
                             Margin="0,0,10,0" VerticalAlignment="Center"/>
                    <TextBlock Text="Axis 2" FontSize="30" FontWeight="Bold"
                              Foreground="{StaticResource TextBrush}"/>
                </StackPanel>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="Status:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis2Data.Status}" Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="CurrentPos:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis2Data.Data.CurrentPos, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentVel:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis2Data.Data.CurrentVel, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentTorque:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis2Data.Data.CurrentTorque,StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
            </StackPanel>
        </Border>

        <!-- ====================== 轴卡片 Axis 3 ====================== -->
        <Border Grid.Row="1" Grid.Column="0"
                Background="{StaticResource CardBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1"
                CornerRadius="10" Margin="5">
            <StackPanel VerticalAlignment="Top" Margin="30">
                <StackPanel Orientation="Horizontal" Margin="0,0,0,15">
                    <Ellipse Width="12" Height="12"
                             Fill="{StaticResource AccentBrush}"
                             Margin="0,0,10,0" VerticalAlignment="Center"/>
                    <TextBlock Text="Axis 3" FontSize="30" FontWeight="Bold"
                              Foreground="{StaticResource TextBrush}"/>
                </StackPanel>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="Status:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis3Data.Status}" Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="CurrentPos:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis3Data.Data.CurrentPos, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentVel:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis3Data.Data.CurrentVel, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentTorque:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis3Data.Data.CurrentTorque,StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
            </StackPanel>
        </Border>

        <!-- ====================== 轴卡片 Axis 4 ====================== -->
        <Border Grid.Row="1" Grid.Column="1"
                Background="{StaticResource CardBrush}"
                BorderBrush="{StaticResource BorderBrush}"
                BorderThickness="1"
                CornerRadius="10" Margin="5">
            <StackPanel VerticalAlignment="Top" Margin="30">
                <StackPanel Orientation="Horizontal" Margin="0,0,0,15">
                    <Ellipse Width="12" Height="12"
                             Fill="{StaticResource AccentBrush}"
                             Margin="0,0,10,0" VerticalAlignment="Center"/>
                    <TextBlock Text="Axis 4" FontSize="30" FontWeight="Bold"
                              Foreground="{StaticResource TextBrush}"/>
                </StackPanel>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="Status:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis4Data.Status}" Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30" Margin="0,0,0,8">
                    <Run Text="CurrentPos:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis4Data.Data.CurrentPos, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentVel:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis4Data.Data.CurrentVel, StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
                <TextBlock FontSize="30">
                    <Run Text="CurrentTorque:" Foreground="{StaticResource LabelBrush}"/>
                    <Run Text="{Binding Axis4Data.Data.CurrentTorque,StringFormat=N2}"
                         Foreground="{StaticResource TextBrush}"/>
                </TextBlock>
            </StackPanel>
        </Border>

        <!-- ======================== 右侧面板(核心新内容)==================== -->
        <!-- RowSpan="2":跨两行,与4个轴卡片等高 -->
        <Border Grid.Row="0" Grid.Column="2" Grid.RowSpan="2"
                Background="{StaticResource PanelBrush}"
                BorderBrush="{StaticResource BorderBrush}" Margin="5"
                CornerRadius="5" BorderThickness="1">

            <!-- 内层 Grid:3行结构 Auto / Auto / * -->
            <Grid>
                <Grid.RowDefinitions>
                    <RowDefinition Height="Auto"/>  <!-- Row 0: 操作按钮 -->
                    <RowDefinition Height="Auto"/>  <!-- Row 1: 分隔线 -->
                    <RowDefinition Height="*"/>     <!-- Row 2: 导航按钮(撑满) -->
                </Grid.RowDefinitions>

                <!-- ===== Row 0:控制面板(Control Panel) ===== -->
                <StackPanel Grid.Row="0" Margin="10">
                    <!-- Control Panel 标题 -->
                    <TextBlock Text="Control Panel" FontSize="28"
                               Foreground="{StaticResource AccentBrush}"
                               HorizontalAlignment="Center"
                               VerticalAlignment="Center"
                               FontWeight="Bold" Margin="0,0,0,10"/>

                    <!-- 分割线:3px高的水平彩色条 -->
                    <Rectangle Height="3" Fill="{StaticResource BorderBrush}"
                               Margin="0,0,0,15"/>

                    <!-- 按钮组卡片:CardBrush圆角背景包裹所有操作按钮 -->
                    <Border Background="{StaticResource CardBrush}"
                            CornerRadius="8" Padding="10"
                            BorderBrush="{StaticResource BorderBrush}"
                            BorderThickness="1">
                        <StackPanel>

                            <!-- 全宽按钮:Axis Param Settings(轴参数配置入口) -->
                            <Button Content="Axis Param Settings"
                                    FontSize="22" Background="{StaticResource PanelBrush}"
                                    Foreground="{StaticResource AccentBrush}"
                                    BorderThickness="0" Height="45" Margin="0,0,0,8"/>

                            <!-- 3列按钮:Start / Stop / Pause -->
                            <Grid Margin="0,0,0,8">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <!-- Start:绿色语义,表示运行/启动 -->
                                <Button Grid.Column="0" Content="Start"
                                        Background="{StaticResource GreenBrush}"
                                        Foreground="White" FontSize="20"
                                        BorderThickness="1" Height="45" Margin="0,0,5,0"/>
                                <!-- Stop:红色语义,表示停止 -->
                                <Button Grid.Column="1" Content="Stop"
                                        Background="{StaticResource RedBrush}"
                                        Foreground="White" FontSize="20"
                                        BorderThickness="1" Height="45" Margin="5,0,5,0"/>
                                <!-- Pause:蓝色语义,表示暂停 -->
                                <Button Grid.Column="2" Content="Pause"
                                        Margin="5" Background="{StaticResource BlueBrush}"
                                        FontSize="20" Height="45"
                                        BorderThickness="1" Foreground="White"/>
                            </Grid>

                            <!-- 急停按钮:全宽、加大加粗,EmergencyRedBrush深红 -->
                            <Button Content="⚠ Emergency Stop" FontSize="22"
                                    FontWeight="Bold"
                                    Background="{StaticResource EmergencyRedBrush}"
                                    Foreground="White" BorderThickness="0" Height="55"/>

                            <!-- 2列按钮:Return Home / Manual Adjust -->
                            <Grid Margin="0,15,0,0">
                                <Grid.ColumnDefinitions>
                                    <ColumnDefinition Width="*"/>
                                    <ColumnDefinition Width="*"/>
                                </Grid.ColumnDefinitions>
                                <!-- Return Home:回原,橙色 -->
                                <Button Grid.Column="0" Content="Return Home"
                                        FontSize="12" Background="{StaticResource OrangeBrush}"
                                        Foreground="white" BorderThickness="0"
                                        Height="55" FontWeight="Bold" Margin="0,0,5,0"/>
                                <!-- Manual Adjust:手动调节,CadetBlue -->
                                <Button Grid.Column="1" Content="Manual Adjust"
                                        FontSize="12" Background="{StaticResource CadeBlueBrush}"
                                        Foreground="White" Height="55"
                                        FontWeight="Bold" Margin="5,0,0,0"/>
                            </Grid>

                        </StackPanel>
                    </Border>
                </StackPanel>

                <!-- ===== Row 1:操作区与导航区的分隔线 ===== -->
                <Rectangle Grid.Row="1" Height="3"
                           Fill="{StaticResource BorderBrush}"
                           Margin="0,5,0,0"/>

                <!-- ===== Row 2:功能导航区 ===== -->
                <!-- 用 Grid 而非 StackPanel 让 Border 能撑满剩余高度 -->
                <Grid Grid.Row="2" Margin="10">
                    <Grid.RowDefinitions>
                        <RowDefinition Height="Auto"/>  <!-- 标题行 -->
                        <RowDefinition Height="*"/>      <!-- 按钮卡片行(撑满) -->
                    </Grid.RowDefinitions>

                    <!-- Navigation 标题 -->
                    <TextBlock Grid.Row="0" Text="Navigation"
                               FontSize="15" Foreground="{StaticResource AccentBrush}"
                               HorizontalAlignment="Center" FontWeight="Heavy"/>

                    <!-- 导航按钮卡片:Border 因为 Row 1 是 *,自动撑满 -->
                    <Border Grid.Row="1"
                            Background="{StaticResource CardBrush}"
                            CornerRadius="8" Margin="0,10,0,0"
                            BorderBrush="{StaticResource BorderBrush}"
                            BorderThickness="1">
                        <!-- 内层 Grid:3行 × 2列,6个导航按钮 -->
                        <Grid Margin="10">
                            <Grid.RowDefinitions>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="*"/>
                                <RowDefinition Height="*"/>
                            </Grid.RowDefinitions>
                            <Grid.ColumnDefinitions>
                                <ColumnDefinition Width="*"/>
                                <ColumnDefinition Width="*"/>
                            </Grid.ColumnDefinitions>

                            <!-- Modbus Settings:通信配置入口 -->
                            <Button Grid.Row="0" Grid.Column="0"
                                    FontSize="17" Margin="0,10,5,0"
                                    Foreground="White"
                                    Background="{StaticResource AccentBrush}" Height="60">
                                <!-- 使用 TextBlock 支持多行文字换行 -->
                                <Button.Content>
                                    <TextBlock TextWrapping="Wrap" TextAlignment="Center">
                                        Modbus<LineBreak/>Settings
                                    </TextBlock>
                                </Button.Content>
                            </Button>

                            <!-- RealTime Monitor:实时监控入口 -->
                            <Button Grid.Row="0" Grid.Column="1"
                                    FontSize="17" Margin="5,10,0,0"
                                    Foreground="White"
                                    Background="{StaticResource AccentBrush}" Height="60">
                                <Button.Content>
                                    <TextBlock TextWrapping="Wrap" TextAlignment="Center">
                                        RealTime<LineBreak/>Monitor
                                    </TextBlock>
                                </Button.Content>
                            </Button>

                            <!-- I/O Monitor:IO点监控 -->
                            <Button Grid.Row="1" Grid.Column="0"
                                    Content="I/O Monitor" FontSize="17"
                                    Margin="0,10,5,0" Foreground="White"
                                    Background="{StaticResource GreenBrush}" Height="60"/>

                            <!-- Excel Export:报表导出入口 -->
                            <Button Grid.Row="1" Grid.Column="1"
                                    FontSize="17" Margin="5,10,0,0"
                                    Foreground="White"
                                    Background="{StaticResource OrangeBrush}" Height="60">
                                <Button.Content>
                                    <TextBlock TextWrapping="Wrap" TextAlignment="Center">
                                        Excel<LineBreak/>Export
                                    </TextBlock>
                                </Button.Content>
                            </Button>

                            <!-- Operation Log:日志查询入口 -->
                            <!-- Foreground="Black" 因为 YellowBrush 配白色看不清 -->
                            <Button Grid.Row="2" Grid.Column="0"
                                    FontSize="17" Margin="0,10,5,0"
                                    Foreground="Black"
                                    Background="{StaticResource YellowBrush}" Height="60">
                                <Button.Content>
                                    <TextBlock TextWrapping="Wrap" TextAlignment="Center">
                                        Operation<LineBreak/>Log
                                    </TextBlock>
                                </Button.Content>
                            </Button>

                            <!-- Tentative:占位按钮,待定功能 -->
                            <Button Grid.Row="2" Grid.Column="1"
                                    Content="Tentative" FontSize="17"
                                    Margin="5,10,0,0" Foreground="White"
                                    Background="{StaticResource BlueBrush}" Height="60"/>

                        </Grid>
                    </Border>
                </Grid>

            </Grid>
        </Border>
    </Grid>
</Page>

四、App.xaml 新增资源

今天新加了两个画刷:

xml 复制代码
<!-- 急停专用深红:比 RedBrush 更深,用来区分普通停止和紧急停止 -->
<SolidColorBrush x:Key="EmergencyRedBrush" Color="#B71C1C"/>

<!-- Manual Adjust 按钮背景色:Cadet Blue 青蓝 -->
<SolidColorBrush x:Key="CadeBlueBrush" Color="#5F9EA0"/>

五、今天踩过的坑汇总

# 问题 原因 解决
1 按钮不显示 Row 固定 80px,内容超了 Height="Auto"
2 导航框很小 StackPanel 不拉伸子元素 改用 Grid + *
3 两个按钮重叠 Grid.Column="1" 但没定义 2 列 ColumnDefinitions
4 CadeBlueBrush 报错 资源没在 App.xaml 定义 加在 App.xaml 中
5 Yellow 按钮字看不清 白色字 + 黄背景对比度低 Foreground="Black"
6 Margin 把按钮压没了 Margin="50" 四边各吃 50px 只加需要的方向

六、下一步计划

  1. 创建子页面 --- ParamSettingsPage、CommConfigPage、ManualAdjustPage 等
  2. 绑定按钮命令 --- Start/Stop/Pause/E-Stop 绑定到 ViewModel 的 RelayCommand
  3. 页面导航 --- 点击导航按钮通过 MainFrame.Navigate() 跳转
  4. AxisData 加 INotifyPropertyChanged --- 让状态绑定动态刷新
  5. 写回逻辑 --- ConvertFloatToPlc + WriteSingleRegister 发送指令到 PLC