003003002_WPF Grid 基类官方类定义逐行深度解析

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 中直接添加 RowDefinitionColumnDefinition,而不需要显式指定 Grid.RowDefinitionsGrid.ColumnDefinitions 标签

  • 示例

    xaml 复制代码
    <!-- 简化写法 -->
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
    </Grid>

四、静态构造函数解析(核心初始化逻辑)

静态构造函数是 Grid 最关键的部分,负责所有核心依赖属性和附加属性的注册。

1. RowDefinitionsPropertyColumnDefinitionsProperty 注册

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));
  • 类型RowDefinitionCollectionColumnDefinitionCollection
  • 默认值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. RowDefinitionsColumnDefinitions
c# 复制代码
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public RowDefinitionCollection RowDefinitions { get; }

[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ColumnDefinitionCollection ColumnDefinitions { get; }
  • 类型RowDefinitionCollectionColumnDefinitionCollection

  • 核心作用:定义 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. RowColumn 附加属性
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. RowSpanColumnSpan 附加属性
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. SharedSizeGroupIsSharedSizeScope 附加属性
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 需要测量自身大小时调用

  • 核心逻辑(简化版)

    csharp 复制代码
    protected 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 需要排列子元素时调用

  • 核心逻辑

    1. 根据测量结果计算每个单元格的位置和大小
    2. 遍历所有子元素,根据其 Row、Column、RowSpan、ColumnSpan 属性计算排列矩形
    3. 排列所有子元素
    4. 返回最终大小

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 的测量分为两个阶段,这是它最复杂也最强大的部分:

  1. 第一阶段:测量 Auto 和固定尺寸

    • 遍历所有行和列
    • 对于 Auto 尺寸的行/列:测量其中所有子元素的大小,取最大值作为行/列的尺寸
    • 对于固定尺寸的行/列:直接使用指定的像素值
    • 累加所有 Auto 和固定尺寸的行/列的总宽度和总高度
  2. 第二阶段:分配剩余空间给 * 号行/列

    • 计算剩余空间:总可用空间 - Auto 和固定尺寸的总大小
    • 计算总 * 数:所有 * 号行/列的 * 数之和
    • 每个号行 / 列的尺寸 = 剩余空间 × (当前数 / 总 * 数)

7.3 跨行跨列处理

当子元素设置了RowSpanColumnSpan时:

  • 测量阶段:子元素会影响所有跨越的行/列的尺寸
  • 排列阶段:子元素的排列矩形是跨越的所有单元格的合并矩形

7.4 共享尺寸组工作原理

  1. 当某个元素设置了Grid.IsSharedSizeScope="True"时,它成为共享尺寸范围的根
  2. 在这个范围内,所有具有相同SharedSizeGroup名称的行 / 列会被归为一组
  3. 测量阶段:同一组的所有行 / 列会取其中的最大值作为统一尺寸
  4. 排列阶段:同一组的所有行 / 列会使用这个统一尺寸进行排列

八、工业上位机典型应用实例

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 最佳实践

  1. 合理使用三种尺寸类型

    • 标签、按钮等固定大小的元素使用 Auto
    • 工具栏、状态栏等固定高度的区域使用固定像素值
    • 内容区域使用 * 号分配剩余空间
  2. 使用共享尺寸组:多个表单的标签列使用共享尺寸组,保持界面整齐

  3. 避免嵌套过多 Grid:嵌套超过 3 层会降低性能和可读性,复杂布局可以使用一个 Grid 实现

  4. 显式指定 Row 和 Column:即使是第 0 行 / 第 0 列,也建议显式写出,提高代码可读性

  5. 使用 ShowGridLines 调试布局:布局出现问题时,开启网格线帮助定位问题

  6. 避免使用过多的 ** 号比例过多会导致布局计算复杂,影响性能

  7. 工业界面保持简洁:避免过于复杂的网格结构,保持界面清晰易读

9.2 常见问题与解决方案### 9.2 常见问题与解决方案

问题 1:* 号比例不对

原因* 号是分配剩余空间 ,而不是总空间。Auto 和固定尺寸的行 / 列会先占用空间,剩余空间才按 * 号分配。

解决方案:检查是否有 Auto 或固定尺寸的行 / 列占用了过多空间。

问题 2:跨行跨列的元素显示不正确

可能原因

  1. RowSpanColumnSpan的值超过了实际的行数或列数

  2. 子元素的大小超过了合并后的单元格大小

解决方案

  1. 子元素的大小超过了合并后的单元格大小

  2. 确保RowSpanColumnSpan的值不超过实际的行数和列数

  3. 设置子元素的HorizontalAlignment="Stretch"VerticalAlignment="Stretch"

问题 3:共享尺寸组不生效

可能原因

  1. 没有在父容器上设置Grid.IsSharedSizeScope="True"

  2. 共享尺寸组的名称拼写错误

  3. 行 / 列不在同一个共享尺寸范围内

    解决方案

  4. 在父容器上设置Grid.IsSharedSizeScope="True"

  5. 检查共享尺寸组的名称是否正确

  6. 确保需要共享尺寸的行 / 列在同一个共享尺寸范围内

问题 4:性能问题

可能原因

  1. 嵌套了过多的 Grid

  2. 有太多的行和列

  3. 子元素过于复杂

    解决方案

  4. 减少 Grid 的嵌套层数,使用一个 Grid 实现复杂布局

  5. 减少不必要的行和列

  6. 简化子元素的 UI 结构

  7. 大数据量场景使用 DataGrid 并开启虚拟化

问题 5:子元素超出 Grid 边界

原因:Grid 默认会裁剪超出边界的子元素

解决方案

  • 如果需要显示超出边界的内容,设置ClipToBounds="False"
  • 或者调整 Grid 的大小,确保子元素能够完全显示

十、官方设计意图总结

微软设计 Grid 的核心目标是:

  1. 提供最强大的布局能力:支持二维网格布局,几乎可以实现任何复杂的界面
  2. 灵活的尺寸控制:提供 Auto、Pixel、Star 三种尺寸类型,满足不同的布局需求
  3. 支持跨行跨列:允许子元素跨越多个行或列,实现复杂的布局结构
  4. 支持共享尺寸:使多个行或列保持相同的尺寸,提高界面的一致性
  5. 保持高性能:优化的布局算法,即使是复杂的网格布局也能保持良好的性能
  6. 易于理解和使用:直观的行和列定义,简单的附加属性,学习成本低

总结

Grid是 WPF 中最强大、最常用的布局容器,它的核心特性包括:

  • 二维网格布局:将容器划分为行和列的单元格
  • 三种尺寸类型:Auto(自动适应)、Pixel(固定像素)、Star(按比例分配)
  • 跨行跨列:子元素可以跨越多个行或列
  • 共享尺寸组:多个行或列可以保持相同的尺寸
  • 高性能:优化的布局算法,支持复杂的网格结构

在工业上位机开发中,Grid 是实现主界面框架、参数表单、数据面板、复杂仪表盘的首选控件。掌握 Grid 的正确使用方法,特别是三种尺寸类型和跨行跨列的使用,可以开发出任何复杂的工业界面。

相关推荐
xieliyu.1 小时前
Java 线程池入门详解:线程池基础定义 + ThreadPoolExecutor 全参构造方法逐参数解析
java·开发语言·idea·javaee
一条泥憨鱼1 小时前
【从0开始学习计算机网络】| HTTP方法疑点解析
开发语言·计算机网络·http
aaaameliaaa9 小时前
字符函数和字符串函数
c语言·笔记·算法
微学AI10 小时前
一根针指向所有方向:挂谷猜想对 LLM Agent 技能-记忆架构的启示
开发语言·人工智能·架构·挂谷猜想
城管不管11 小时前
ReAct、Plan-and-Execute、Reflection 三大智能 Agent 范式核心区别
java·人工智能·算法·spring·ai·动态规划
月疯11 小时前
二分法算法(水平等分图形面积)
算法
豆瓣鸡11 小时前
算法日记 - Day3
java·开发语言·算法
白白白小纯11 小时前
算法篇—反转链表
c语言·数据结构·算法·leetcode
Achou.Wang12 小时前
深入理解go语言-第5章 并发编程——Go的灵魂
大数据·算法·golang