您的位置:首页 > 其它

WPF/Silverlight Layout 系统概述——Measure(转)

2014-09-15 10:29 330 查看

前言

在WPF/Silverlight当中,如果已经存在的Element无法满足你特殊的需求,你可能想自定义Element,那么就有可能会面临重写MeasureOverride和ArrangeOverride两个方法,而这两个方法是WPF/SL的Layout系统提供给用户的自定义接口,因此,理解Layout系统的工作机制,对自定义Element是非常有必要的。那么,究竟WPF/SL的Layout系统是怎么工作的呢?接下来,我简单的描述一下,然后,在后面的章节具体分析。

简单来说,WPF的Layout系统是一个递归系统,他有两个子过程,总是以调用父元素的Measure方法开始,以调用Ararnge方法结束,而进入每个子过程之后,父元素又会调用孩子元素的Measure,完成后,又调用孩子元素的Arrange方法,这样一直递归下去。而对两个子过程的一次调用,可以看作是一次会话,可以理解为下图所示:





这个会话可以用下面一段话描述:

子过程1:父根据自己的策略给孩子一个availableSize,并发起对话,通过调用孩子的Measure(availableSize)方法,询问孩子:你想要多大的空间显示自己?孩子接到询问后,根据父给的availableSize以及自己的一些限制,比如Margin,Width,等等,孩子回答:我想要XXX大小的空间。父拿到孩子给的期望的空间大小后,根据自己的策略开始真正给孩子分配空间,就进入第二个子过程。

子过程2:父拿到孩子的期望空间后,再根据自己的情况,决定给孩子分配finalRect大小的矩形区域,然后他发起对话,调用孩子的Arrange(finalRect)给孩子说:我给你了finalRect这么大的空间。孩子拿到这个大小后,会去布置它的内容,并且布置完成后,会告诉父:其实我用了XXX大小的空间来绘制我自己的内容。父知道后,什么也没说,还是按照分配给他的finalRect去安置孩子,如果孩子最终绘制的区域大于这个区域,就被父裁剪了。Layout过程完成。

通过上面两个子过程的理解,或多或少对WPF的Layout系统有个初步的了解,接下来的章节,我具体描述Measure过程和Arrange过程具体做了哪些事情,帮助你跟深入的理解Layout系统。

预设条件

通过下面的一个预设场景,我们来展开Layout系统的讲解。

假定:我们需要自定义一个Panel,类型为*MyPanel*,MyPanel的父为*MyPanelParent*,也是一个Panel;MyPanel的孩子为*MyPanelChild*,也是一个Panel。

切入点1:重写MyPanelParent的MeasureOverride()和ArrangeOverride(),研究父如何影响孩子MyPanel的Layout;

切入点2:重写MyPanel.MeasureOverride()和ArrangeOverride方法,研究自身有哪些属性影响MyPanel的Layout,以及重写这两个方法时应该注意的点;

注意:后面的研究,我只基于Element的Width,也就是水平方向的维度,所有的数据都是只设置水平方向的,垂直方向设置的跟水平方向一致,但不做描述。

Measure过程概述

1.普通基类属性对Measure过程的影响

请看下面的一些设置:

