WPF 自定义控件:AutoGrid 实现灵活自动布局的网格控件

在 WPF 开发中,原生Grid控件是布局的核心工具,但它存在一个明显痛点:需要手动定义RowDefinitionsColumnDefinitions,且每个子控件都要显式设置Grid.RowGrid.Column属性,当子控件数量变化或布局调整时,维护成本较高。

本文将介绍一款自定义控件AutoGrid,它基于原生Grid扩展而来,支持自动生成行列自动排列子控件统一配置子控件样式,大幅提升网格布局的开发效率和灵活性。

一、AutoGrid 核心功能概述

AutoGrid的核心价值是简化Grid布局的配置流程,主要提供以下功能:

  1. 支持两种方式定义行列:直接设置行列数量(ColumnCount/RowCount),或通过字符串格式自定义行列尺寸(Columns/Rows,支持*、像素、Auto);
  2. 支持两种布局方向(Orientation):Horizontal(先行后列,横向优先填充)、Vertical(先列后行,纵向优先填充);
  3. 统一配置子控件样式:批量设置子控件的边距(ChildMargin)、水平对齐(ChildHorizontalAlignment)、垂直对齐(ChildVerticalAlignment),且支持单个子控件覆盖默认样式;
  4. 支持子控件RowSpan/ColumnSpan,自动标记被占用的格子,避免子控件重叠;
  5. 可禁用自动索引(IsAutoIndexing),兼容原生Grid的手动布局方式。

二、完整代码实现

AutoGrid的实现包含两个类:核心控件AutoGrid(继承自原生Grid)和依赖属性扩展类DependencyExtensions(提供默认值判断扩展方法),代码如下:

2.1 依赖属性扩展类 DependencyExtensions

cs 复制代码
namespace WpfAutoGrid
{
    using System.Windows;

    /// <summary>
    /// 依赖对象扩展方法类
    /// 提供"仅当属性为默认值时赋值"的功能
    /// </summary>
    public static class DependencyExtensions
    {
        /// <summary>
        /// 仅当目标依赖属性为默认值(未显式设置)时,才为其赋值
        /// </summary>
        /// <typeparam name="T">属性值类型</typeparam>
        /// <param name="d">依赖对象</param>
        /// <param name="property">依赖属性</param>
        /// <param name="value">要设置的属性值</param>
        /// <returns>是否成功赋值</returns>
        public static bool SetIfDefault<T>(this DependencyObject d, DependencyProperty property, T value)
        {
            // 获取属性值来源,判断是否为默认值
            if (DependencyPropertyHelper.GetValueSource(d, property).BaseValueSource == BaseValueSource.Default)
            {
                d.SetValue(property, value);
                return true;
            }

            return false;
        }
    }
}

2.2 核心控件 AutoGrid

cs 复制代码
namespace WpfAutoGrid
{
    using System.ComponentModel;
    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;

    /// <summary>
    /// 灵活的自动布局网格控件
    /// 基于原生Grid扩展,支持行列自动生成、子控件自动排列
    /// 部分实现参考:http://rachel53461.wordpress.com/2011/09/17/wpf-grids-rowcolumn-count-properties/
    /// </summary>
    public class AutoGrid : Grid
    {
        #region 依赖属性 - 公共配置项
        /// <summary>
        /// 所有子控件的统一水平对齐方式
        /// </summary>
        [Category("Layout"), Description("Presets the horizontal alignment of all child controls")]
        public HorizontalAlignment? ChildHorizontalAlignment
        {
            get { return (HorizontalAlignment?)GetValue(ChildHorizontalAlignmentProperty); }
            set { SetValue(ChildHorizontalAlignmentProperty, value); }
        }

        /// <summary>
        /// 所有子控件的统一边距
        /// </summary>
        [Category("Layout"), Description("Presets the margin of all child controls")]
        public Thickness? ChildMargin
        {
            get { return (Thickness?)GetValue(ChildMarginProperty); }
            set { SetValue(ChildMarginProperty, value); }
        }

        /// <summary>
        /// 所有子控件的统一垂直对齐方式
        /// </summary>
        [Category("Layout"), Description("Presets the vertical alignment of all child controls")]
        public VerticalAlignment? ChildVerticalAlignment
        {
            get { return (VerticalAlignment?)GetValue(ChildVerticalAlignmentProperty); }
            set { SetValue(ChildVerticalAlignmentProperty, value); }
        }

        /// <summary>
        /// 列数量(自动生成列)
        /// </summary>
        [Category("Layout"), Description("Defines a set number of columns")]
        public int ColumnCount
        {
            get { return (int)GetValue(ColumnCountProperty); }
            set { SetValue(ColumnCountProperty, value); }
        }

        /// <summary>
        /// 列定义字符串(逗号分隔,支持*、像素、Auto)
        /// </summary>
        [Category("Layout"), Description("Defines all columns using comma separated grid length notation")]
        public string Columns
        {
            get { return (string)GetValue(ColumnsProperty); }
            set { SetValue(ColumnsProperty, value); }
        }

        /// <summary>
        /// 统一列宽度(针对ColumnCount生成的列)
        /// </summary>
        [Category("Layout"), Description("Presets the width of all columns set using the ColumnCount property")]
        public GridLength ColumnWidth
        {
            get { return (GridLength)GetValue(ColumnWidthProperty); }
            set { SetValue(ColumnWidthProperty, value); }
        }

        /// <summary>
        /// 是否启用子控件自动索引(默认true)
        /// </summary>
        [Category("Layout"), Description("Set to false to disable the auto layout functionality")]
        public bool IsAutoIndexing
        {
            get { return (bool)GetValue(IsAutoIndexingProperty); }
            set { SetValue(IsAutoIndexingProperty, value); }
        }

        /// <summary>
        /// 布局方向(默认Horizontal:先行后列)
        /// </summary>
        [Category("Layout"), Description("Defines the directionality of the autolayout. Use vertical for a column first layout, horizontal for a row first layout.")]
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        /// <summary>
        /// 行数量(自动生成行)
        /// </summary>
        [Category("Layout"), Description("Defines a set number of rows")]
        public int RowCount
        {
            get { return (int)GetValue(RowCountProperty); }
            set { SetValue(RowCountProperty, value); }
        }

        /// <summary>
        /// 统一行高度(针对RowCount生成的行)
        /// </summary>
        [Category("Layout"), Description("Presets the height of all rows set using the RowCount property")]
        public GridLength RowHeight
        {
            get { return (GridLength)GetValue(RowHeightProperty); }
            set { SetValue(RowHeightProperty, value); }
        }

        /// <summary>
        /// 行定义字符串(逗号分隔,支持*、像素、Auto)
        /// </summary>
        [Category("Layout"), Description("Defines all rows using comma separated grid length notation")]
        public string Rows
        {
            get { return (string)GetValue(RowsProperty); }
            set { SetValue(RowsProperty, value); }
        }
        #endregion

        #region 依赖属性 - 注册与回调
        public static readonly DependencyProperty ChildHorizontalAlignmentProperty =
            DependencyProperty.Register("ChildHorizontalAlignment", typeof(HorizontalAlignment?), typeof(AutoGrid),
                new FrameworkPropertyMetadata((HorizontalAlignment?)null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildHorizontalAlignmentChanged));

        public static readonly DependencyProperty ChildMarginProperty =
            DependencyProperty.Register("ChildMargin", typeof(Thickness?), typeof(AutoGrid),
                new FrameworkPropertyMetadata((Thickness?)null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildMarginChanged));

        public static readonly DependencyProperty ChildVerticalAlignmentProperty =
            DependencyProperty.Register("ChildVerticalAlignment", typeof(VerticalAlignment?), typeof(AutoGrid),
                new FrameworkPropertyMetadata((VerticalAlignment?)null, FrameworkPropertyMetadataOptions.AffectsMeasure, OnChildVerticalAlignmentChanged));

        public static readonly DependencyProperty ColumnCountProperty =
            DependencyProperty.RegisterAttached("ColumnCount", typeof(int), typeof(AutoGrid),
                new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure, ColumnCountChanged));

