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

WPF 的两列网格

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (13投票s)

2011 年 8 月 9 日

CPOL

3分钟阅读

viewsIcon

76112

downloadIcon

1379

一个自定义面板,用于成对的标签-控件行,例如,在首选项屏幕中很有用。

sample-app.png

引言

您是否曾经设计过一个屏幕,其中您拥有一对对“Label: TextBox”的行,彼此相邻排列?

我猜是这样,因为这种 UI 几乎是每个配置窗口的标准配置。 我也猜,在设计时,您会因为必须一直在 Grid 中定义新行而感到沮丧。 或者您的 StackPanelStackPanel 内部没有正确对齐这些列。

将网格的行/列布局与 StackPanel 的简单*无需定义任何内容*样式结合起来不是很好吗?

请继续阅读,我将展示一个实现此目的的自定义面板。

问题

WPF 中的 Grid 非常强大,几乎可以实现任何可想象的布局。 遗憾的是,它要求您预先定义所有行和列,并且还强制您为每个子项指定它们应该放置在网格的哪个单元格中。 要实现上述截图中的布局,您必须在 XAML 中编写类似这样的代码

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Label Content="Name:" />
    <TextBox Text="John Doe" VerticalAlignment="Center" Grid.Column="1" />
    <Label Content="Address:" Grid.Row="1" />
    <TextBox Text="34 Some Street, 123 45 SomeTown, Some Country" 
          VerticalAlignment="Center" Height="70" Grid.Row="1" Grid.Column="1"/>
    <Label Content="Position:" Grid.Row="2" />
    <TextBox Text="Manager" Grid.Row="2" Grid.Column="1"/>
</Grid>

更糟糕的是,如果您想移动内容(或在中间添加内容),您必须手动更新所有 Grid.Column="x"Grid.Row="y" 条目。

解决方案

让我们来定义一个具有这些属性的自定义面板

  • 没有行/列定义。 相反,隐式假定两列和无限数量的行。
  • 没有显式定位。 根据子元素的顺序推断在网格中的位置。

child-ordering.png

对于列大小调整,我们定义如下

  • 第 1 列将仅使用其最宽子项所需的空间
  • 第 2 列将使用剩余宽度

这会产生所有第 2 列中的项目将很好地对齐的效果,这最符合此类屏幕的 UI 设计最佳实践。

column-sizing.png

此外,我们将添加两个属性来控制子项之间的额外间距。

  • RowSpacing - 在每行之间添加一定数量的像素(默认为零)
  • ColumnSpacing - 在两列之间添加一定数量的像素(默认为零)

代码

要创建您自己的面板,您需要做的就是创建一个从 Panel继承的类。 这个类是 abstract 的,需要您重写两种方法

  • MeasureOverride - 给定大小约束,测量所有子项并计算面板的所需大小
  • ArrangeOverride - 根据父控件/面板提供的区域排列所有子项

测量子项

MeasureOverride 中,我们首先测量将出现在第一列中的所有子项

// First, measure all the left column children
for (int i = 0; i < VisualChildrenCount; i += 2)
{
    var child = Children[i];
    child.Measure(constraint);
    col1Width = Math.Max(child.DesiredSize.Width, col1Width);
    RowHeights.Add(child.DesiredSize.Height);
}

请注意,我们存储了第 1 列子项的最大宽度。 这将成为第一列的最终宽度。 我们还存储了所有行高,以便我们可以在最后计算最终高度。 我们将这些值存储在一个列表中的原因不是简单地将它们加起来,因为我们还没有测量第二列子项。 一行的两个子项具有不同高度是完全有效的。

现在,我们有足够的信息来开始测量第二列

// Then, measure all the right column children, they get whatever remains in width
var newWidth = Math.Max(0, constraint.Width - col1Width - ColumnSpacing);
Size newConstraint = new Size(newWidth, constraint.Height);
for (int i = 1; i < VisualChildrenCount; i += 2)
{
    var child = Children[i];
    child.Measure(newConstraint);
    col2Width = Math.Max(child.DesiredSize.Width, col2Width);
    RowHeights[i/2] = Math.Max(RowHeights[i/2], child.DesiredSize.Height);
}

newWidth 变量保存剩余的可用宽度。 它基本上是提供的边界减去第 1 列的宽度(减去任何列间距)。 Math.Max 的作用是确保我们没有得到负大小,以防第 1 列想要占据整个宽度。

另请注意我们如何更新 RowHeights 值,以防右侧子项比其左侧同级更高。

最后,我们有足够的信息将我们所需的大小返回给我们的父级

return new Size(
                col1Width + ColumnSpacing + col2Width, 
                RowHeights.Sum() + ((RowHeights.Count - 1) * RowSpacing));

排列子项

这比测量阶段更简单,因为我们已经准备好所有大小

/// <summary>
/// Position elements and determine the final size for this panel.
/// </summary>
/// <param name="arrangeSize">The final area where child elements should 
///                           be positioned.</param>
/// <returns>The final size required by this panel</returns>
protected override Size ArrangeOverride(Size arrangeSize)
{
    double y = 0;
    for (int i = 0; i < VisualChildrenCount; i++)
    {
        var child = Children[i];
        double height = RowHeights[i/2];
        if (i % 2 == 0)
        {
            // Left child
            var r = new Rect(0, y, Column1Width, height);
            child.Arrange(r);
        }
        else
        {
            // Right child
            var r = new Rect(Column1Width + ColumnSpacing, y, 
                             arrangeSize.Width - Column1Width - 
                             ColumnSpacing, height);
            child.Arrange(r);
            y += height;
            y += RowSpacing;
        }
    }
    return base.ArrangeOverride(arrangeSize);
}

我们基本上只是遍历所有子项并将它们放置在正确的位置。 变量 y 维护当前行的垂直位置,并且每个子项的 x 位置是根据先前测量的列宽度计算出来的。

基本上就是这样。 顶部第一个截图所示的布局现在可以这样实现

<local:TwoColumnGrid>
    <Label Content="Name:" />
    <TextBox Text="John Doe" VerticalAlignment="Center" />
    <Label Content="Address:" />
    <TextBox Text="34 Some Street, 123 45 SomeTown, Some Country" 
             VerticalAlignment="Center" Height="70" />
    <Label Content="Position:" />
    <TextBox Text="Manager" />
</local:TwoColumnGrid>

历史

  • 2011-08-09:第一个版本。
© . All rights reserved.