65.9K
CodeProject 正在变化。 阅读更多。
Home

WPF Padded Grid

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (13投票s)

2010年9月6日

CPOL

5分钟阅读

viewsIcon

87134

downloadIcon

1217

一个适用于 WPF 的带内边距的 Grid 控件,非常适合布局表单。

引言

如果 WPF 的 Grid 控件有一个“Padding”属性,那该多好啊?在这篇文章中,我将向您展示如何创建 PaddedGrid 控件,这是一个从 Grid 派生的自定义控件,它提供了这个急需的属性。

这是 PaddedGrid 在 Visual Studio XAML 设计器窗口中使用时的图片。正如您所见,标签和文本框之间根据 Padding 属性中指定的值进行了间隔。

Designer.png

背景

我经常使用 Grid 来布局表单的数据录入部分。通常,左侧会有一列 Label,右侧会有一列字段。由于 Grid 控件没有 Padding 属性,因此必须将 Grid 中控件的 Margin 设置为适当的值才能正确地间隔元素。这会弄乱 XAML,并且有些耗时。在父 Grid 上拥有 Padding 属性可以让事情变得更整洁。

规划控件

在逐步介绍控件代码之前,我将解释内边距的工作原理。

在 WPF 中,通常有很多方法可以实现类似的功能;每种技术都有其优点和缺点。理想情况下,我们会将每个网格单元格包装在某种容器中,并为其设置边距。这将在所有情况下提供正确的布局,但会不必要地复杂化。一个更简单的解决方案是让控件执行我一直手动执行的操作——设置子控件的边距属性。事实上,如果我们可以将网格直接子元素的 Margin 属性绑定到父 PaddedGridPadding 属性,那么我们就能得到我们想要的东西。

通过将子元素的 Margin 绑定到父元素的 Padding(一种在 DataTemplates、ItemTemplates 等中常用的技术),我们可以避免在过程代码中进行任何复杂的自定义布局。通过使 Padding 成为依赖项属性,我们可以使 PaddedGrid 与标准的 WPF 控件保持一致。

PaddedGrid 控件

我将逐步向您介绍 PaddedGrid 控件。我已尽力按照文章最合乎逻辑的顺序进行;代码文件本身顺序略有不同。我们开始吧。

