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 |
只加需要的方向 |
六、下一步计划
- 创建子页面 --- ParamSettingsPage、CommConfigPage、ManualAdjustPage 等
- 绑定按钮命令 --- Start/Stop/Pause/E-Stop 绑定到 ViewModel 的 RelayCommand
- 页面导航 --- 点击导航按钮通过
MainFrame.Navigate()跳转 - AxisData 加 INotifyPropertyChanged --- 让状态绑定动态刷新
- 写回逻辑 ---
ConvertFloatToPlc+WriteSingleRegister发送指令到 PLC