003003002_WPF Grid 基类官方类定义逐行深度解析
摘要:本文基于 .NET 8 官方源码,从类层次结构、完整类定义、类级特性、附加属性注册、公共/附加属性详解、布局方法(Measure/Arrange/Clip)、网格工作原理(GridLength、测量流程、跨列、共享尺寸),到工业上位机三个典型应用实例,全面逐行解析 WPF Grid 基类的设计与实现,帮助开发者深入掌握二维网格布局的核心机制、三种尺寸类型逻辑及工业级布局最佳实践。
一、Grid 在 WPF 类层次结构中的位置
plaintext:
plaintext
System.Object
↳ System.Windows.Threading.DispatcherObject
↳ System.Windows.DependencyObject
↳ System.Windows.Media.Visual
↳ System.Windows.UIElement
↳ System.Windows.FrameworkElement
↳ System.Windows.Controls.Panel ← 所有布局容器的基类
↳ System.Windows.Controls.Grid ← 我们今天的主角
核心设计意义:
- 实现二维网格模型:将容器划分为行和列的单元格,子元素可以放置在任意单元格中。
- 支持三种尺寸类型:Auto(自动适应内容)、Pixel(固定像素)、Star(按比例分配剩余空间)。
- 支持跨行跨列:子元素可以跨越多个行或列。
- 支持共享尺寸组:多个行或列可以保持相同的尺寸。
- 工业场景价值:几乎可以实现任何复杂的布局,是工业上位机开发的首选布局容器
二、完整官方类定义(.NET 8 源码级)
c#
using System.Windows.Automation.Peers;
using System.Windows.Media;
using System.Windows.Markup;
using System.Windows.Controls.Primitives;
namespace System.Windows.Controls
{
/// <summary>
/// 表示一个二维网格布局容器
/// </summary>
/// <remarks>
/// Grid 将容器划分为行和列的单元格,子元素可以放置在任意单元格中。
/// 支持三种尺寸类型:Auto(自动适应内容)、Pixel(固定像素)、Star(按比例分配剩余空间)。
/// 子元素可以通过 Row、Column、RowSpan、ColumnSpan 附加属性指定位置和跨度。
/// </remarks>
[ContentProperty("Children")]
[Localizability(LocalizationCategory.None)]
public class Grid : Panel, IAddChild
{
// ==============================================
// 依赖属性定义(Grid特有)
// ==============================================
public static readonly DependencyProperty RowDefinitionsProperty;
public static readonly DependencyProperty ColumnDefinitionsProperty;
public static readonly DependencyProperty ShowGridLinesProperty;
public static readonly DependencyProperty IsSharedSizeScopeProperty;
// ==============================================
// 附加属性定义(Grid特有)
// ==============================================
public static readonly DependencyProperty RowProperty;
public static readonly DependencyProperty ColumnProperty;
public static readonly DependencyProperty RowSpanProperty;
public static readonly DependencyProperty ColumnSpanProperty;
public static readonly DependencyProperty SharedSizeGroupProperty;
// ==============================================
// 静态构造函数
// ==============================================
static Grid()
{
// 注册依赖属性
RowDefinitionsProperty = DependencyProperty.Register(
nameof(RowDefinitions),
typeof(RowDefinitionCollection),
typeof(Grid),
new FrameworkPropertyMetadata(null));
ColumnDefinitionsProperty = DependencyProperty.Register(
nameof(ColumnDefinitions),
typeof(ColumnDefinitionCollection),
typeof(Grid),
new FrameworkPropertyMetadata(null));
ShowGridLinesProperty = DependencyProperty.Register(
nameof(ShowGridLines),
typeof(bool),
typeof(Grid),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender));
IsSharedSizeScopeProperty = DependencyProperty.RegisterAttached(
nameof(IsSharedSizeScope),
typeof(bool),
typeof(Grid),
new FrameworkPropertyMetadata(false));
// 注册附加属性
RowProperty = DependencyProperty.RegisterAttached(
nameof(Row),
typeof(int),
typeof(Grid),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentMeasure),
new ValidateValueCallback(IsValidRowColumn));
ColumnProperty = DependencyProperty.RegisterAttached(
nameof(Column),
typeof(int),
typeof(Grid),
new FrameworkPropertyMetadata(0,
FrameworkPropertyMetadataOptions.AffectsParentMeasure),
new ValidateValueCallback(IsValidRowColumn));
RowSpanProperty = DependencyProperty.RegisterAttached(
nameof(RowSpan),
typeof(int),
typeof(Grid),
new FrameworkPropertyMetadata(1,
FrameworkPropertyMetadataOptions.AffectsParentMeasure),
new ValidateValueCallback(IsValidSpan));
ColumnSpanProperty = DependencyProperty.RegisterAttached(
nameof(ColumnSpan),
typeof(int),
typeof(Grid),
new FrameworkPropertyMetadata(1,
FrameworkPropertyMetadataOptions.AffectsParentMeasure),
new ValidateValueCallback(IsValidSpan));
SharedSizeGroupProperty = DependencyProperty.RegisterAttached(
nameof(SharedSizeGroup),
typeof(string),
typeof(Grid),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.AffectsParentMeasure));
// 重写默认样式键
DefaultStyleKeyProperty.OverrideMetadata(
typeof(Grid),
new FrameworkPropertyMetadata(typeof(Grid)));
}
// ==============================================
// 公共构造函数
// ==============================================
public Grid();
// ==============================================
// 公共属性
// ==============================================
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RowDefinitionCollection RowDefinitions { get; }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ColumnDefinitionCollection ColumnDefinitions { get; }
[Bindable(true)]
[Category("Appearance")]
public bool ShowGridLines { get; set; }
// ==============================================
// 附加属性访问器方法
// ==============================================
public static int GetRow(UIElement element);
public static void SetRow(UIElement element, int value);
public static int GetColumn(UIElement element);
public static void SetColumn(UIElement element, int value);
public static int GetRowSpan(UIElement element);
public static void SetRowSpan(UIElement element, int value);
public static int GetColumnSpan(UIElement element);
public static void SetColumnSpan(UIElement element, int value);
public static string GetSharedSizeGroup(DefinitionBase definition);
public static void SetSharedSizeGroup(DefinitionBase definition, string value);
public static bool GetIsSharedSizeScope(DependencyObject element);
public static void SetIsSharedSizeScope(DependencyObject element, bool value);
// ==============================================
// 受保护内部属性(逻辑导航)
// ==============================================
protected internal override bool HasLogicalOrientation { get; }
protected internal override Orientation LogicalOrientation { get; }
// ==============================================
// 受保护方法(布局核心)
// ==============================================
protected override AutomationPeer OnCreateAutomationPeer();
protected override Size MeasureOverride(Size constraint);
protected override Size ArrangeOverride(Size arrangeSize);
protected override Geometry GetLayoutClip(Size layoutSlotSize);
protected override void OnVisualChildrenChanged(DependencyObject visualAdded, DependencyObject visualRemoved);
private static bool IsValidRowColumn(object value);
private static bool IsValidSpan(object value);
}
}
三、类级特性逐行解析
1. [ContentProperty("Children")]
c#
[ContentProperty("Children")]
- 作用:指定控件的默认内容属性
- 设计意图 :允许在 XAML 中直接编写子元素,无需显式指定
Grid.Children标签 - 核心意义:极大简化 XAML 代码,提高开发效率
2. IAddChild 接口实现
c#
public class Grid : Panel, IAddChild
-
核心意义 :支持在 XAML 中直接添加
RowDefinition和ColumnDefinition,而不需要显式指定Grid.RowDefinitions和Grid.ColumnDefinitions标签 -
示例:
xaml<!-- 简化写法 --> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> </Grid>
四、静态构造函数解析(核心初始化逻辑)
静态构造函数是 Grid 最关键的部分,负责所有核心依赖属性和附加属性的注册。
1. RowDefinitionsProperty 和 ColumnDefinitionsProperty 注册
c#
RowDefinitionsProperty = DependencyProperty.Register(
nameof(RowDefinitions),
typeof(RowDefinitionCollection),
typeof(Grid),
new FrameworkPropertyMetadata(null));
ColumnDefinitionsProperty = DependencyProperty.Register(
nameof(ColumnDefinitions),
typeof(ColumnDefinitionCollection),
typeof(Grid),
new FrameworkPropertyMetadata(null));
- 类型 :
RowDefinitionCollection和ColumnDefinitionCollection - 默认值 :
null(构造函数中会初始化空集合) - 核心作用:存储 Grid 的行和列定义
2. ShowGridLinesProperty 注册
c#
ShowGridLinesProperty = DependencyProperty.Register(
nameof(ShowGridLines),
typeof(bool),
typeof(Grid),
new FrameworkPropertyMetadata(false,
FrameworkPropertyMetadataOptions.AffectsRender));
- 类型 :
bool - 默认值 :
false - 元数据标志 :
AffectsRender(属性变化会影响渲染) - 核心作用:控制是否显示网格线,主要用于调试布局
3. 核心附加属性注册
Grid 的所有核心功能都通过附加属性实现,这是 Grid 区别于其他布局容器的本质特征:
RowProperty:指定子元素所在的行ColumnProperty:指定子元素所在的列RowSpanProperty:指定子元素跨越的行数ColumnSpanProperty:指定子元素跨越的列数SharedSizeGroupProperty:指定行/列所属的共享尺寸组IsSharedSizeScopeProperty:指定某个元素是否是共享尺寸范围的根
五、核心属性逐行解析
5.1 公共属性
1. RowDefinitions 和 ColumnDefinitions
c#
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RowDefinitionCollection RowDefinitions { get; }
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ColumnDefinitionCollection ColumnDefinitions { get; }
-
类型 :
RowDefinitionCollection和ColumnDefinitionCollection -
核心作用:定义 Grid 的行和列
-
每个 RowDefinition/ColumnDefinition 包含一个
Height/Width属性 ,类型为GridLength,支持三种尺寸类型:Auto:自动适应内容大小。Pixel:固定像素值(如100)。Star:按比例分配剩余空间(如*、2*)。
示例:
xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <!-- 自动适应内容 -->
<RowDefinition Height="50"/> <!-- 固定50像素 -->
<RowDefinition Height="*"/> <!-- 占剩余空间的1/3 -->
<RowDefinition Height="2*"/> <!-- 占剩余空间的2/3 -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
</Grid>
2. ShowGridLines
c#
[Bindable(true)]
[Category("Appearance")]
public bool ShowGridLines { get; set; }
- 作用:控制是否显示网格线。
- 默认值 :
false - 设计意图:主要用于调试布局,帮助开发者查看单元格的边界。
- 注意 :显示的网格线是虚线,仅用于调试,生产环境应设置为
false。
5.2 附加属性(Grid 的灵魂)
1. Row 和 Column 附加属性
c#
public static int GetRow(UIElement element);
public static void SetRow(UIElement element, int value);
public static int GetColumn(UIElement element);
public static void SetColumn(UIElement element, int value);
- 作用:指定子元素所在的行和列。
- 默认值 :
0(第一行/第一列)。 - 验证回调 :
IsValidRowColumn,确保值大于等于 0。
示例:
xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Content="(0,0)" Grid.Row="0" Grid.Column="0"/>
<Button Content="(0,1)" Grid.Row="0" Grid.Column="1"/>
<Button Content="(1,0)" Grid.Row="1" Grid.Column="0"/>
<Button Content="(1,1)" Grid.Row="1" Grid.Column="1"/>
</Grid>
2. RowSpan 和 ColumnSpan 附加属性
c#
public static int GetRowSpan(UIElement element);
public static void SetRowSpan(UIElement element, int value);
public static int GetColumnSpan(UIElement element);
public static void SetColumnSpan(UIElement element, int value);
- 作用:指定子元素跨越的行数和列数。
- 默认值 :
1(不跨越)。 - 验证回调 :
IsValidSpan,确保值大于等于 1。 - 工业场景应用:合并单元格,实现复杂的布局结构。
示例:
xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 跨越2行 -->
<Button Content="跨越2行" Grid.Row="0" Grid.Column="0" Grid.RowSpan="2"/>
<!-- 跨越2列 -->
<Button Content="跨越2列" Grid.Row="0" Grid.Column="1" Grid.ColumnSpan="2"/>
<!-- 跨越2行2列 -->
<Button Content="跨越2行2列" Grid.Row="1" Grid.Column="1" Grid.RowSpan="2" Grid.ColumnSpan="2"/>
</Grid>
3. SharedSizeGroup 和 IsSharedSizeScope 附加属性
c#
public static string GetSharedSizeGroup(DefinitionBase definition);
public static void SetSharedSizeGroup(DefinitionBase definition, string value);
public static bool GetIsSharedSizeScope(DependencyObject element);
public static void SetIsSharedSizeScope(DependencyObject element, bool value);
- 作用:使多个行或列保持相同的尺寸
SharedSizeGroup:为行 / 列指定一个组名,同一组的行 / 列会保持相同的尺寸IsSharedSizeScope:指定某个元素是否是共享尺寸范围的根,只有在同一个范围内的同组行 / 列才会保持相同尺寸- 工业场景应用:多个参数表单的标签列保持相同宽度,使界面更加整齐
示例:
xaml
<!-- 父容器设置为共享尺寸范围 -->
<StackPanel Grid.IsSharedSizeScope="True">
<!-- 第一个表单 -->
<Grid Margin="0 0 0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelColumn"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="设备编号:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding DeviceId}"/>
</Grid>
<!-- 第二个表单,标签列宽度与第一个表单相同 -->
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelColumn"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<TextBlock Grid.Column="0" Text="设备名称:" VerticalAlignment="Center"/>
<TextBox Grid.Column="1" Text="{Binding DeviceName}"/>
</Grid>
</StackPanel>
六、受保护方法逐行解析(布局核心)
Grid 重写了 Panel 基类的多个核心方法,实现了复杂的二维网格布局逻辑。
1. MeasureOverride() 方法(最复杂)
c#
protected override Size MeasureOverride(Size constraint);
-
触发时机:当 Grid 需要测量自身大小时调用
-
核心逻辑(简化版):
csharpprotected override Size MeasureOverride(Size constraint) { // 1. 初始化行和列的尺寸信息 InitializeRowColumnSizes(); // 2. 第一遍测量:测量所有Auto和固定尺寸的行/列 MeasureAutoAndFixedRowsColumns(constraint); // 3. 计算剩余空间 double remainingWidth = constraint.Width - totalFixedWidth; double remainingHeight = constraint.Height - totalFixedHeight; // 4. 第二遍测量:按比例分配剩余空间给*号行/列 MeasureStarRowsColumns(remainingWidth, remainingHeight); // 5. 测量所有子元素 MeasureAllChildren(); // 6. 返回总大小 return new Size(totalWidth, totalHeight); } -
设计意图:实现三种尺寸类型的测量逻辑,特别是 * 号比例的计算
2. ArrangeOverride() 方法
c#
protected override Size ArrangeOverride(Size arrangeSize);
-
触发时机:当 Grid 需要排列子元素时调用
-
核心逻辑:
- 根据测量结果计算每个单元格的位置和大小
- 遍历所有子元素,根据其 Row、Column、RowSpan、ColumnSpan 属性计算排列矩形
- 排列所有子元素
- 返回最终大小
3. GetLayoutClip() 方法
c#
protected override Geometry GetLayoutClip(Size layoutSlotSize);
- 触发时机:当 WPF 需要确定控件的裁剪区域时调用
- 官方实现:返回一个与 Grid 大小相同的矩形,裁剪所有超出边界的子元素
- 设计意图:确保子元素不会超出 Grid 的边界
七、Grid 核心工作原理
7.1 GridLength 三种尺寸类型详解
Grid 的布局核心是 GridLength 结构,它支持三种尺寸类型:
| 类型 | 语法 | 说明 | 优先级 |
|---|---|---|---|
| Auto | Height="Auto" |
自动适应内容大小,内容多大,行/列就多大 | 最高 |
| Pixel | Height="100" |
固定像素值,无论内容和容器大小如何,都保持固定尺寸 | 中 |
| Star | Height="*"、Height="2*" |
按比例分配剩余空间,总 * 数为分母,每个 * 数为分子 | 最低 |
7.2 完整测量流程
Grid 的测量分为两个阶段,这是它最复杂也最强大的部分:
-
第一阶段:测量 Auto 和固定尺寸
- 遍历所有行和列
- 对于 Auto 尺寸的行/列:测量其中所有子元素的大小,取最大值作为行/列的尺寸
- 对于固定尺寸的行/列:直接使用指定的像素值
- 累加所有 Auto 和固定尺寸的行/列的总宽度和总高度
-
第二阶段:分配剩余空间给 * 号行/列
- 计算剩余空间:总可用空间 - Auto 和固定尺寸的总大小
- 计算总 * 数:所有 * 号行/列的 * 数之和
- 每个号行 / 列的尺寸 = 剩余空间 × (当前数 / 总 * 数)
7.3 跨行跨列处理
当子元素设置了RowSpan或ColumnSpan时:
- 测量阶段:子元素会影响所有跨越的行/列的尺寸
- 排列阶段:子元素的排列矩形是跨越的所有单元格的合并矩形
7.4 共享尺寸组工作原理
- 当某个元素设置了
Grid.IsSharedSizeScope="True"时,它成为共享尺寸范围的根 - 在这个范围内,所有具有相同
SharedSizeGroup名称的行 / 列会被归为一组 - 测量阶段:同一组的所有行 / 列会取其中的最大值作为统一尺寸
- 排列阶段:同一组的所有行 / 列会使用这个统一尺寸进行排列
八、工业上位机典型应用实例
Grid 是工业上位机开发中最常用的布局容器,几乎所有复杂界面都使用 Grid 来实现。
实例 1:标准工业主界面框架
xaml
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25"/> <!-- 菜单栏 -->
<RowDefinition Height="40"/> <!-- 工具栏 -->
<RowDefinition Height="*"/> <!-- 内容区域 -->
<RowDefinition Height="25"/> <!-- 状态栏 -->
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200"/> <!-- 左侧导航 -->
<ColumnDefinition Width="*"/> <!-- 右侧内容 -->
</Grid.ColumnDefinitions>
<!-- 菜单栏(跨越2列) -->
<Menu Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2">
<MenuItem Header="文件"/>
<MenuItem Header="编辑"/>
<MenuItem Header="视图"/>
</Menu>
<!-- 工具栏(跨越2列) -->
<ToolBar Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2">
<Button Content="启动" Width="60" Height="30"/>
<Button Content="停止" Width="60" Height="30"/>
</ToolBar>
<!-- 左侧导航 -->
<TreeView Grid.Row="2" Grid.Column="0" Background="#F5F5F5">
<TreeViewItem Header="生产监控" IsExpanded="True">
<TreeViewItem Header="实时数据"/>
<TreeViewItem Header="趋势曲线"/>
</TreeViewItem>
</TreeView>
<!-- 右侧内容区域 -->
<Border Grid.Row="2" Grid.Column="1" Background="White">
<ContentControl Content="{Binding CurrentViewModel}"/>
</Border>
<!-- 状态栏(跨越2列) -->
<StatusBar Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2">
<StatusBarItem Content="系统状态:运行中"/>
</StatusBar>
</Grid>
实例 2:参数输入表单
xaml
<GroupBox Header="设备参数" Margin="10">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" SharedSizeGroup="LabelColumn"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 设备编号 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="设备编号:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="0" Grid.Column="1" Text="{Binding DeviceId}" Height="30" Margin="0 0 0 10"/>
<!-- 设备名称 -->
<TextBlock Grid.Row="1" Grid.Column="0" Text="设备名称:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="1" Grid.Column="1" Text="{Binding DeviceName}" Height="30" Margin="0 0 0 10"/>
<!-- 生产速度 -->
<TextBlock Grid.Row="2" Grid.Column="0" Text="生产速度:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="2" Grid.Column="1" Text="{Binding ProductionSpeed}" Height="30" Margin="0 0 0 10"/>
<!-- 温度上限 -->
<TextBlock Grid.Row="3" Grid.Column="0" Text="温度上限:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="3" Grid.Column="1" Text="{Binding TemperatureUpper}" Height="30" Margin="0 0 0 10"/>
<!-- 压力上限 -->
<TextBlock Grid.Row="4" Grid.Column="0" Text="压力上限:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="4" Grid.Column="1" Text="{Binding PressureUpper}" Height="30" Margin="0 0 0 10"/>
<!-- 报警阈值 -->
<TextBlock Grid.Row="5" Grid.Column="0" Text="报警阈值:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="5" Grid.Column="1" Text="{Binding AlarmThreshold}" Height="30" Margin="0 0 0 10"/>
<!-- 设备 IP -->
<TextBlock Grid.Row="6" Grid.Column="0" Text="设备 IP:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="6" Grid.Column="1" Text="{Binding DeviceIP, UpdateSourceTrigger=PropertyChanged}" Height="30" Margin="0 0 0 10"/>
<!-- 端口号 -->
<TextBlock Grid.Row="7" Grid.Column="0" Text="端口号:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<TextBox Grid.Row="7" Grid.Column="1" Text="{Binding PortNumber, UpdateSourceTrigger=PropertyChanged}" Height="30" Margin="0 0 0 10"/>
<!-- 设备类型 -->
<TextBlock Grid.Row="8" Grid.Column="0" Text="设备类型:" VerticalAlignment="Center" Margin="0 0 10 10"/>
<ComboBox Grid.Row="8" Grid.Column="1"
ItemsSource="{Binding DeviceTypes}"
SelectedItem="{Binding SelectedDeviceType}"
Height="30" Margin="0 0 0 10"/>
<!-- 提交按钮(横跨两列,右对齐) -->
<Button Grid.Row="9" Grid.Column="0" Grid.ColumnSpan="2"
Content="提交参数"
Width="120" Height="35"
HorizontalAlignment="Right"
Margin="0 10 0 0"
Command="{Binding SubmitCommand}"
Style="{StaticResource PrimaryButtonStyle}"/>
</Grid>
</GroupBox>
csharp
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Windows.Input;
namespace IndustrialApp.ViewModels
{
/// <summary>
/// 设备参数 ViewModel,实现属性通知、数据验证和提交命令
/// </summary>
public class DeviceParameterViewModel : INotifyPropertyChanged, IDataErrorInfo
{
private string _deviceId;
private string _deviceName;
private string _productionSpeed;
private string _temperatureUpper;
private string _pressureUpper;
private string _alarmThreshold;
private string _deviceIP;
private string _portNumber;
private string _selectedDeviceType;
public List<string> DeviceTypes { get; } = new List<string> { "PLC", "DCS", "RTU", "智能仪表", "其他" };
public string DeviceId
{
get => _deviceId;
set { _deviceId = value; OnPropertyChanged(); }
}
public string DeviceName
{
get => _deviceName;
set { _deviceName = value; OnPropertyChanged(); }
}
public string ProductionSpeed
{
get => _productionSpeed;
set { _productionSpeed = value; OnPropertyChanged(); }
}
public string TemperatureUpper
{
get => _temperatureUpper;
set { _temperatureUpper = value; OnPropertyChanged(); }
}
public string PressureUpper
{
get => _pressureUpper;
set { _pressureUpper = value; OnPropertyChanged(); }
}
public string AlarmThreshold
{
get => _alarmThreshold;
set { _alarmThreshold = value; OnPropertyChanged(); }
}
public string DeviceIP
{
get => _deviceIP;
set { _deviceIP = value; OnPropertyChanged(); }
}
public string PortNumber
{
get => _portNumber;
set { _portNumber = value; OnPropertyChanged(); }
}
public string SelectedDeviceType
{
get => _selectedDeviceType;
set { _selectedDeviceType = value; OnPropertyChanged(); }
}
// ===== IDataErrorInfo 数据验证 =====
public string Error => null;
public string this[string columnName]
{
get
{
switch (columnName)
{
case nameof(DeviceId):
if (string.IsNullOrWhiteSpace(DeviceId))
return "设备编号不能为空";
break;
case nameof(DeviceName):
if (string.IsNullOrWhiteSpace(DeviceName))
return "设备名称不能为空";
break;
case nameof(DeviceIP):
if (!string.IsNullOrWhiteSpace(DeviceIP) && !IsValidIP(DeviceIP))
return "IP 地址格式不正确";
break;
case nameof(PortNumber):
if (!string.IsNullOrWhiteSpace(PortNumber) &&
(!int.TryParse(PortNumber, out int port) || port < 1 || port > 65535))
return "端口号必须在 1-65535 之间";
break;
}
return null;
}
}
private bool IsValidIP(string ip)
{
return Regex.IsMatch(ip, @"^(\d{1,3}\.){3}\d{1,3}$");
}
// ===== 提交命令 =====
public ICommand SubmitCommand { get; }
private bool CanSubmit(object parameter)
{
// 简单判断:必填项不能为空
return !string.IsNullOrWhiteSpace(DeviceId)
&& !string.IsNullOrWhiteSpace(DeviceName);
}
private void OnSubmit(object parameter)
{
// 实际提交逻辑(示例:控制台输出)
System.Diagnostics.Debug.WriteLine($"提交设备参数:{DeviceId} - {DeviceName}");
// 可在此处调用服务层保存数据
}
public DeviceParameterViewModel()
{
SubmitCommand = new RelayCommand(OnSubmit, CanSubmit);
}
// ===== INotifyPropertyChanged =====
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
// 命令可执行状态刷新
(SubmitCommand as RelayCommand)?.RaiseCanExecuteChanged();
}
}
/// <summary>
/// 通用 RelayCommand(简化版)
/// </summary>
public class RelayCommand : ICommand
{
private readonly Action<object> _execute;
private readonly Func<object, bool> _canExecute;
public RelayCommand(Action<object> execute, Func<object, bool> canExecute = null)
{
_execute = execute;
_canExecute = canExecute;
}
public event EventHandler CanExecuteChanged;
public bool CanExecute(object parameter) => _canExecute?.Invoke(parameter) ?? true;
public void Execute(object parameter) => _execute(parameter);
public void RaiseCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
实例 3:设备参数展示面板
xaml
<GroupBox Header="实时参数" Margin="10">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Border Grid.Row="0" Grid.Column="0" Background="#F5F5F5" Margin="5" Padding="10">
<StackPanel>
<TextBlock Text="温度" FontSize="12" Foreground="#757575"/>
<TextBlock Text="{Binding Temperature, StringFormat='{0:F1}℃'}" FontSize="18" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Grid.Row="0" Grid.Column="1" Background="#F5F5F5" Margin="5" Padding="10">
<StackPanel>
<TextBlock Text="压力" FontSize="12" Foreground="#757575"/>
<TextBlock Text="{Binding Pressure, StringFormat='{0:F1}MPa'}" FontSize="18" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Grid.Row="0" Grid.Column="2" Background="#F5F5F5" Margin="5" Padding="10">
<StackPanel>
<TextBlock Text="速度" FontSize="12" Foreground="#757575"/>
<TextBlock Text="{Binding Speed, StringFormat='{0:F1}m/s'}" FontSize="18" FontWeight="Bold"/>
</StackPanel>
</Border>
<Border Grid.Row="0" Grid.Column="3" Background="#F5F5F5" Margin="5" Padding="10">
<StackPanel>
<TextBlock Text="产量" FontSize="12" Foreground="#757575"/>
<TextBlock Text="{Binding Output}" FontSize="18" FontWeight="Bold"/>
</StackPanel>
</Border>
</Grid>
</GroupBox>
九、最佳实践与常见问题(工业场景必看)
9.1 最佳实践
-
合理使用三种尺寸类型:
- 标签、按钮等固定大小的元素使用
Auto - 工具栏、状态栏等固定高度的区域使用固定像素值
- 内容区域使用
*号分配剩余空间
- 标签、按钮等固定大小的元素使用
-
使用共享尺寸组:多个表单的标签列使用共享尺寸组,保持界面整齐
-
避免嵌套过多 Grid:嵌套超过 3 层会降低性能和可读性,复杂布局可以使用一个 Grid 实现
-
显式指定 Row 和 Column:即使是第 0 行 / 第 0 列,也建议显式写出,提高代码可读性
-
使用 ShowGridLines 调试布局:布局出现问题时,开启网格线帮助定位问题
-
避免使用过多的
*号 :*号比例过多会导致布局计算复杂,影响性能 -
工业界面保持简洁:避免过于复杂的网格结构,保持界面清晰易读
9.2 常见问题与解决方案### 9.2 常见问题与解决方案
问题 1:* 号比例不对
原因 :* 号是分配剩余空间 ,而不是总空间。Auto 和固定尺寸的行 / 列会先占用空间,剩余空间才按 * 号分配。
解决方案:检查是否有 Auto 或固定尺寸的行 / 列占用了过多空间。
问题 2:跨行跨列的元素显示不正确
可能原因:
-
RowSpan或ColumnSpan的值超过了实际的行数或列数 -
子元素的大小超过了合并后的单元格大小
解决方案:
-
子元素的大小超过了合并后的单元格大小
-
确保
RowSpan和ColumnSpan的值不超过实际的行数和列数 -
设置子元素的
HorizontalAlignment="Stretch"和VerticalAlignment="Stretch"
问题 3:共享尺寸组不生效
可能原因:
-
没有在父容器上设置
Grid.IsSharedSizeScope="True" -
共享尺寸组的名称拼写错误
-
行 / 列不在同一个共享尺寸范围内
解决方案
:
-
在父容器上设置
Grid.IsSharedSizeScope="True" -
检查共享尺寸组的名称是否正确
-
确保需要共享尺寸的行 / 列在同一个共享尺寸范围内
问题 4:性能问题
可能原因:
-
嵌套了过多的 Grid
-
有太多的行和列
-
子元素过于复杂
解决方案
:
-
减少 Grid 的嵌套层数,使用一个 Grid 实现复杂布局
-
减少不必要的行和列
-
简化子元素的 UI 结构
-
大数据量场景使用 DataGrid 并开启虚拟化
问题 5:子元素超出 Grid 边界
原因:Grid 默认会裁剪超出边界的子元素
解决方案:
- 如果需要显示超出边界的内容,设置
ClipToBounds="False" - 或者调整 Grid 的大小,确保子元素能够完全显示
十、官方设计意图总结
微软设计 Grid 的核心目标是:
- 提供最强大的布局能力:支持二维网格布局,几乎可以实现任何复杂的界面
- 灵活的尺寸控制:提供 Auto、Pixel、Star 三种尺寸类型,满足不同的布局需求
- 支持跨行跨列:允许子元素跨越多个行或列,实现复杂的布局结构
- 支持共享尺寸:使多个行或列保持相同的尺寸,提高界面的一致性
- 保持高性能:优化的布局算法,即使是复杂的网格布局也能保持良好的性能
- 易于理解和使用:直观的行和列定义,简单的附加属性,学习成本低
总结
Grid是 WPF 中最强大、最常用的布局容器,它的核心特性包括:
- 二维网格布局:将容器划分为行和列的单元格
- 三种尺寸类型:Auto(自动适应)、Pixel(固定像素)、Star(按比例分配)
- 跨行跨列:子元素可以跨越多个行或列
- 共享尺寸组:多个行或列可以保持相同的尺寸
- 高性能:优化的布局算法,支持复杂的网格结构
在工业上位机开发中,Grid 是实现主界面框架、参数表单、数据面板、复杂仪表盘的首选控件。掌握 Grid 的正确使用方法,特别是三种尺寸类型和跨行跨列的使用,可以开发出任何复杂的工业界面。