布局系统

本主题描述 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 的边界框现在可见。

如黄色矩形所示,为 TextBlock 元素分配的空间实际上远远大于其显示的空间。 由于还有其他元素添加到 Grid,此分配可能会收缩或扩展,这取决于所添加元素的类型和大小。

可以使用 GetLayoutSlot 方法将 TextBlock 的布局槽转换为 Path。 此方法对于显示元素的边界框非常有用。

Private Sub getLayoutSlot1(ByVal sender As Object, ByVal e As RoutedEventArgs)
    Dim myRectangleGeometry As New RectangleGeometry
    myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1)
    Dim myGeometryDrawing As New GeometryDrawing
    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
private void getLayoutSlot1(object sender, System.Windows.RoutedEventArgs e)
{
    RectangleGeometry myRectangleGeometry = new RectangleGeometry();
    myRectangleGeometry.Rect = LayoutInformation.GetLayoutSlot(txt1);
    GeometryDrawing myGeometryDrawing = new GeometryDrawing();
    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();
}

布局系统

简单地说,布局是一个递归系统,实现对元素进行大小调整、定位和绘制。 更具体地说,布局描述对 Panel 元素的 Children 集合的成员执行的测量和排列过程。 布局是一个计算密集型过程。 Children 集合越大,必须执行的计算次数就越多。 根据拥有该集合的 Panel 元素所定义的布局行为,还可能会增加复杂性。 相对简单的 Panel(如 Canvas)的性能可能明显优于较为复杂的 Panel(如 Grid)。

每当子 UIElement 改变其位置时,布局系统就可能触发一个新的处理过程。 因此,了解哪些事件会调用布局系统就很重要,因为不必要的调用可能导致应用程序性能变差。 下面描述调用布局系统时发生的过程。

  1. UIElement 通过首先测量它的核心属性来开始布局过程。

  2. 计算在 FrameworkElement 上定义的大小调整属性,例如 WidthHeightMargin

  3. 应用特定于 Panel 的逻辑,例如 Dock 方向或堆栈 Orientation

  4. 测量所有子级后排列内容。

  5. Children 集合绘制到屏幕。

  6. 如果其他 Children 添加到集合、应用 LayoutTransform 或调用 UpdateLayout 方法,会再次调用此过程。

下面的章节将更详尽地定义此过程及其调用方式。

测量和排列子控件

布局系统为 Children 集合的每个成员完成两个处理过程:测量处理过程和排列处理过程。 每个子 Panel 均提供自己的 MeasureOverrideArrangeOverride 方法,以实现自己特定的布局行为。

测量处理过程期间,将对 Children 集合的每个成员进行计算。 此过程将以调用 Measure 方法开始。 此方法将在父 Panel 元素的实现中调用,无需为要出现的布局显式调用该方法。

首先,将计算 UIElement 的本机大小属性,如 ClipVisibility。 这将生成一个名为 constraintSize 的传递给 MeasureCore 的值。

其次,会处理在 FrameworkElement 上定义的框架属性,这将影响 constraintSize 的值。 这些属性通常描述基础 UIElement 的大小调整特性,例如其 HeightWidthMarginStyle。 上述每个属性均可能更改显示元素所必需的空间。 然后使用 constraintSize 作为参数调用 MeasureOverride

注意注意

HeightWidthActualHeightActualWidth 的属性之间存在着差异。例如,ActualHeight 属性是基于其他高度输入和布局系统的计算值。该值是由布局系统本身基于实际呈现处理过程设置的,因此可能稍微小于属性(例如作为输入更改基础的 Height)的设置值。

由于 ActualHeight 是一个计算值,因此您应该知道,作为布局系统各种操作的结果,该值可能有多次或递增的报告的更改。布局系统可能正在计算子元素所需的测量空间、父元素的约束等。

测量处理过程的最终目标是让子级确定其 DesiredSize,这是在 MeasureCore 调用期间发生的。 DesiredSize 值由 Measure 存储,以便在内容排列处理过程期间使用。

此排列处理过程将以调用 Arrange 方法开始。 在排列处理过程期间,父 Panel 元素生成一个代表子级边界的矩形。 该值会传递给 ArrangeCore 方法以便进行处理。

ArrangeCore 方法将计算子元素的 DesiredSize 以及可能会影响元素呈现大小的任何其他边距。 ArrangeCore 生成 arrangeSize,后者传递到 PanelArrangeOverride 方法作为参数。 ArrangeOverride 生成子级的 finalSize。 最后,ArrangeCore 方法执行偏移属性(例如边距和对齐方式)的最终计算,并将子元素放在其布局槽内。 子级无需(且通常不会)填充整个分配空间。 然后,控件返回到父 Panel,至此布局过程完成。

