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

WPF 的灵活面板

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (4投票s)

2009 年 7 月 3 日

CPOL

5分钟阅读

viewsIcon

35879

downloadIcon

564

受 wxWidgets 的 Sizers 启发的 WPF 自定义布局面板

引言

我非常喜欢 WPF。它是微软迄今为止我见过的最好的布局技术。但大多数时候我希望我的控件能占据所有可用空间,这给了我两种内置面板的选择。首先是 DockPanel,如果我想在多个控件之间分配空间,它就无法正常工作(并且它要求拉伸的控件必须是最后一个子元素,这可能导致一些奇怪的 XAML)。然后是 Grid,它可以做到一切,但它要求你预先定义所有行和列,这对我来说显得过于啰嗦,特别是如果我只需要一行或一列。

决定实现自己的 Panel 后,我回到了我的初恋:wxWidgetswxWidgets 使用 Sizers 进行布局,特别是其中有两个相对简单但允许你完成大部分想要做的事情:wxBoxSizer wxFlexGridSizer

BoxSizer

BoxSizer 类(基于 wxBoxSizer)的概念非常简单。它与 StackPanel 非常相似,因为它沿着 Orientation 方向排列子元素,并允许它们在另一个方向上任意大。但是,它还为其子元素提供了 Proportion 附加属性。Orientation 方向上所有剩余的空间将按比例分配给具有非零比例的子元素。为简化本文内容,我们将开发一个具有固定方向(水平)的 Sizer,但下载文件中使用的是一个 Orientation 属性,就像 StackPanel 类一样。

Sizers1.png

Proportion 附加属性

附加属性允许我们基本上为其他类添加属性。我们需要一个这样的属性,以便我们的子元素可以告诉我们它们想要的比例。第一步是注册附加属性。这需要告诉框架该属性的类型、我们的类型以及默认值。我们还可以告诉框架,当属性更改时需要重新评估布局。

public static readonly DependencyProperty ProportionProperty =
    DependencyProperty.RegisterAttached
	("Proportion", typeof(double), typeof(HorizontalBoxSizer),
        new FrameworkPropertyMetadata
	(0.0, FrameworkPropertyMetadataOptions.AffectsArrange |
            FrameworkPropertyMetadataOptions.AffectsMeasure |
            FrameworkPropertyMetadataOptions.AffectsRender | 
            FrameworkPropertyMetadataOptions.AffectsParentArrange | 
            FrameworkPropertyMetadataOptions.AffectsParentMeasure));

第二件我们需要做的事情是提供一些标准的便捷函数

public static double GetProportion(DependencyObject obj)
{
    return (double)obj.GetValue(ProportionProperty);
}

public static void SetProportion(DependencyObject obj, double value)
{
    obj.SetValue(ProportionProperty, value);
}

一次测量

测量过程是布局的第一部分。在这里,我们被告知我们允许有多大,并且必须返回我们想要有多大。我们还负责递归调用子元素的 Measure。请注意,我们真正想要覆盖的函数是 MeasureOverride,而不是 Measure 本身。这让基类的 MeasureCore 方法为我们处理诸如边距之类的东西,这是一个很大的优势。对于我们的水平 BoxSizer,实现非常简单。我们只需要找到最高的子元素并使用它的高度;宽度将尽可能大。

此函数中的技巧在于处理无穷大。首先,由于我们将允许子元素任意宽度,因此我们需要将 double.PositiveInfinity 作为其可用宽度传递。如果我们只是简单地传递相同的可用宽度,一些具有自身拉伸逻辑(如 Image)的控件将无法正确调整大小。这意味着我们的 availableSize 可能包含无穷大,但我们不允许在从函数返回的 Size 中包含无穷大,因此我们需要一种策略来处理这个问题。最后,我们还想避免返回大于可用尺寸的尺寸;如果这样做,我们的控件将超出其应占用的区域。

protected override Size MeasureOverride(Size availableSize)
{
    Size SizeForChildren = new Size(double.PositiveInfinity, availableSize.Height);
    double TotalWidth = 0;
    double MaxHeight = 0;
    foreach (UIElement Child in InternalChildren)
    {
        Child.Measure(SizeForChildren);
        TotalWidth += Child.DesiredSize.Width;
        MaxHeight = Math.Max(MaxHeight, Child.DesiredSize.Height);
    }
    TotalWidth = Math.Min(TotalWidth, availableSize.Width);
    return new Size()
    {
        Width = TotalWidth,
        Height = double.IsInfinity(availableSize.Height) ? 
				MaxHeight : availableSize.Height
    };
}

这里要注意的另一件事是我们正在使用 InternalChildren 属性而不是 Children 属性,因为 MSDN 建议我们这样做

整体排列

排列过程是布局的第二部分。现在,我们被告知我们的实际尺寸应该是多少,并且必须通过调用子元素的 Arrange 方法来告诉子元素它们实际将有多大以及它们的确切位置。我们的第一个实现并不复杂。我们只需要计算固定尺寸子元素需要多少空间,然后将剩余空间分配给可变尺寸子元素。

protected override Size ArrangeOverride(Size finalSize)
{
    double TotalProportion = 0;
    double FixedWidth = 0;
    // The first time we iterate over the children
    // we're looking for how much space needs to be
    // allocated for fixed-size elements and what
    // the total proportion is for sizing the variable-size
    // elements.
    foreach (UIElement Child in InternalChildren)
    {
        double ChildProportion = GetProportion(Child);
        if (ChildProportion == 0)
        {
            FixedWidth += Child.DesiredSize.Width;
        }
        else
        {
            TotalProportion += ChildProportion;
        }
    }
    double VariableWidth = Math.Max(finalSize.Width - FixedWidth, 0);

    // The second time we iterate over the children
    // we'll tell them the size they're getting.
    double CurrentPos = 0;
    foreach (UIElement Child in InternalChildren)
    {
        double ChildProportion = GetProportion(Child);
        double ChildWidth;
        if (ChildProportion == 0)
        {
            ChildWidth = Child.DesiredSize.Width;
        }
        else
        {
            ChildWidth = ChildProportion / TotalProportion * VariableWidth;
        }
        Child.Arrange(new Rect(CurrentPos, 0, ChildWidth, finalSize.Height));
        CurrentPos += ChildWidth;
    }

    return finalSize;
}