        public static readonly DependencyProperty ColumnsProperty =
            DependencyProperty.RegisterAttached("Columns", typeof(string), typeof(AutoGrid),
                new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsMeasure, ColumnsChanged));

        public static readonly DependencyProperty ColumnWidthProperty =
            DependencyProperty.RegisterAttached("ColumnWidth", typeof(GridLength), typeof(AutoGrid),
                new FrameworkPropertyMetadata(GridLength.Auto, FrameworkPropertyMetadataOptions.AffectsMeasure, FixedColumnWidthChanged));

        public static readonly DependencyProperty IsAutoIndexingProperty =
            DependencyProperty.Register("IsAutoIndexing", typeof(bool), typeof(AutoGrid),
                new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.AffectsMeasure));

        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register("Orientation", typeof(Orientation), typeof(AutoGrid),
                new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));

        public static readonly DependencyProperty RowCountProperty =
            DependencyProperty.RegisterAttached("RowCount", typeof(int), typeof(AutoGrid),
                new FrameworkPropertyMetadata(1, FrameworkPropertyMetadataOptions.AffectsMeasure, RowCountChanged));

        public static readonly DependencyProperty RowHeightProperty =
            DependencyProperty.RegisterAttached("RowHeight", typeof(GridLength), typeof(AutoGrid),
                new FrameworkPropertyMetadata(GridLength.Auto, FrameworkPropertyMetadataOptions.AffectsMeasure, FixedRowHeightChanged));

        public static readonly DependencyProperty RowsProperty =
            DependencyProperty.RegisterAttached("Rows", typeof(string), typeof(AutoGrid),
                new FrameworkPropertyMetadata("", FrameworkPropertyMetadataOptions.AffectsMeasure, RowsChanged));
        #endregion

        #region 依赖属性回调 - 行列生成与更新
        /// <summary>
        /// 列数量变化回调:自动生成对应数量的列
        /// </summary>
        public static void ColumnCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if ((int)e.NewValue < 0) return;

            var grid = d as AutoGrid;
            if (grid == null) return;

            // 复用现有列的宽度(默认Auto,或已设置的ColumnWidth)
            var columnWidth = grid.ColumnDefinitions.Count > 0 ? grid.ColumnDefinitions[0].Width : GridLength.Auto;

            // 清空现有列,重新生成
            grid.ColumnDefinitions.Clear();
            for (int i = 0; i < (int)e.NewValue; i++)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = columnWidth });
            }
        }

        /// <summary>
        /// 列定义字符串变化回调:解析字符串生成列
        /// </summary>
        public static void ColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var newColumns = (string)e.NewValue;
            if (string.IsNullOrEmpty(newColumns)) return;

            var grid = d as AutoGrid;
            if (grid == null) return;

            // 清空现有列,解析字符串生成新列
            grid.ColumnDefinitions.Clear();
            var gridLengths = Parse(newColumns);
            foreach (var length in gridLengths)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = length });
            }
        }

        /// <summary>
        /// 统一列宽度变化回调:批量更新所有列的宽度
        /// </summary>
        public static void FixedColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as AutoGrid;
            if (grid == null) return;

            // 无列时默认生成一列
            if (grid.ColumnDefinitions.Count == 0)
            {
                grid.ColumnDefinitions.Add(new ColumnDefinition());
            }

            // 批量更新所有列的宽度
            var newColumnWidth = (GridLength)e.NewValue;
            for (int i = 0; i < grid.ColumnDefinitions.Count; i++)
            {
                grid.ColumnDefinitions[i].Width = newColumnWidth;
            }
        }

        /// <summary>
        /// 统一行高度变化回调:批量更新所有行的高度
        /// </summary>
        public static void FixedRowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as AutoGrid;
            if (grid == null) return;

            // 无行时默认生成一行
            if (grid.RowDefinitions.Count == 0)
            {
                grid.RowDefinitions.Add(new RowDefinition());
            }

            // 批量更新所有行的高度
            var newRowHeight = (GridLength)e.NewValue;
            for (int i = 0; i < grid.RowDefinitions.Count; i++)
            {
                grid.RowDefinitions[i].Height = newRowHeight;
            }
        }

        /// <summary>
        /// 行数量变化回调:自动生成对应数量的行
        /// </summary>
        public static void RowCountChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if ((int)e.NewValue < 0) return;

            var grid = d as AutoGrid;
            if (grid == null) return;

            // 复用现有行的高度(默认Auto,或已设置的RowHeight)
            var rowHeight = grid.RowDefinitions.Count > 0 ? grid.RowDefinitions[0].Height : GridLength.Auto;

            // 清空现有行,重新生成
            grid.RowDefinitions.Clear();
            for (int i = 0; i < (int)e.NewValue; i++)
            {
                grid.RowDefinitions.Add(new RowDefinition() { Height = rowHeight });
            }
        }

        /// <summary>
        /// 行定义字符串变化回调:解析字符串生成行
        /// </summary>
        public static void RowsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var newRows = (string)e.NewValue;
            if (string.IsNullOrEmpty(newRows)) return;

            var grid = d as AutoGrid;
            if (grid == null) return;

            // 清空现有行,解析字符串生成新行
            grid.RowDefinitions.Clear();
            var gridLengths = Parse(newRows);
            foreach (var length in gridLengths)
            {
                grid.RowDefinitions.Add(new RowDefinition() { Height = length });
            }
        }
        #endregion

        #region 私有回调 - 子控件样式更新
        /// <summary>
        /// 子控件水平对齐方式变化回调:批量更新所有子控件
        /// </summary>
        private static void OnChildHorizontalAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as AutoGrid;
            if (grid == null) return;

            foreach (UIElement child in grid.Children)
            {
                if (grid.ChildHorizontalAlignment.HasValue)
                {
                    child.SetValue(FrameworkElement.HorizontalAlignmentProperty, grid.ChildHorizontalAlignment);
                }
                else
                {
                    child.SetValue(FrameworkElement.HorizontalAlignmentProperty, DependencyProperty.UnsetValue);
                }
            }
        }

        /// <summary>
        /// 子控件边距变化回调:批量更新所有子控件
        /// </summary>
        private static void OnChildMarginChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as AutoGrid;
            if (grid == null) return;

            foreach (UIElement child in grid.Children)
            {
                if (grid.ChildMargin.HasValue)
                {
                    child.SetValue(FrameworkElement.MarginProperty, grid.ChildMargin);
                }
                else
                {
                    child.SetValue(FrameworkElement.MarginProperty, DependencyProperty.UnsetValue);
                }
            }
        }

        /// <summary>
        /// 子控件垂直对齐方式变化回调:批量更新所有子控件
        /// </summary>
        private static void OnChildVerticalAlignmentChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var grid = d as AutoGrid;
            if (grid == null) return;

            foreach (UIElement child in grid.Children)
            {
                if (grid.ChildVerticalAlignment.HasValue)
                {
                    child.SetValue(FrameworkElement.VerticalAlignmentProperty, grid.ChildVerticalAlignment);
                }
                else
                {
                    child.SetValue(FrameworkElement.VerticalAlignmentProperty, DependencyProperty.UnsetValue);
                }
            }
        }
        #endregion

        #region 核心工具方法
        /// <summary>
        /// 解析逗号分隔的字符串,转换为GridLength数组
        /// 支持三种格式:*(比例)、像素(数字)、Auto(默认)
        /// </summary>
        public static GridLength[] Parse(string text)
        {
            if (string.IsNullOrEmpty(text)) return Array.Empty<GridLength>();

            var tokens = text.Split(',');
            var gridLengths = new GridLength[tokens.Length];

            for (var i = 0; i < tokens.Length; i++)
            {
                var token = tokens[i].Trim(); // 去除空格,提升兼容性
                if (string.IsNullOrEmpty(token))
                {
                    gridLengths[i] = GridLength.Auto;
                    continue;
                }

                // 处理比例格式(*),如"2*"表示占2份比例
                if (token.Contains('*'))
                {
                    var valueStr = token.Replace("*", "");
                    double ratio = string.IsNullOrEmpty(valueStr) ? 1.0 : (double.TryParse(valueStr, out var r) ? r : 1.0);
                    gridLengths[i] = new GridLength(ratio, GridUnitType.Star);
                    continue;
                }

                // 处理像素格式(纯数字),如"100"表示100像素
                if (double.TryParse(token, out var pixelValue))
                {
                    gridLengths[i] = new GridLength(pixelValue);
                    continue;
                }

                // 默认Auto格式
                gridLengths[i] = GridLength.Auto;
            }

            return gridLengths;
        }

        /// <summary>
        /// 限制数值在最大范围内,防止索引越界
        /// </summary>
        private int Clamp(int value, int max)
        {
            return Math.Min(value, max);
        }

        /// <summary>
        /// 应用子控件的统一样式(边距、对齐方式)
        /// 仅当子控件对应属性为默认值时才赋值,支持单个子控件覆盖
        /// </summary>
        private void ApplyChildLayout(UIElement child)
        {
            if (child == null) return;

            // 统一边距
            if (ChildMargin.HasValue)
            {
                child.SetIfDefault(FrameworkElement.MarginProperty, ChildMargin.Value);
            }

            // 统一水平对齐
            if (ChildHorizontalAlignment.HasValue)
            {
                child.SetIfDefault(FrameworkElement.HorizontalAlignmentProperty, ChildHorizontalAlignment.Value);
            }

            // 统一垂直对齐
            if (ChildVerticalAlignment.HasValue)
            {
                child.SetIfDefault(FrameworkElement.VerticalAlignmentProperty, ChildVerticalAlignment.Value);
            }
        }
        #endregion

        #region 核心布局逻辑 - 子控件自动排列
        /// <summary>
        /// 执行自动布局:计算子控件的Grid.Row和Grid.Column
        /// </summary>
        private void PerformLayout()
        {
            // 禁用自动索引或无行列时,直接返回
            if (!IsAutoIndexing || RowDefinitions.Count == 0 || ColumnDefinitions.Count == 0)
            {
                return;
            }

            var isRowFirst = Orientation == Orientation.Horizontal; // 先行后列(横向)
            var rowCount = RowDefinitions.Count;
            var colCount = ColumnDefinitions.Count;
            var position = 0; // 记录当前布局位置
            var skipGrid = new bool[rowCount, colCount]; // 标记被RowSpan/ColumnSpan占用的格子,避免重叠

            foreach (UIElement child in Children)
            {
                // 折叠状态的子控件不参与自动布局
                if (child.Visibility == Visibility.Collapsed)
                {
                    ApplyChildLayout(child);
                    continue;
                }

                if (IsAutoIndexing)
                {
                    int targetRow = 0;
                    int targetCol = 0;

                    if (isRowFirst)
                    {
                        // 先行后列:行=position/列数,列=position%列数
                        targetRow = Clamp(position / colCount, rowCount - 1);
                        targetCol = Clamp(position % colCount, colCount - 1);

                        // 跳过已被占用的格子
                        if (skipGrid[targetRow, targetCol])
                        {
                            position++;
                            targetRow = Clamp(position / colCount, rowCount - 1);
                            targetCol = Clamp(position % colCount, colCount - 1);
                        }

                        // 设置子控件的行列索引
                        Grid.SetRow(child, targetRow);
                        Grid.SetColumn(child, targetCol);

                        // 更新下一个位置(跳过列跨度的格子)
                        position += Grid.GetColumnSpan(child);

                        // 标记RowSpan占用的格子(纵向占用)
                        var rowSpanOffset = Grid.GetRowSpan(child) - 1;
                        while (rowSpanOffset > 0 && targetRow + rowSpanOffset < rowCount)
                        {
                            skipGrid[targetRow + rowSpanOffset, targetCol] = true;
                            rowSpanOffset--;
                        }
                    }
                    else
                    {
                        // 先列后行:行=position%行数,列=position/行数
                        targetRow = Clamp(position % rowCount, rowCount - 1);
                        targetCol = Clamp(position / rowCount, colCount - 1);

                        // 跳过已被占用的格子
                        if (skipGrid[targetRow, targetCol])
                        {
                            position++;
                            targetRow = Clamp(position % rowCount, rowCount - 1);
                            targetCol = Clamp(position / rowCount, colCount - 1);
                        }

                        // 设置子控件的行列索引
                        Grid.SetRow(child, targetRow);
                        Grid.SetColumn(child, targetCol);

                        // 更新下一个位置(跳跨行跨度的格子)
                        position += Grid.GetRowSpan(child);

                        // 标记ColumnSpan占用的格子(横向占用)
                        var colSpanOffset = Grid.GetColumnSpan(child) - 1;
                        while (colSpanOffset > 0 && targetCol + colSpanOffset < colCount)
                        {
                            skipGrid[targetRow, targetCol + colSpanOffset] = true;
                            colSpanOffset--;
                        }
                    }
                }

                // 应用统一样式
                ApplyChildLayout(child);
            }
        }
        #endregion

        #region 重写控件生命周期 - 确保布局生效
        /// <summary>
        /// 重写测量方法:在测量子控件尺寸前执行自动布局
        /// </summary>
        /// <param name="constraint">布局上限尺寸</param>
        /// <returns>所需布局尺寸</returns>
        protected override Size MeasureOverride(Size constraint)
        {
            PerformLayout(); // 测量前先完成自动布局,确保子控件位置正确
            return base.MeasureOverride(constraint);
        }
        #endregion
    }
}

三、核心原理深度解析

AutoGrid的实现基于 WPF 控件的生命周期和依赖属性机制,核心逻辑可分为 3 个部分:

3.1 依赖属性:控件配置的核心载体

WPF 自定义控件优先使用依赖属性(DependencyProperty) 而非普通 CLR 属性,原因是依赖属性支持数据绑定、样式应用、属性变更回调、元数据配置等功能,这是实现 AutoGrid 自动布局的基础。

  • 关键元数据配置:FrameworkPropertyMetadataOptions.AffectsMeasure,标记该属性变化会触发控件的测量流程(MeasureOverride),进而重新执行PerformLayout,实现布局实时更新;
  • 两种注册方式:Register(控件自身属性,如ChildMargin)和RegisterAttached(附加属性,如ColumnCount),附加属性允许其他元素访问,提升控件灵活性;
  • 属性变更回调:如ColumnCountChangedOnChildMarginChanged,当属性值变化时,自动执行行列生成或子控件样式更新,无需手动调用。

3.2 行列生成:两种方式灵活配置

AutoGrid提供两种行列配置方式,满足不同场景需求:

  1. 简单配置(数量 + 统一尺寸):通过ColumnCount/RowCount设置行列数量,ColumnWidth/RowHeight设置统一尺寸,适合行列尺寸一致的场景(如网格列表);
  2. 自定义配置(字符串解析):通过Columns/Rows设置逗号分隔的字符串,支持*(比例)、像素、Auto三种格式,适合行列尺寸不一致的场景(如表单布局);
  3. 核心解析方法Parse:将字符串转换为GridLength数组,是自定义行列配置的关键,处理了空格兼容、默认值容错,提升了使用便捷性。

3.3 自动布局:子控件位置计算与冲突避免

PerformLayout是 AutoGrid 的核心方法,负责计算每个子控件的Grid.RowGrid.Column,核心逻辑如下:

  1. 布局方向判断:Horizontal(先行后列,类似 Excel 横向填充)、Vertical(先列后行,类似 Excel 纵向填充);
  2. 位置计算:通过position变量记录当前布局位置,利用除法 / 取模 运算计算行列索引(position/列数= 行,position%列数= 列);
  3. 越界防护:Clamp方法限制索引在行列数量范围内,避免IndexOutOfRangeException
  4. 跨度处理:skipGrid二维数组标记RowSpan/ColumnSpan占用的格子,确保后续子控件跳过已占用位置,避免重叠;
  5. 样式应用:ApplyChildLayout调用SetIfDefault扩展方法,仅当子控件属性为默认值时才赋值,兼顾统一样式和单个控件的灵活性。

3.4 控件生命周期:确保布局实时生效

AutoGrid重写了GridMeasureOverride方法,在控件测量子控件尺寸之前 执行PerformLayout,确保子控件的位置和样式在测量、排列阶段已配置完成,最终呈现正确的布局效果。

同时,依赖属性的AffectsMeasure元数据配置,确保当布局相关属性(如OrientationColumnCount)变化时,自动触发MeasureOverride,实现布局的实时更新。