<Windowx:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow"Height="522"Width="594"Loaded="Window_Loaded"xmlns:my="clr-namespace:WpfApplication1"> <Canvas> <my:MyPanelParentx:Name="myPanelParent1"Height="400"Width="400"Background="Green"Canvas.Left="10"Canvas.Top="10"> <my:MyPanelMargin="10"x:Name="myPanel1"Background="Red"MinWidth="150"Width="200"MaxWidth="250"/> <my:MyPanelMargin="10"x:Name="myPanel2"Background="Red"MinWidth="150"Width="200"MaxWidth="250"/> </my:MyPanelParent> </Canvas> </Window>
publicclassMyPanelParent:Panel { protectedoverrideSystem.Windows.SizeMeasureOverride(System.Windows.SizeavailableSize) { foreach(UIElementiteminthis.InternalChildren) { item.Measure(newSize(120,120));//这里是入口 } returnavailableSize; } protectedoverrideSystem.Windows.SizeArrangeOverride(System.Windows.SizefinalSize) { doublex=0; foreach(UIElementiteminthis.InternalChildren) { item.Arrange(newRect(x,0,item.DesiredSize.Width,item.DesiredSize.Height)); x+=item.DesiredSize.Width; } returnfinalSize; } } publicclassMyPanel:Panel { protectedoverrideSystem.Windows.SizeMeasureOverride(System.Windows.SizeavailableSize) { foreach(UIElementiteminthis.InternalChildren) { item.Measure(availableSize); } returnnewSize(50,50);//MyPanel返回它期望的大小 } protectedoverrideSystem.Windows.SizeArrangeOverride(System.Windows.SizefinalSize) { doublexCordinate=0; foreach(UIElementiteminthis.InternalChildren) { item.Arrange(newRect(newPoint(xCordinate,0),item.DesiredSize)); xCordinate+=item.DesiredSize.Width; } returnfinalSize; } }

在上面的设置之后,应用程序运行起来之后,Window的表现为:





分析一下设置:

MyPanel1.Width=200,MyPanel1.MinWidth=150,MyPanel1.MaxWidth=250,MyPanel1.Margin=Thickness(10)

MyPanel1.Measure()传入的参数为120*120,MyPanel1.MeasureOverride返回的参数为50*50

分析一下结果:

MyPanel1实际的画出来的大小(红色部分)是100*50

从结果可以看出,红色的部分受多个因素的影响,有人要问,我已经设置了MyPanel.Width=200,可是怎么画出来的Width却是100;MyPanel.Height没设置,可是画出来的却是50,为什么不是其他值。接下来我通过Measure的流程图说明一下这个结果是怎么来的:





看了上图,有些人可能会看出一些端倪,也可能还不是很清晰,我按照自己的理解总结一下Measure过程究竟想干什么?

1.第一点很清晰,MyPanelParent调用MyPanel.Measure的过程是想得到MyPanel.DesiredSize,MyPanelParent需要在Arrange孩子MyPanel时,参考孩子的DesiredSize,决定将孩子MyPanel安置多大的空间。

2.MyPanel.DesiredSize是包含Margin以及内容的大小空间

3.MyPanel.MeasureOverride传入的参数constrainedSize,是基类的实现刨去Margin的大小,然后按照MyPanel对MinWidth,MaxWidth,Width的设置计算的一个MyPanel想要的值,我们自定义时在MeasureOverride当中不需要关心自己的Margin,以及其他基类上影响Layout的属性,只要考虑在给定参数的范围类安排自己的内容区域;MyPanel.MinWidth,Width,MaxWidth的设定都是针对内容区域的,不含Margin部分

4.如果不设定Width,那么可以在MeasureOverride返回的时候返回一个期望的内容区域大小,它会被MinWidth和MaxWidth再调整一下,调整后,还有待于MyPanelParent的衡量(旁白:别瞎折腾,也别玩Layout系统,都设置MinWidth,MaxWidth,就乖乖的呆在这个范围内。)

5.不论MyPanel怎么设置自己的Width,MinWidth,MaxWidth,以及在MeasureOverride返回一个大小,来表明自己期望多大的空间显示自己的内容,但这些都仅仅是期望的,期望是美好的,现实是残酷的,这一切还必须限定在MyPanel.Measure开始时传入的参数availableSize刨去MyPanel.Margin后的范围内,小于这个范围就满足,大于这个范围就被裁断。(可怜呀,总是受制于父)

6.影响Measure过程的参数和属性存在一个优先级的,大概如下所示:

Measure方法参数availableSize>MinWidth,Width,MaxWidth>MeasureOverride返回值

2.Transform对Measure过程的影响

通过上面的过程,我们已经大概了解了Measure过程的工作方式,以及各个属性是如何影响的。但是还有一个属性我们没有提及,但它对Measure的过程也影响甚大,这就是LayoutTransform。通过下面的两段分析,你会看到这个属性的具体表现。

设置1:

<Windowx:Class="WpfApplication1.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="MainWindow"Height="522"Width="594"Loaded="Window_Loaded"xmlns:my="clr-namespace:WpfApplication1"> <Canvas> <my:MyPanelParentx:Name="myPanelParent1"Height="400"Width="400"Background="Lime"Canvas.Left="10"Canvas.Top="10"> <my:MyPanelMargin="10"x:Name="myPanel1"Background="Red"Width="200"> <my:MyPanel.LayoutTransform> <RotateTransformAngle="90"/> </my:MyPanel.LayoutTransform> </my:MyPanel> <my:MyPanelMargin="10"x:Name="myPanel2"Background="Red"MinWidth="150"MaxWidth="250"/> </my:MyPanelParent> </Canvas> </Window>

publicclassMyPanelParent:Panel { protectedoverrideSystem.Windows.SizeMeasureOverride(System.Windows.SizeavailableSize) { foreach(UIElementiteminthis.InternalChildren) { item.Measure(newSize(1000,800)); } returnavailableSize; } protectedoverrideSystem.Windows.SizeArrangeOverride(System.Windows.SizefinalSize) { doublex=0; foreach(UIElementiteminthis.InternalChildren) { item.Arrange(newRect(x,0,item.DesiredSize.Width,item.DesiredSize.Height)); x+=item.DesiredSize.Width; } returnfinalSize; }


publicclassMyPanel:Panel
{
protectedoverrideSystem.Windows.SizeMeasureOverride(System.Windows.SizeavailableSize)
{
foreach(UIElementiteminthis.InternalChildren)
{
item.Measure(availableSize);
}
returnnewSize(80,50);
}

protectedoverrideSystem.Windows.SizeArrangeOverride(System.Windows.SizefinalSize)
{
doublexCordinate=0;
foreach(UIElementiteminthis.InternalChildren)
{
item.Arrange(newRect(newPoint(xCordinate,0),item.DesiredSize));
xCordinate+=item.DesiredSize.Width;
}
returnfinalSize;
}
}

运行的表现为:





分析一下设置:

MyPanel1.LayoutTransform=newRotateTransform(90)//旋转了90度

MyPanel1.Width=200

MyPanel1.Margin=Thickness(10)

MyPanel1.Measure()传入的参数为1000*800,MyPanel1.MeasureOverride返回的参数为80*50.

分析一下结果:

MyPanel1实际的画出来的大小是50×200,明显是被旋转了90度。

运行起来,你会发现最终的MyPanel1.DesiredSize在Measure过程之后为70×220,也就是说,它是被Transform之后的大小,明显是被旋转过的。另外,观察MyPanel.MeasureOverride传入的参数,为200×980,根据上一节对Measure过程的分析,MeasureOverride传入的参数宽为200是可预知的,因为我们设置了MyPanel1.Width为200,但Height为980,明显是MyPanel.Measure传入的宽1000减去2*10等于980,看来在进入MeasureOverride之前,Layout系统也处理了LayoutTransform对Measure过程的影响,它希望MeasureOverride不要关心自身LayoutTransform的影响。MeasureOverride结束后,返回值为80×50,根据上一节对Measure过程的分析,宽为80被调节为符合自己的设置,为200,由于高没有设置,这个50肯定会保留,因此最后在没有Transform之前的DesiredSize应该是220×70,然而基类会将MeasureOverride返回的大小再进行一次Transform,达到最终的DesiredSize的大小,以便Arrange的时候分配合适的空间来容纳MyPanel的大小。

如果你将上面例子的MyPanel1.LayoutTransform设置成ScaleTransform:

<Windowx:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow"Height="522"Width="594"Loaded="Window_Loaded"xmlns:my="clr-namespace:WpfApplication1">
<Canvas>
<my:MyPanelParentx:Name="myPanelParent1"Height="400"Width="400"Background="Lime"Canvas.Left="10"Canvas.Top="10">
<my:MyPanelMargin="10"x:Name="myPanel1"Background="Red"Width="200">
<my:MyPanel.LayoutTransform>
<ScaleTransformScaleX="2"ScaleY="2"/>
</my:MyPanel.LayoutTransform>
</my:MyPanel>
<my:MyPanelMargin="10"x:Name="myPanel2"Background="Red"MinWidth="150"MaxWidth="250"/>
</my:MyPanelParent>
</Canvas>
</Window>

然后再观察myPanel.MeasureOverride传入的参数,为200×390,首先200是可预知的,因为设置了Width属性,而390是怎么回事呢,其实为Measure传入的1000×800的高800减去Margin为20后得到780,然后根据LayoutTransform将高缩小2倍之后得到的390,因此传入的参数就是200×390,可见,Layout系统,在进入MeasureOverride之前,他希望,MeasureOverride只关心内容怎么布置,而不需要关心基类属性的设置对MeasureOverride的影响。由于MeasureOverride的返回值依然是80×50,可推理,80被调节为200,50被保留,没有Transform之前的值应该是200×50。因为基类还要进行Transform,因此,内容区域的真实的大小应该是400×100,再加上Margin之后,最终的DesiredSize肯定为420*120,你可以尝试调试给出的代码。

3.Measure过程的总结

Measure过程的总结

通过上面的过程分析,我相信你或多或少对WPF的Layout系统的Measure过程有了更进一步的了解,其实还有一些因素影响Measure的过程,比如UseLayoutRounding属性,在进入MeasureOverride之前和之后,基类都被将参数根据DPI进行Rounding,这个过程知道就行了,不需要在自己的MeasureOverride里面关心。我们总结一下哪些属性和参数会影响Measure的过程:MyPanel.Measure传入的参数availableSize,MyPanel的MinWidth,Width,MaxWidth,Margin,UseLayoutRounding,LayoutTransform,MeasureOverride的返回值。

Measure过程相关问题解答

Q1:什么是LayoutSlot?什么时候能获取到?在哪里获取?

LayoutSlot就是调用Arrange方法的时候,传入的参数finalRect,这是父分配给子的容纳Margin以及内容区域的矩形空间;

当Arrange过程结束后,你可以拿到;

通过调用静态类LayoutInformation.GetLayoutSlot(FrameworkElementelement)方法可以拿到。

Q2:什么是LayoutClip?什么时候能获取到?在哪里获取?

LayoutClip只的是当内容区域要绘制的大小,大于LayoutSlot刨去Margin区域后的大小,这时候,内容区域就会被Clip,超出的部分会被Clip掉,而剩下的可显示的部分就是LayoutClip,他是一个Geometry。

Arrange过程结束后,可以拿到;

通过调用静态类LayoutInformation.GetLayoutClip(FrameworkElementelement)方法可以拿到。如果内容区域可以完全显示

在LayoutSlot刨去Margin的区域内,LayoutClip为Null。

Q3:在父的MeasureOverride当中调用孩子的Measure方法时,传入的参数有没有什么限制?

有,确保availableSize.Width和Height不是NaN;但可以是Infinity

Q4:在进入自己的MeasureOverride方法后,面对参数我该咋办?

首先,心里应该明白,传入的参数已经是基类刨去自己的Margin,并且考虑了基类影响Measure过程的属性之后的值。

其次,看自身有没有自定义的,并且影响Layout的属性,根据自己的内容要求,或者孩子的情况,调用孩子的Measure方法,并传入希望孩子限定在多大范围内空间。

最后,返回一个自己期望的Size。

这里应该注意的点:

1.调用孩子的Measure方法时,传入的参数,是你限定孩子的最大空间,用来显示孩子的Margin以及内容区域的,而孩子不管最终期望的大小有多少,都会被你给他的availableSize裁剪。

2.根据自身的策略返回一个期望的值,这个期望的值应该是在自己的MinWidth,Width,MaxWidth限定的范围呢,如果没有,基类还会强行调整。

3.基类调整后的值还会被父传入的availableSize再次调整,返回值不能大于父传入的参数减去Margin之后的值

Q5:MeasureOverride的返回值有没有什么限制?

有,除了如Q5所说,返回值会被重新调节之外,必须保证自己定义的MeasureOverride的返回值是一个确定的值,不是NaN,也不是Infinity。如果小于0时,基类会强制调节为0.

Q6:DesiredSize究竟是什么?

DesiredSize是Measure过程结束后确定的一个大小,他是孩子期望父在Arrange的时候给他分配的大小,包含孩子的Margin区域以及内容区域。如果父在ArrangeOverride的时候,需要调用孩子的Arrange方法时,如果根据策略他希望满足孩子的期望大小,那么,调用孩子的Arrange方法应该传入孩子DesiredSize大小的Rect。

Q7:孩子的DesiredSize确定后,是不是最终就可以得到这么大的空间?

不一定。就像Q7答案所讲,根据父的策略而定,如果父期望分配给孩子期望的大小,就在调用孩子的Arrange方法时,传入DesiredSize大小的Rect,比如Canvas,Canvas的孩子的大小就是孩子的DesiredSize那么大;而如果父是根据自身的设置决定,就不会参考孩子的DesiredSize,传入的当然是自己只能分配给孩子的空间,比如UniformGrid,他根据自身的可用大小,根据行数列数均分空间,然后,均分后的空间分配给每个孩子,而不考虑孩子的DesiredSize。给孩子分配空间,这个过程是在Arrange阶段的。

原文地址:http://www.cnblogs.com/powertoolsteam/archive/2011/01/10/1932036.html
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: