用于 WPF 中 WrapPanel 数据绑定的更优面板






4.88/5 (24投票s)
一篇关于对 WrapPanel 控件的有用扩展的文章。
引言
本文介绍了一个对 WPF WrapPanel
控件的扩展,该扩展推断出美观的 ItemWidth
和 ItemHeight
属性。这种推断在数据绑定应用程序中很重要,因为您可能不知道在设计时为 ItemWidth
或 ItemHeight
选择一个好的值。
背景
WPF 附带的 System.Windows.Controls.WrapPanel
控件从左到右(或从上到下)按顺序定位其子元素,并在包含框的边缘换行到下一行。后续排序从左到右(或从上到下)依次进行。以下是使用 WrapPanel
显示一组按钮的输出
用于生成此代码的代码很简单
<WrapPanel>
<Button Margin="5,5,5,5">Alpha</Button>
<Button Margin="5,5,5,5">Bravo</Button>
<!-- ... -->
<Button Margin="5,5,5,5">Zebra</Button>
</WrapPanel>
输出看起来很丑,因为每个按钮都有不同的大小。为了使按钮大小统一,我们可以设置 ItemWidth
属性
<WrapPanel ItemWidth="69">
<Button Margin="5,5,5,5">Alpha</Button>
<Button Margin="5,5,5,5">Bravo</Button>
<!-- ... -->
<Button Margin="5,5,5,5">Zebra</Button>
</WrapPanel>
现在,我们得到了一个更好的输出
选择正确的 ItemWidth
值需要反复试验。目标是使 ItemWidth
足够大,以完全显示具有所需最大宽度的子控件(在本例中为“Yesterday”Button
),但不要使其更大。在此示例中,如果大小太大,按钮会看起来很傻。如果大小太小,一个或多个按钮的文本将被截断。
当我们引入数据绑定时,这个问题会变得更加困难。当我们将 WrapPanel
的子项绑定到一组数据以及一组相应的数据绑定控件时,我们可能不知道设计时所需的最大子控件宽度。为了演示这一点,我首先将同一组名称分解为新类 TextToDisplay
,我将使用它来数据绑定这组按钮名称
public class TextToDisplay
{
public TextToDisplay()
{
Text = new ObservableCollection<string>();
Text.Add("Alpha");
Text.Add("Bravo");
//...
Text.Add("Zebra");
}
public ObservableCollection<string> Text
{
get;
private set;
}
}
此新视图代码使用数据绑定来显示 TextToDisplay
类的 Text
元素
<Window
x:Class="UniformWrapPanelExample.WrapPanelWithDataBindingWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sys="clr-namespace:System;assembly=mscorlib"
xmlns:local="clr-namespace:UniformWrapPanelExample"
Title="WrapPanelWithDataBindingWindow" Height="300" Width="300">
<Window.Resources>
<local:TextToDisplay x:Key="TextContainer"/>
</Window.Resources>
<ItemsControl
ItemsSource="{Binding Text,
Source={StaticResource TextContainer}}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel ItemWidth="69"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type sys:String}">
<Button
Margin="5,5,5,5"
Content="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>
此新实现允许某人通过简单地从 TextToDisplay
对象的 Text
属性中添加或删除字符串来更改按钮集合。视图代码会自动处理将此字符串转换为按钮。
使用数据绑定,我们不能再可靠地使用反复试验来猜测正确的 ItemWidth
值。我们对按钮文本宽度的假设在运行时可能会被认为无效。例如,如果您猜测 50 是 ItemWidth
的一个好值,那么您最终将得到一些在示例集中被截断的单词
为了解决选择正确 ItemWidth
(或 ItemHeight
)值时涉及的猜测工作,我创建了 UniformWrapPanel
。 UnfiormWrapPanel
只是一个 WrapPanel
,具有一个特殊的属性 IsAutoUniform
。当此属性为 true
时,ItemWidth
(或 ItemHeight
)将自动设置为子项在布局之前的所需最大宽度,从而使所有项目获得美观的统一布局,而无需任何猜测。 IsAutoUniformProperty
定义如下
public class UniformWrapPanel : WrapPanel
{
public bool IsAutoUniform
{
get { return (bool)GetValue(IsAutoUniformProperty); }
set { SetValue(IsAutoUniformChanged, value); }
}
public static readonly DependencyProperty
IsAutoUniformProperty = DependencyProperty.Register(
"IsAutoUniform", typeof(bool), typeof(UniformWrapPanel),
new FrameworkPropertyMetadata(true,
new PropertyChangedCallback(IsAutoUniformChanged)));
//...
}
}
此属性默认为 true
;将其设置为 false
将使此面板的行为类似于标准的 WrapPanel
。由于更改此值可能会影响布局,因此 PropertyChangedCallback
IsAutoUniformChanged
将导致面板在 IsAutoUniform
属性每次更改时重新计算其元素的布局
private static void IsAutoUniformChanged(DependencyObject sender,
DependencyPropertyChangedEventArgs e)
{
if (sender is UniformWrapPanel)
{
((UniformWrapPanel)sender).InvalidateVisual();
}
}
为了产生正确的行为,UniformWrapPanel
计算一个适当的 ItemWidth
(或 ItemHeight
)属性,然后委托给 WrapPanel
基类以完成布局
protected override Size MeasureOverride(Size availableSize)
{
if (Children.Count > 0 && IsAutoUniform)
{
if (Orientation == Orientation.Horizontal)
{
double totalWidth = availableSize.Width;
ItemWidth = 0.0;
foreach (UIElement el in Children)
{
el.Measure(availableSize);
Size next = el.DesiredSize;
if (!(Double.IsInfinity(next.Width) ||
Double.IsNaN(next.Width)))
{
ItemWidth = Math.Max(next.Width, ItemWidth);
}
}
}
else
{
double totalHeight = availableSize.Height;
ItemHeight = 0.0;
foreach (UIElement el in Children)
{
el.Measure(availableSize);
Size next = el.DesiredSize;
if (!(Double.IsInfinity(next.Height) ||
Double.IsNaN(next.Height)))
{
ItemHeight = Math.Max(next.Height, ItemHeight);
}
}
}
}
return base.MeasureOverride(availableSize);
}
Using the Code
要使用此新面板,请将上述数据绑定示例中的 ItemsPanel
替换为
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<local:UniformWrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
它会产生以下输出
关注点
当我最初创建此面板时,我将其作为 Panel
的扩展而不是 WrapPanel
。在原始版本中,没有 ItemWidth
或 ItemHeight
属性,因为这些属性是自动推断的。这似乎是有限制的,因为它不能总是用来代替 WrapPanel
,所以我切换到上述模型。果然,当我这样做时,代码实际上变得更简单了 :)
历史
我将来所做的任何更改或改进都将在此处发布。