我们在之前的“ UWP控件开发——用NuGet包装自己的控件 “一文中曾提到XAML的布局系统 和平时使用上的一些问题(重写 Measure/Arrange 还是使用 SizeChanged ?),这篇博文就来为大家简单地描述一下XAML布局系统的行为,并且归纳几个规则。当然真正的XAML布局系统十分复杂,本文无意把情况弄得太复杂,就从一个最简单最直观的例子入手,来为大家提供一点理解XAML布局的新思路。
假设我们有一个Templated Control,其XAML描述如下:
<Style TargetType="local:CustomControl1"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="local:CustomControl1"> <Border x:Name="OuterBorder" BorderBrush="Yellow" BorderThickness="20"> <Border x:Name="InnerBorder" BorderThickness="20" BorderBrush="Red" /> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
两个 Border 嵌套,边宽20。我们的目的就是通过代码来改变 InnerBorder 的大小。比如长宽都变成 OuterBorder 的一半大。
我们很容易就写出了这样的代码:
public sealed class CustomControl1 : Control { public CustomControl1() {...} private Border _border; private Border _inner; protected override void OnApplyTemplate() { base.OnApplyTemplate(); _border = GetTemplateChild("OuterBorder") as Border; _inner = GetTemplateChild("InnerBorder") as Border; if (_border != null && _inner != null) { _border.SizeChanged += (s, e) => { _inner.Width = _border.ActualWidth / 2; _inner.Height = _border.ActualHeight / 2; }; } } }
works perfectly。这一实现很好地达到了我们的需求。(而且对于这样的简单的情况设计器还是能够正常处理的)
但是这却隐藏着问题。首先, SizeChanged 事件是由一轮Measure/Arrange完成后触发的。
XAML的核心布局流程,是从根元素 即页面开始,递归向下。第一次挨个调用 Measure ,提供能用的大小,并确定每个子项所希望的空间大小;再来一次挨个调用 Arrange,提供能用大小,按实际情况给子项分配空间(不一定能满足它们的需要)和确定位置 。本例的过程中就涉及到 OuterBorder 和 InnerBorder ,它们以此能根据 Border 类布局规则确定自己的大小,即刨去 BorderThickness。
这之后, OuterBorder 和 InnerBorder 的 实际 大小就确定了。如果和上次布局的结果不一样, OuterBorder 就会触发 SizeChanged 事件(是Chang* ed* 哦),改变 InnerBorder 的 设定 大小。因为设定大小变化了,会引发新一轮递归Measure和Arrange。这一次之后, OuterBorder 的大小不变, InnerBorder 的大小变成 OuterBorder 的一半。之后没有事件和布局再被触发,大家相安无事。
但实际上,布局进行了两轮。如果Visual Tree很大的话,后果可想而知。
那么,根据我们刚才介绍的过程,从Measure出发,实现如下(去掉 SizeChanged 的事件绑定并override MeasureOvrride 方法):
public sealed class CustomControl1 : Control { public CustomControl1() {...} private Border _border; private Border _inner; protected override void OnApplyTemplate() { base.OnApplyTemplate(); _border = GetTemplateChild("Border") as Border; _inner = GetTemplateChild("InnerBorder") as Border; } protected override Size MeasureOverride(Size availableSize) { // availableSize就是OuterBorder的大小 if (_inner != null) { _inner.Width = availableSize.Width / 2; _inner.Height = availableSize.Height / 2; } return base.MeasureOverride(availableSize); } }
设定大小后再进入真正的measure环节,一次性搞定布局。原因就在于我们在布局开始之前就搞定了Size信息,而不是在布局结束后再把它辛辛苦苦计算出来的Size踩在地上并让它重来一遍。在我们设定的需求看来,甚至无需插手Arrange流程。
当然,这免不了地要自己计算Size,可能需要手动减去 BorderThickness 的大小,甚至还可能要自行调用一次 Measure 。复杂的具体情况需要具体分析。
通过调试工具,我们来对比一下两种方法的实际性能:
SizeChanged | MeasureOverride |
在两种实现下,分别大力地快速拖动窗口大小。。。
其中柱形图是一段时间内UI线程的响应情况,占最大比重的橙色是布局行为。下面的扇形图是选中差不多的时间段内,布局消耗的占比情况。
可见通过提供Measure策略的方式,即使是这样简单的设定,性能提升也还看得出来。
如果我们发扬奥卡姆剃刀的精神,不要自己写这陌生的 MeasureOverride ,用 Grid 来做如何?
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="2*"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*"/> <RowDefinition Height="2*"/> <RowDefinition Height="*"/> </Grid.RowDefinitions> <Border x:Name="Border" BorderBrush="Yellow" BorderThickness="20" Grid.RowSpan="3" Grid.ColumnSpan="3"/> <Border x:Name="InnerBorder" BorderThickness="20" BorderBrush="Red" Grid.Column="1" Grid.Row="1"/> </Grid>
OnApplyTemplate 和 MeasureOverride 都可以不要了,整个code behind十分清爽。行为看起来差不多,那么性能呢?
想必 Grid 作为标准控件,优化得应该很好了,但它本身就有一点复杂,和 MesureOverride 的实现在性能上有一点点差距。但毕竟我们这样简单的例子对于 Grid 太不公平了,对于更为复杂的情况,还是要使用 Grid 的。
说了这么多,主要是表现一下不必要的布局对于性能的影响,以及 对于这样的简单情况 如何替代原有实现。
对于布局有影响的操作大致有:
如果调用了这些属性方法,就需要顾虑一下是否会造成不必要的布局了,特别是在 SizeChanged 这样的由布局触发的事件里。当然这也是一般论,如果控件本来就隐藏了,或者Template改变了原有外型,这些内容也自然随之变化。
P.S. RenderTransform 是不造成重新布局的。
另外,就本文的例子来说,并不是要大家都把 SizeChanged 改写成 MeasureOverride 。
MeasureOverride 给了一个好处,就是第一时间获知高层布局的相关信息,也就能赶在布局前最后设置一次属性; SizeChanged 能给出复杂布局计算后的最新尺寸,如果自己来计算的话没有意义。总之还是要因地制宜。
虽然本文的例子十分简单,可能没有多少实际意义,不过希望通过它介绍的流程,能为大家的开发提供一点新的思路。
[1] 开源的WPF中的Border.MeasureOverride实现: http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Border.cs,00c166b0e025bc8d
[2] WPF中的Grid.MeasureOverride实现: http://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Grid.cs,f9ce1d6be154348a