但现在让我们让我们的 Sizer 更通用。与其只将所有内容拉伸到相同的高度,不如使用 VerticalAlignment 属性允许子元素像 StackPanel 一样指定其垂直处理方式。这没什么复杂的,但有一个小窍门:VerticalAlignmentFrameworkElement 类的一个属性,但我们的子元素包含在 UIElement 的集合中,因此需要进行类型转换。(FrameworkElement 继承自 UIElement。)

double VerticalPos = 0;
double ChildHeight = finalSize.Height;
FrameworkElement ChildElement = Child as FrameworkElement;
if (ChildElement != null)
{
    if (ChildElement.VerticalAlignment != VerticalAlignment.Stretch)
    {
        ChildHeight = Child.DesiredSize.Height;
    }
    switch (ChildElement.VerticalAlignment)
    {
        case VerticalAlignment.Top:
        case VerticalAlignment.Stretch:
            VerticalPos = 0;
            break;
        case VerticalAlignment.Bottom:
            VerticalPos = finalSize.Height - ChildHeight;
            break;
        case VerticalAlignment.Center:
            VerticalPos = (finalSize.Height - ChildHeight) / 2;
            break;
    }
}
Child.Arrange(new Rect(CurrentPos, VerticalPos, ChildWidth, ChildHeight));

FlexGridSizer

内置的 Grid 面板具有一些不错的调整大小功能,其星号语义(star semantics),但我们需要为其他列指定宽度,而不是让它们自动适应内容。如果不对大小进行定义,还必须定义 ColumnDefinitionRowDefinition 将显得过于冗长。因此,让我们创建一个 FlexGridSizer 类,使其能够适应其内容的大小。这是受 wxWidgets 中的 wxFlexGridSizer 的启发,但并不完全相同。

本文不详细介绍实现,因为概念与我们构建 BoxSizer 的方式非常相似。现在我们是在两个方向上进行布局。因此,这里有一个 FlexGridSizer 如何使用的例子

Sizers2.png

以及 XAML

<Window x:Class="wxl.FlexGridSizerDemo"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:wxl="clr-namespace:wxl" 
    Title="FlexGridSizer Demo" Width="200" SizeToContent="Height">
    <wxl:FlexGridSizer Margin="2">
        <Label wxl:FlexGridSizer.Row="0">First Name</Label>
        <TextBox wxl:FlexGridSizer.Row="0" 
	wxl:FlexGridSizer.Column="1" wxl:FlexGridSizer.HorizontalProportion="1" />
        <Label wxl:FlexGridSizer.Row="1">Last Name</Label>
        <TextBox wxl:FlexGridSizer.Row="1" wxl:FlexGridSizer.Column="1" />
        <Label wxl:FlexGridSizer.Row="2">Location</Label>
        <TextBox wxl:FlexGridSizer.Row="2" wxl:FlexGridSizer.Column="1" />
        <Label wxl:FlexGridSizer.Row="3">User Icon</Label>
        <TextBox wxl:FlexGridSizer.Row="3" wxl:FlexGridSizer.Column="1" />
        <Button wxl:FlexGridSizer.Row="3" 
		wxl:FlexGridSizer.Column="2" Padding="3">. . .</Button>
    </wxl:FlexGridSizer>
</Window>

现在你可能会问自己,“我能用普通的 Grid 获得同样的效果吗?”是的,你可以。让我们看看 XAML

<Window x:Class="wxl.GridDemo"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Grid Demo" Width="200" SizeToContent="Height">
    <Grid Margin="2">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="*"/>
            <ColumnDefinition Width="Auto"/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>
        <Label Grid.Row="0">First Name</Label>
        <TextBox Grid.Row="0" Grid.Column="1" />
        <Label Grid.Row="1">Last Name</Label>
        <TextBox Grid.Row="1" Grid.Column="1" />
        <Label Grid.Row="2">Location</Label>
        <TextBox Grid.Row="2" Grid.Column="1" />
        <Label Grid.Row="3">User Icon</Label>
        <TextBox Grid.Row="3" Grid.Column="1" />
        <Button Grid.Row="3" Grid.Column="2" Padding="3">. . .</Button>
    </Grid>
</Window>

基本上,我们有更多的 XAML 行,尽管它们更短。尽管预先定义行和列将帮助你理清复杂的网格,但我喜欢能够摆脱这些“样板”定义来创建简单的网格。

最终结果

此 zip 文件(10.07 KB) 包含上面讨论的 BoxSizer FlexGridSizer 的代码。

功劳归于应得者

网上有很多关于实现自定义 Panel 的资源,但我发现其中有两个特别有用

  • Switched On The Code 的 The Tallest 撰写的《WPF 教程 - 创建自定义 Panel 控件》
  • Alec Bryte 在其网站上撰写的《WPF Proportional Panel》

我还要感谢 CodeProject 主办此活动并提供了许多我发现有帮助的资源,感谢 wxWidgets 项目为这些类提供了灵感,并通过其示例和社区教给了我很多关于编程的知识。

修订历史

  • 7 月 2 日
    • 初始版本
  • 7 月 3 日
    • 根据 Reinhard Ostermeier 的评论,已更新以更准确地描述内置 Grid 类的功能。
© . All rights reserved.