布局
本主题介绍 Windows Presentation Foundation (WPF) 布局系统。 了解布局计算的发生方式和时间对于在 WPF 中创建用户界面至关重要。
本主题包含以下部分:
元素边界框
在考虑 WPF 中的布局时,必须了解包围所有元素的边界框。 布局系统使用的每个 FrameworkElement 都可以被视为嵌入到布局中的矩形。 LayoutInformation 类返回元素布局分配的区域边界或插槽。 矩形的大小通过计算可用屏幕空间、任何约束的大小、特定于布局的属性(如边距和填充)以及父 Panel 元素的各个行为来确定。 处理此数据时,布局系统能够计算特定 Panel 的所有子级的位置。 请务必记住,父元素(如 Border)上定义的尺寸特性会影响其子元素。
下图显示了一个简单的布局。
可以使用以下 XAML 实现此布局。
<Grid Name="myGrid" Background="LightSteelBlue" Height="150">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Name="txt1" Margin="5" FontSize="16" FontFamily="Verdana" Grid.Column="0" Grid.Row="0">Hello World!</TextBlock>
<Button Click="getLayoutSlot1" Width="125" Height="25" Grid.Column="0" Grid.Row="1">Show Bounding Box</Button>
<TextBlock Name="txt2" Grid.Column="1" Grid.Row="2"/>
</Grid>
单个 TextBlock 元素托管在 Grid 中。 虽然文本仅填充第一列的左上角,但 TextBlock 的分配空间实际上要大得多。 可以使用 GetLayoutSlot 方法检索任何 FrameworkElement 的边界框。 下图显示 TextBlock 元素的边界框。
如黄色矩形所示,TextBlock 元素的分配空间实际上比显示的要大得多。 由于将其他元素添加到 Grid,因此此分配可能会收缩或扩展,具体取决于添加的元素的类型和大小。
使用 GetLayoutSlot 方法将 TextBlock 的布局槽转换为 Path。 此方法可用于显示元素的边界框。
private void getLayoutSlot1(object sender, System.Windows.RoutedEventArgs e)
{
RectangleGeometry myRectangleGeometry = new RectangleGeometry();
myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1);
Path myPath = new Path();
myPath.Data = myRectangleGeometry;
myPath.Stroke = Brushes.LightGoldenrodYellow;
myPath.StrokeThickness = 5;
Grid.SetColumn(myPath, 0);
Grid.SetRow(myPath, 0);
myGrid.Children.Add(myPath);
txt2.Text = "LayoutSlot is equal to " + LayoutInformation.GetLayoutSlot(txt1).ToString();
}
Private Sub getLayoutSlot1(ByVal sender As Object, ByVal e As RoutedEventArgs)
Dim myRectangleGeometry As New RectangleGeometry
myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1)
Dim myPath As New Path
myPath.Data = myRectangleGeometry
myPath.Stroke = Brushes.LightGoldenrodYellow
myPath.StrokeThickness = 5
Grid.SetColumn(myPath, 0)
Grid.SetRow(myPath, 0)
myGrid.Children.Add(myPath)
txt2.Text = "LayoutSlot is equal to " + LayoutInformation.GetLayoutSlot(txt1).ToString()
End Sub
布局系统
简单地说,布局是一个递归系统,实现对元素进行大小调整、定位和绘制。 更具体地说,布局描述了测量和排列 Panel 元素 Children 集合的成员的过程。 布局是一个复杂的过程。 Children 集合越大,必须进行的计算数就越大。 根据拥有该集合的 Panel 元素所定义的布局行为,还可能会增加复杂性。 相对简单的 Panel(如 Canvas)比更复杂的 Panel(如 Grid)的性能要好得多。
每当子 UIElement 改变其位置时,布局系统都可能触发一个新的传递。 因此,请务必了解可以调用布局系统的事件,因为不必要的调用可能会导致应用程序性能不佳。 下面介绍了调用布局系统时发生的过程。
子 UIElement 通过首先测量其核心属性来开始布局过程。
计算 FrameworkElement 上定义的大小调整属性,例如 Width、Height 和 Margin。
应用 Panel 特定的逻辑,例如 Dock 方向或堆叠 Orientation。
测量所有子级后排列内容。
在屏幕上绘制 Children 集合。
如果将其他 Children 添加到集合、应用 LayoutTransform 或调用 UpdateLayout 方法,则会再次调用该过程。
以下各节将更详细地定义此过程及其调用方式。
测量和排列子元素
布局系统为 Children 集合的每个成员完成两个过程:一个测量过程和一个排列过程。 每个子 Panel 都提供自己的 MeasureOverride 和 ArrangeOverride 方法来实现自己的特定布局行为。
在测量过程中,会计算 Children 集合的每个成员。 该过程从调用 Measure 方法开始。 此方法在父 Panel 元素的实现中调用,无需显式调用此方法即可进行布局。
首先,计算 UIElement 的本机大小属性,例如 Clip 和 Visibility。 这将生成一个名为 constraintSize
的值,该值传递给 MeasureCore。
其次,处理在 FrameworkElement 上定义的框架属性,这会影响 constraintSize
的值。 这些属性通常描述基础 UIElement的尺寸特性,例如其 Height、Width、Margin和 Style。 每个属性都可以更改显示元素所需的空间。 然后使用 constraintSize
作为参数调用 MeasureOverride。
说明
Height 和 Width、ActualHeight 和 ActualWidth的属性之间存在差异。 例如,ActualHeight 属性是基于其他高度输入和布局系统的计算值。 该值由布局系统本身在实际渲染过程中设置,因此可能会略微滞后于作为输入更改基础的属性设置值(如 Height)。
因为 ActualHeight 是计算所得的值,所以你应该知道,由于布局系统各种操作的结果,该值可能有多次或递增的报告的更改。 布局系统可能会计算子元素所需的度量值空间、父元素的约束等。
测量过程的最终目标是让子元素确定其 DesiredSize,这发生在 MeasureCore 调用期间。 Measure 存储 DesiredSize 值,供在内容排列过程中使用。
排列过程从调用 Arrange 方法开始。 在排列过程中,父 Panel 元素会生成一个表示子元素边界的矩形。 此值将传递给 ArrangeCore 方法进行处理。
ArrangeCore 方法计算子元素的 DesiredSize,并且计算可能影响元素呈现大小的任何其他边距。 ArrangeCore 生成一个 arrangeSize
,后者作为参数传递给 Panel 的 ArrangeOverride 方法。 ArrangeOverride 生成子元素的 finalSize
。 最后,ArrangeCore 方法执行偏移量属性(如边距和对齐)的最终计算,并将子元素放在其布局槽内。 子元素不需要(并且通常不)填充整个分配空间。 然后,控件将返回到父 Panel,并且布局过程已完成。
面板元素和自定义布局行为
WPF 包括一组派生自 Panel的元素。 这些 Panel 元素可实现许多复杂的布局。 例如,可以通过使用 StackPanel 元素轻松实现堆叠元素,而使用 Canvas可以实现更复杂的自由流动布局。
下表汇总了可用布局 Panel 元素。
面板名称 | 描述 |
---|---|
Canvas | 定义一个区域,可在其中通过相对于 Canvas 区域的坐标显式定位子元素。 |
DockPanel | 定义一个区域,可在其中使子元素相互水平或垂直排列。 |
Grid | 定义由列和行组成的灵活网格区域。 |
StackPanel | 将子元素排列成水平或垂直的一行。 |
VirtualizingPanel | 为虚拟化其子数据集合的 Panel 元素提供一个框架。 这是一个抽象类。 |
WrapPanel | 按从左到右的顺序位置定位子元素,在包含框的边缘处将内容切换到下一行。 随后的排序按照从上到下或从右到左的顺序依次进行,具体顺序取决于 Orientation 属性的值。 |
对于那些无法通过任何预定义 Panel 元素实现所需布局的应用程序,可以通过继承 Panel 并重写 MeasureOverride 和 ArrangeOverride 方法来实现自定义布局行为。
布局性能注意事项
布局是递归过程。 Children 集合中的每个子元素在每次调用布局系统期间都会得到处理。 因此,应避免在不必要时触发布局系统。 以下注意事项可帮助你实现更好的性能。
请注意,哪些属性值更改将强制布局系统进行递归更新。
可能导致布局系统初始化的依赖属性会用公共标志进行标记。 AffectsMeasure 和 AffectsArrange 提供了有用的线索,说明哪些属性值更改将强制布局系统进行递归更新。 通常,任何可能影响元素边界框大小的属性都应将 AffectsMeasure 标志设置为 true。 有关详细信息,请参阅 依赖项属性概述。
如果可能,请使用 RenderTransform 而不是 LayoutTransform。
LayoutTransform 是一种影响用户界面 (UI) 内容的非常有用的方式。 但是,如果转换的效果不必影响其他元素的位置,最好改用 RenderTransform,因为 RenderTransform 不调用布局系统。 LayoutTransform 会应用其转换,并强制执行递归布局更新以获得受影响元素的新位置。
避免对 UpdateLayout 进行不必要的调用。
UpdateLayout 方法强制进行递归布局更新,并且通常不需要。 除非您确定需要完全更新,否则请依赖布局系统自动为您调用此方法。
在处理大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而不是常规的 StackPanel。
通过虚拟化子集合,VirtualizingStackPanel 仅将当前位于父视图端口中的对象保留在内存中。 因此,在大多数情况下,性能会大幅提高。
子像素渲染和布局舍入
WPF 图形系统使用独立于设备的单元来实现分辨率和设备独立性。 每个设备独立像素都会根据系统的每英寸像素点数(dpi)设置自动缩放。 这为 WPF 应用程序提供了不同 dpi 设置的适当缩放,并使应用程序自动感知 dpi。
但是,这种 dpi 无关性可能由于抗锯齿而呈现出不规则的边缘。 这些伪影通常被视为模糊或半透明边缘,当边缘的位置落在设备像素的中间而不是设备像素之间时,就可能出现。 布局系统提供了一种通过布局倒圆对此进行调整的方法。 布局舍入是布局系统在布局传递中舍入任何非整数像素值的情况。
默认情况下禁用布局舍入。 若要启用布局舍入,请在任何 FrameworkElement 上将 UseLayoutRounding 属性设置为 true
。 因为它是一个依赖属性,所以该值将传播到可视化树中的所有子级。 若要为整个 UI 启用布局舍入,请在根容器上将 UseLayoutRounding 设置为 true
。 有关示例,请参阅 UseLayoutRounding。
下一步
了解元素的测量和排列方式是理解布局的第一步。 有关可用 Panel 元素的详细信息,请参阅 面板概述。 若要更好地了解可能影响布局的各种定位属性,请参阅 对齐、边距和填充概述。 准备好将其全部整合到轻量级应用程序中时,请参阅 演练:我的第一个 WPF 桌面应用程序。