public class PaddedGrid : Grid
{
    /// <summary>
    /// The internal dependency property object for the 'Padding' property.
    /// </summary>
    private static readonly DependencyProperty PaddingProperty =
        DependencyProperty.Register("Padding", 
            typeof(Thickness), typeof(PaddedGrid),
            new UIPropertyMetadata(new Thickness(0.0), 
            new PropertyChangedCallback(OnPaddingChanged)));

首先,我们创建类并使其从 Grid 派生。然后,我们为 Padding 创建静态依赖项属性。依赖项属性对 WPF 至关重要;如果您不熟悉它们,请从 MSDN 文章开始:“依赖项属性概述”。

我们将依赖项属性注册为 Thickness 类型。这是大多数控件拥有的 Margin 属性相同的数据类型。它允许指定左、右、上和下的内边距(或每个侧面的单一内边距)。默认值设置为零,这意味着默认情况下,PaddedGrid 将看起来与标准网格完全一样。需要注意的一点是,我们还指定了一个 PropertyChangedCallback——每次更改 Padding 时都会调用此回调。稍后将详细介绍。

public PaddedGrid()
{
    //  Add a loded event handler.
    Loaded += new RoutedEventHandler(PaddedGrid_Loaded);
}

构造函数非常简单——它只是为 Loaded 事件添加了一个事件处理程序。我们将在 Loaded 事件处理程序中将子控件的 Margin 属性绑定到 PaddedGridPadding 属性。

void PaddedGrid_Loaded(object sender, RoutedEventArgs e)
{
    //  Get the number of children.
    int childCount = VisualTreeHelper.GetChildrenCount(this);

    //  Go through the children.
    for (int i = 0; i < childCount; i++)
    {
        //  Get the child.
        DependencyObject child = VisualTreeHelper.GetChild(this, i);

        //  Try and get the margin property.
        DependencyProperty marginProperty = GetMarginProperty(child);

        //  If we have a margin property, bind it to the padding.
        if (marginProperty != null)
        {
            //  Create the binding.
            Binding binding = new Binding();
            binding.Source = this;
            binding.Path = new PropertyPath("Padding");

            //  Bind the child's margin to the grid's padding.
            BindingOperations.SetBinding(child, marginProperty, binding);
        }
    }
}

我们使用 VisualTreeHelper 来获取视觉树中直接子元素的数量。然后,我们可以使用循环和 VisualTreeHelper 将每个子元素获取为 DependencyObject。通过调用帮助函数 GetMarginProperty(稍后将对其进行描述),我们尝试获取子元素的 Margin 属性。如果存在,我们将创建一个绑定到 PaddedGridPadding 属性的绑定;然后将其设置为子元素的 Margin 属性。这样,每个直接子元素都将拥有绑定到父 PaddedGridPaddingMargin 属性。

通过使用这样的绑定,我们确保在运行时对内边距的更改能够正确地反映在每个子控件的 Margin 中。

protected static DependencyProperty GetMarginProperty(DependencyObject dependencyObject)
{
    //  Go through each property for the object.
    foreach (PropertyDescriptor propertyDescriptor in 
                TypeDescriptor.GetProperties(dependencyObject))
    {
        //  Get the dependency property descriptor.
        DependencyPropertyDescriptor dpd = 
           DependencyPropertyDescriptor.FromProperty(propertyDescriptor);

        //  Have we found the margin?
        if (dpd != null && dpd.Name == "Margin")
        {
            //  We've found the margin property, return it.
            return dpd.DependencyProperty;
        }
    }

    //  Failed to find the margin, return null.
    return null;
}

获取依赖项对象的 Margin 依赖项属性非常简单。我们使用 TypeDescriptor 对象迭代每个属性。DependencyPropertyDescriptor.FromProperty 函数将获取对依赖项属性的引用——但前提是该属性是依赖项属性(而不是标准属性)。如果不是,它将返回 null。这是我发现的枚举依赖项属性的最快方法。

如果找到了依赖项属性,我们会检查它是否是“Margin”属性。如果是,我们就返回它。很简单!

此函数可以通用化以按名称查找任何依赖项属性,但我在这里保持简单,以确保逻辑易于理解。

最后要做的是实现我们在 Padding 依赖项属性的构造函数中设置的 OnPaddingChanged 回调。

private static void OnPaddingChanged(DependencyObject dependencyObject, 
               DependencyPropertyChangedEventArgs args)
{
    //  Get the padded grid that has had its padding changed.
    PaddedGrid paddedGrid = dependencyObject as PaddedGrid;

    //  Force the layout to be updated.
    paddedGrid.UpdateLayout();
}

回调非常简单。我们获取已更改内边距的 PaddedGrid,并强制其更新布局。

使用 PaddedGrid

首先,将 PaddedGrid 程序集添加到您的项目中,然后将命名空间添加到您将使用的 Window(或 Page)的 XAML 中。

<Window x:Class="PaddedGridExample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:paddedGrid="clr-namespace:PaddedGrid;assembly=PaddedGrid"
        Title="MainWindow" Height="350" Width="525">

定义了命名空间后,您就可以这样使用 PaddedGrid(及其 Padding):

<!-- The padded grid. -->
<paddedGrid:PaddedGrid x:Name="paddedGrid" Padding="2,6,3,6" >
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="1*" />
        <ColumnDefinition Width="2*" />
    </Grid.ColumnDefinitions>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <!-- ...etc... -->
     </Grid.RowDefinitions>

     <!-- Some simple fields. -->
     <Label Grid.Row="0" Grid.Column="0">Name</Label>
     <TextBox Grid.Row="0" Grid.Column="1" Text="Enter Name Here" />

     <!-- More controls etc etc -->

</paddedGrid:PaddedGrid>

限制

我只在相当简单的布局中使用过 PaddedGrid。设置直接子元素边距的技巧可能并不适用于所有情况。这是一个简单的控件,适用于简单的场景——但是,如果您遇到任何问题,请告诉我,我将努力解决问题并发布更新。

改进

如果您有任何改进建议,请告诉我。我很乐意在有更多工作可做时扩展控件和文章!

PaddedGrid 库和示例项目可以从上面的链接下载。尽情享用!

© . All rights reserved.