WPF 的两列网格
一个自定义面板,用于成对的标签-控件行,例如,在首选项屏幕中很有用。
引言
您是否曾经设计过一个屏幕,其中您拥有一对对“Label
: TextBox
”的行,彼此相邻排列?
我猜是这样,因为这种 UI 几乎是每个配置窗口的标准配置。 我也猜,在设计时,您会因为必须一直在 Grid
中定义新行而感到沮丧。 或者您的 StackPanel
在 StackPanel
内部没有正确对齐这些列。
将网格的行/列布局与 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"
条目。
解决方案
让我们来定义一个具有这些属性的自定义面板
- 没有行/列定义。 相反,隐式假定两列和无限数量的行。
- 没有显式定位。 根据子元素的顺序推断在网格中的位置。
对于列大小调整,我们定义如下
- 第 1 列将仅使用其最宽子项所需的空间
- 第 2 列将使用剩余宽度
这会产生所有第 2 列中的项目将很好地对齐的效果,这最符合此类屏幕的 UI 设计最佳实践。
此外,我们将添加两个属性来控制子项之间的额外间距。
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:第一个版本。