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

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.88/5 (24投票s)

2009年1月18日

MIT

3分钟阅读

viewsIcon

134399

downloadIcon

2814

一篇关于对 WrapPanel 控件的有用扩展的文章。

img4.jpg

引言

本文介绍了一个对 WPF WrapPanel 控件的扩展,该扩展推断出美观的 ItemWidthItemHeight 属性。这种推断在数据绑定应用程序中很重要,因为您可能不知道在设计时为 ItemWidthItemHeight 选择一个好的值。

背景

WPF 附带的 System.Windows.Controls.WrapPanel 控件从左到右(或从上到下)按顺序定位其子元素,并在包含框的边缘换行到下一行。后续排序从左到右(或从上到下)依次进行。以下是使用 WrapPanel 显示一组按钮的输出

img1.jpg

用于生成此代码的代码很简单

<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>

现在,我们得到了一个更好的输出

img2.jpg

选择正确的 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 的一个好值,那么您最终将得到一些在示例集中被截断的单词

img3.jpg

为了解决选择正确 ItemWidth(或 ItemHeight)值时涉及的猜测工作,我创建了 UniformWrapPanelUnfiormWrapPanel 只是一个 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>

它会产生以下输出

img4.jpg

关注点

当我最初创建此面板时,我将其作为 Panel 的扩展而不是 WrapPanel。在原始版本中,没有 ItemWidthItemHeight 属性,因为这些属性是自动推断的。这似乎是有限制的,因为它不能总是用来代替 WrapPanel,所以我切换到上述模型。果然,当我这样做时,代码实际上变得更简单了 :)

历史

我将来所做的任何更改或改进都将在此处发布。

© . All rights reserved.