四、使用示例(实战演示)

4.1 环境准备

使用AutoGrid前,需在 XAML 中引用对应的命名空间(假设程序集名称为WpfAutoGrid):

XML 复制代码
xmlns:ag="clr-namespace:WpfAutoGrid;assembly=WpfAutoGrid"

4.2 示例 1:简单网格布局(统一行列尺寸)

需求:创建 3 列 2 行的网格,子控件统一边距,横向优先填充,统一居中对齐。

XML 复制代码
<ag:AutoGrid ColumnCount="3" 
             RowCount="2" 
             ColumnWidth="100" 
             RowHeight="60" 
             ChildMargin="5" 
             ChildHorizontalAlignment="Center" 
             ChildVerticalAlignment="Center"
             Background="#F5F5F5">
    <Button Content="按钮1" />
    <Button Content="按钮2" />
    <Button Content="按钮3" />
    <Button Content="按钮4" />
    <Button Content="按钮5" Background="LightBlue" HorizontalAlignment="Left" /> <!-- 覆盖水平对齐 -->
</ag:AutoGrid>
效果说明:
  1. 自动生成 3 列(每列 100 像素)、2 行(每行 60 像素);
  2. 5 个按钮自动横向填充,统一边距 5,居中对齐;
  3. 按钮 5 显式设置HorizontalAlignment="Left",覆盖统一样式,呈现左对齐效果。

