WPF基础之布局系统
本主题描述 Windows Presentation Foundation (WPF) 布局系统。了解在构造外观醒目、性能优良的用户界面时如何以及何时进行布局计算是非常重要的。
布局系统
术语“布局”描述测量和排列 Panel 元素的 Children 集合的成员、然后在屏幕上绘制它们的过程。这是一个计算密集型过程,即 Children 集合越大,执行的计算次数就越多。根据拥有该集合的 Panel 元素所定义的布局行为,还可能会增加复杂性。如果不需要较为复杂的 Panel(如 Grid),则可以使用构造相对简单的布局(如 Canvas),这种布局可产生更佳的性能。
每当子 UIElement 改变其位置时,布局系统就可能触发一个新的处理过程。因此,了解哪些事件会调用布局系统就很重要,因为不必要的调用可能导致应用程序性能变差。
简单地说,布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制。布局系统为 Children 集合的每个成员完成两个处理过程:测量处理过程和排列处理过程。每个子 Panel 均提供自己的 MeasureOverride 和 ArrangeOverride 方法,以实现自己特定的布局行为。不论何时调用布局系统,都会发生以下系列事件。
子 UIElement 通过首先测量它的核心属性来开始布局过程。
计算在 FrameworkElement 上定义的大小调整属性,例如 Width、Height 和 Margin。
应用 Panel 特定逻辑,例如 Dock 方向或堆栈 Orientation。
测量所有子级后排列内容。
Children 集合绘制到屏幕。
如果其他 Children 添加到集合、应用 LayoutTransform 或调用 UpdateLayout 方法,会再次调用此过程。
下面的小节将更详尽地定义此过程及其调用方式。
元素边界框
在 Windows Presentation Foundation (WPF) 中构思应用程序布局时,了解环绕所有元素的边界框非常重要。这有助于您理解布局系统的行为。布局系统使用的每个 FrameworkElement 可以被视为是嵌入到布局分区中的矩形。LayoutInformation 类会公开,可以返回元素布局分配的几何边界或槽。矩形的大小是由系统通过计算可用屏幕空间、任意约束的大小、布局特定属性(如边距和填充)及父 Panel 元素的个别行为来确定的。通过处理此数据,系统将能够计算给定的 Panel 的所有子级的位置。牢记在父元素上定义的哪些大小调整特性(如 Border)会影响其子级,这非常重要。
例如,请考虑下面的简单布局方案。
可以使用以下可扩展应用程序标记语言 (XAML) 来实现此布局。
此单独的 TextBlock 元素是在 Grid 内承载的,而文本仅填充在其所在列的左上角,为 TextBlock 分配的空间实际更大。可以使用 GetLayoutSlot 方法检索任意 FrameworkElement 的边界框。使用此方法,TextBlock 元素的边界框是叠加的,这可能是因为 TextBlock 是在 Grid(它是允许共享布局坐标的 Panel 元素)内承载的。
现在很明显该元素由白线环绕,分配给 TextBlock 元素的分区实际远远大于其填充的空间。由于还有其他元素添加到 Grid,此分配可能会收缩或扩展,这取决于所添加元素的类型和大小。
将会使用 GetLayoutSlot 方法(它是一项用于显示元素边界框的有用技术)返回 TextBlock 的布局槽并将其转换成 Path。
测量和排列子控件
当呈现 Window 对象的内容时,会自动调用布局系统。为了显示内容,窗口的 Content 必须定义根 Panel(用于定义框架,Children 是按框架在屏幕上组织的)。
布局的第一个处理过程是测量处理过程,将在此对 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 存储,以便在内容排列过程期间使用。
此排列过程将以调用 Arrange 方法开始。在排列处理过程期间,父 Panel 元素生成一个代表子级边界的矩形。该值会传递给 ArrangeCore 方法以便进行处理。
ArrangeCore 方法计算子级的 DesiredSize,计算可能影响该元素呈现大小的任何其他边距,并生成 arrangeSize(作为参数传递给 Panel 的 ArrangeOverride)。ArrangeOverride 生成子级的 finalSize,最后,ArrangeCore 方法执行偏移属性(例如边距和对齐方式)的最终计算,并将子级放在其布局槽内。子级无需(且通常不会)填充整个分配空间。然后,控件返回到父 Panel,至此布局过程完成。
面板元素和自定义布局行为
Windows Presentation Foundation (WPF) 包括 Panel 元素的派生套件,可以实现许多复杂的布局。常见方案(如堆栈元素)可以使用 StackPanel 元素方便地实现,而较为复杂和自由流动的布局可以使用 Canvas 来实现。
下表概括了可用的布局元素。
面板名称
说明
Canvas
定义一个区域,在此区域内,您可以使用相对于 Canvas 区域的坐标显式定位子元素。
DockPanel
定义一个区域,在此区域中,您可以使子元素互相水平或垂直排列。
Grid
定义由行和列组成的灵活网格区域。
StackPanel
将子元素排列成一行(可沿水平或垂直方向)。
VirtualizingPanel
为“虚拟化”其子数据集合的 Panel 元素提供一个框架。这是一个抽象类。
WrapPanel
从左至右按顺序位置定位子元素,在包含框的边缘处将内容断开至下一行。后续排序按照从上至下或从右至左的顺序进行,具体取决于 Orientation 属性的值。
对于其所需应用程序布局不可能使用任意预定义的 Panel 元素来实现的方案,您可以通过从 Panel 继承、并重写 MeasureOverride 和 ArrangeOverride 方法来实现自定义布局行为。
布局性能注意事项
布局是一个递归过程。Children 集合中的每个子元素会在每次调用系统期间得到处理。因此,应避免在不必要时触发系统。以下提示有助于实现更高的性能。
其值可能导致布局系统被初始化的相关性属性会标记为公共标志。AffectsMeasure 和 AffectsArrange 提供有关哪个属性值更改会强制执行布局系统的递归更新的有用提示。通常,任何可能影响元素边界框大小的属性应将 AffectsMeasure 标志设置为 true。
LayoutTransform 可能是影响用户界面 (UI) 内容的非常有用的方式。不过,如果转换的效果无需对其他元素的位置施加影响,则最好改为使用 RenderTransform,因为 RenderTransform 不会调用布局系统。LayoutTransform 会应用其转换,并强制对帐户执行递归布局更新,以获取受影响元素的新位置。
避免不必要地调用 UpdateLayout。此方法强制递归布局更新,但常常却是不必要的。除非您确认需要进行完整更新,否则请依赖布局系统来为您调用此方法。
当处理大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而非常规 StackPanel。通过“虚拟化”子元素,VirtualizingStackPanel 仅在内存中保留当前位于父 ViewPort 内的对象。因此,在大多数情况下性能会得到极大改进。