面板元素和自定义布局行为

WPF 包括从 Panel 派生的一组元素。 这些 Panel 元素可用来实现许多复杂的布局。 例如,堆栈元素可以使用 StackPanel 元素方便地实现,而较为复杂和自由流动的布局可以使用 Canvas 来实现。

下表概括了可用的布局 Panel 元素。

面板名称

说明

Canvas

定义一个区域,在此区域内,您可以使用相对于 Canvas 区域的坐标显式定位子元素。

DockPanel

定义一个区域,在此区域中,您可以使子元素互相水平或垂直排列。

Grid

定义由行和列组成的灵活的网格区域。

StackPanel

将子元素排列成一行(可沿水平或垂直方向)。

VirtualizingPanel

为虚拟化其子数据集合的 Panel 元素提供一个框架。 这是一个抽象类。

WrapPanel

从左至右按顺序位置定位子元素,在包含框的边缘处将内容断开至下一行。 后续排序按照从上至下或从右至左的顺序进行,具体取决于 Orientation 属性的值。

对于其所需布局不可能使用任意预定义的 Panel 元素来实现的应用程序,您可以通过从 Panel 继承并重写 MeasureOverrideArrangeOverride 方法来实现自定义布局行为。 有关示例,请参见 Custom Radial Panel Sample(自定义的射线面板示例)

布局性能注意事项

布局是一个递归过程。 Children 集合中的每个子元素会在每次调用布局系统期间得到处理。 因此,应避免在不必要时触发布局系统。 以下注意事项有助于获得更好的性能。

  • 应注意哪些属性值更改会强制执行布局系统的递归更新。

    如果依赖项属性的值可能导致布局系统被初始化,则会使用公共标志对该依赖项属性进行标记。 AffectsMeasureAffectsArrange 提供有关哪些属性值更改会强制执行布局系统递归更新的有用提示。 通常,任何可能影响元素边界框大小的属性应将 AffectsMeasure 标志设置为 true。 有关更多信息,请参见依赖项属性概述

  • 如有可能,应使用 RenderTransform 而不要使用 LayoutTransform

    LayoutTransform 可能是影响user interface (UI) 内容的非常有用的方式。 不过,如果转换的效果无需对其他元素的位置施加影响,则最好改为使用 RenderTransform,因为 RenderTransform 不会调用布局系统。 LayoutTransform 会应用其转换,并强制执行递归布局更新,以便获得受影响元素的新位置。

  • 避免不必要地调用 UpdateLayout

    UpdateLayout 方法强制执行递归布局更新,但常常却是不必要的。 除非您确定需要进行完整更新,否则请依赖布局系统来为您调用此方法。

  • 当使用大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而不是常规 StackPanel

    通过虚拟化子集合,VirtualizingStackPanel 仅在内存中保留当前位于父 ViewPort 内的对象。 因此,在大多数情况下性能会得到极大改进。

子像素呈现和布局舍入

WPF 图形系统使用与设备无关的单元来使分辨率和设备独立。 每个与设备无关的像素都会随系统的dots per inch (dpi) 设置自动缩放。 这支持 WPF 应用程序可以根据不同的 dpi 设置进行适当的缩放,并使应用程序可以自动识别 dpi。

但是,这种 dpi 无关性可能会因为抗锯齿而呈现出不规则的边缘。 当边缘的位置处于设备像素内而不是位于设备像素之间时,可出现这些通常显示为模糊边缘或半透明的效果边缘。 布局系统提供了一种使用布局舍入对此进行调整的方法。 布局舍入是指布局系统在布局处理过程期间舍入任何非整数像素值。

默认情况下,禁用布局舍入。 若要启用布局舍入,请将任何 FrameworkElementUseLayoutRounding 属性设置为 true。 因为该属性是依赖项属性,所以该值将传播到可视化树中的所有子级。 若要为整个 UI 启用布局舍入,请将根容器的 UseLayoutRounding 设置为 true。 有关示例,请参见UseLayoutRounding

接下来的内容

了解如何测量和排列元素是理解布局的第一步。 有关可用 Panel 元素的更多信息,请参见面板概述。 为更好地理解可能影响布局的各种定位属性,请参见Alignment、Margin 和 Padding 概述。 有关自定义 Panel 元素的示例,请参见 Custom Radial Panel Sample(自定义的射线面板示例)。 如果您准备将布局全部放入一个轻型应用程序中,请参见演练:开始使用 WPF

请参见

参考

FrameworkElement

UIElement

概念

面板概述

Alignment、Margin 和 Padding 概述

优化性能:布局和设计