4.3 示例 2:自定义行列尺寸(表单布局)

需求:创建表单布局,列定义为「100 像素(标签)、*(输入框)」,行定义为「Auto、Auto、60、Auto」,纵向优先填充。

XML 复制代码
<ag:AutoGrid Columns="100,*" 
             Rows="Auto,Auto,60,Auto" 
             Orientation="Vertical"
             ChildMargin="5,3" 
             Background="#F5F5F5">
    <TextBlock Text="用户名:" />
    <TextBox Text="请输入用户名" />
    <TextBlock Text="密码:" />
    <PasswordBox Password="123456" />
    <Button Content="登录" Grid.ColumnSpan="2" /> <!-- 跨2列 -->
    <TextBlock Text="备注:" />
    <TextBox Text="请输入备注" AcceptsReturn="True" Grid.RowSpan="2" /> <!-- 跨2行 -->
</ag:AutoGrid>
效果说明:
  1. 列解析为「100 像素、剩余空间填满」,行解析为「Auto、Auto、60 像素、Auto」;
  2. 子控件纵向优先填充,表单布局整洁有序;
  3. 登录按钮跨 2 列,备注输入框跨 2 行,skipGrid自动标记占用格子,无重叠。

五、总结与扩展建议

5.1 核心总结

  1. AutoGrid基于原生Grid扩展,兼容Grid的所有功能,同时提供自动布局能力,简化网格布局开发;
  2. 核心优势:自动生成行列自动排列子控件统一样式配置,兼顾灵活性和易用性;
  3. 核心机制:依赖属性变更回调、控件生命周期重写、行列索引计算与冲突避免;
  4. 关键扩展:SetIfDefault方法实现「统一样式 + 单个控件覆盖」的需求,提升控件的灵活性。

5.2 扩展建议

  1. 支持行列间距配置:添加ColumnSpacingRowSpacing属性,实现行列之间的间距控制;
  2. 支持自适应换行:当子控件数量超过当前行列容量时,自动新增行 / 列,无需手动设置ColumnCount/RowCount
  3. 支持排序功能:添加SortDirection属性,实现子控件的升序 / 降序排列;
  4. 支持数据绑定:结合ItemsControl,实现数据集合的自动绑定与布局,适配列表场景。

AutoGrid大幅简化了 WPF 网格布局的开发成本,尤其适合表单、网格列表等场景,希望本文能帮助你理解其核心原理,并在实际项目中灵活运用。

相关推荐
极客智造2 小时前
WPF Grid 布局高效扩展:GridHelpers 附加属性工具类全解析
wpf
张人玉2 小时前
WPF 多语言实现完整笔记(.NET 4.7.2)
笔记·.net·wpf·多语言实现·多语言适配
暖馒3 小时前
深度剖析串口通讯(232/485)
开发语言·c#·wpf·智能硬件
我要打打代码17 小时前
WPF控件(2)
wpf
c#上位机19 小时前
wpf之行为
c#·wpf
kylezhao201921 小时前
深入浅出地理解 C# WPF 中的属性
hadoop·c#·wpf
全栈开发圈1 天前
干货分享|HarmonyOS核心技术理念
wpf·鸿蒙
海盗12341 天前
WPF上位机组件开发-设备状态运行图基础版
开发语言·c#·wpf
我要打打代码1 天前
WPF入门指南(1)
wpf