WPF Padded Grid
一个适用于 WPF 的带内边距的 Grid 控件,非常适合布局表单。
引言
如果 WPF 的 Grid
控件有一个“Padding
”属性,那该多好啊?在这篇文章中,我将向您展示如何创建 PaddedGrid
控件,这是一个从 Grid
派生的自定义控件,它提供了这个急需的属性。
这是 PaddedGrid
在 Visual Studio XAML 设计器窗口中使用时的图片。正如您所见,标签和文本框之间根据 Padding
属性中指定的值进行了间隔。
背景
我经常使用 Grid
来布局表单的数据录入部分。通常,左侧会有一列 Label
,右侧会有一列字段。由于 Grid
控件没有 Padding
属性,因此必须将 Grid
中控件的 Margin
设置为适当的值才能正确地间隔元素。这会弄乱 XAML,并且有些耗时。在父 Grid
上拥有 Padding
属性可以让事情变得更整洁。
规划控件
在逐步介绍控件代码之前,我将解释内边距的工作原理。
在 WPF 中,通常有很多方法可以实现类似的功能;每种技术都有其优点和缺点。理想情况下,我们会将每个网格单元格包装在某种容器中,并为其设置边距。这将在所有情况下提供正确的布局,但会不必要地复杂化。一个更简单的解决方案是让控件执行我一直手动执行的操作——设置子控件的边距属性。事实上,如果我们可以将网格直接子元素的 Margin
属性绑定到父 PaddedGrid
的 Padding
属性,那么我们就能得到我们想要的东西。
通过将子元素的 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
属性绑定到 PaddedGrid
的 Padding
属性。
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
属性。如果存在,我们将创建一个绑定到 PaddedGrid
的 Padding
属性的绑定;然后将其设置为子元素的 Margin
属性。这样,每个直接子元素都将拥有绑定到父 PaddedGrid
的 Padding
的 Margin
属性。
通过使用这样的绑定,我们确保在运行时对内边距的更改能够正确地反映在每个子控件的 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 库和示例项目可以从上面的链接下载。尽情享用!