WPF Layout 系统概述——Measure
前言
在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过程的影响
请看下面的一些设置:
< Window x:Class="WpfApplication1.MainWindow" Title="MainWindow" < Canvas > < my:MyPanelParent x:Name="myPanelParent1" < my:MyPanel Margin="10" < my:MyPanel Margin="10" </ my:MyPanelParent > </ Canvas > </ Window > |
public class MyPanelParent:Panel { protected override System.Windows.Size { foreach (UIElement in this .InternalChildren) { item.Measure( new Size(120, //这里是入口 } return availableSize; } protected override System.Windows.Size { double x foreach (UIElement in this .InternalChildren) { item.Arrange( new Rect(x, x } return finalSize; } } public class MyPanel { protected override System.Windows.Size { foreach (UIElement in this .InternalChildren) { item.Measure(availableSize); } return new Size(50, //MyPanel } protected override System.Windows.Size { double xCordinate foreach (UIElement in this .InternalChildren) { item.Arrange( new Rect( new Point(xCordinate, xCordinate } return finalSize; } } |
在上面的设置之后,应用程序运行起来之后,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:
< Window x:Class="WpfApplication1.MainWindow" Title="MainWindow" < Canvas > < my:MyPanelParent x:Name="myPanelParent1" < my:MyPanel Margin="10" < my:MyPanel.LayoutTransform > < RotateTransform Angle="90"/> </ my:MyPanel.LayoutTransform > </ my:MyPanel > < my:MyPanel Margin="10" </ my:MyPanelParent > </ Canvas > </ Window > |
public class MyPanelParent:Panel { protected override System.Windows.Size { foreach (UIElement in this .InternalChildren) { item.Measure( new Size(1000, } return availableSize; } protected override System.Windows.Size { double x foreach (UIElement in this .InternalChildren) { item.Arrange( new Rect(x, x } return finalSize; } |
|
public class MyPanel { protected override System.Windows.Size { foreach (UIElement in this .InternalChildren) { item.Measure(availableSize); } return new Size(80, } protected override System.Windows.Size { double xCordinate foreach (UIElement in this .InternalChildren) { item.Arrange( new Rect( new Point(xCordinate, xCordinate } return finalSize; } } |
运行的表现为:
分析一下设置:
MyPanel1.LayoutTransform = new RotateTransform(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:
< Window x:Class="WpfApplication1.MainWindow" Title="MainWindow" < Canvas > < my:MyPanelParent x:Name="myPanelParent1" < my:MyPanel Margin="10" < my:MyPanel.LayoutTransform > < ScaleTransform ScaleX="2" </ my:MyPanel.LayoutTransform > </ my:MyPanel > < my:MyPanel Margin="10" </ 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:什么是Layout Slot? 什么时候能获取到?在哪里获取?
Layout Slot就是调用Arrange方法的时候,传入的参数finalRect,这是父分配给子的容纳Margin以及内容区域的矩形空间;
当Arrange过程结束后,你可以拿到;
通过调用静态类LayoutInformation.GetLayoutSlot(FrameworkElement element)方法可以拿到。
Q2:什么是Layout Clip?什么时候能获取到?在哪里获取?
Layout Clip 只的是当内容区域要绘制的大小,大于LayoutSlot刨去Margin区域后的大小,这时候,内容区域就会被Clip,超出的部分会被Clip掉,而剩下的可显示的部分就是Layout Clip,他是一个Geometry。
Arrange过程结束后,可以拿到;
通过调用静态类LayoutInformation.GetLayoutClip(FrameworkElement element)方法可以拿到。如果内容区域可以完全显示
在Layout Slot刨去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阶段的。
WPF Layout 系统概述——Measure的更多相关文章
- WPF Layout 系统概述——Arrange
原文:WPF Layout 系统概述--Arrange Arrange过程概述 普通基类属性对Arrange过程的影响 我们知道Measure过程是在确定DesiredSize的大小,以便Arrang ...
- WPF/Silverlight Layout 系统概述——Measure(转)
前言 在WPF/Silverlight当中,如果已经存在的Element无法满足你特殊的需求,你可能想自定义Element,那么就有可能会面临重写MeasureOverride和ArrangeOver ...
- WPF Layout 系统概述 MeasureOverride和ArrangeOverride
说的非常的好:多参考!!! https://blog.csdn.net/nncrystal/article/details/47416339 https://www.cnblogs.com/dingl ...
- WPF/Silverlight Layout 系统概述——Arrange(转)
Arrange过程概述 普通基类属性对Arrange过程的影响 我们知道Measure过程是在确定DesiredSize的大小,以便Arrange过程参考这个DesiredSize,确定给MyPane ...
- Understanding the WPF Layout System
Many people don't understand how the WPF layout system works, or how that knowledge can help them in ...
- 理解Android的layout和measure
在Android UI开发中,总会有情况需要自定义View和View Group. 什么是View?就是Android中一个基本视图单位,一个Button是一个view, 一个Layout, 也是一个 ...
- Android自己定义view之measure、layout、draw三大流程
自己定义view之measure.layout.draw三大流程 一个view要显示出来.须要经过測量.布局和绘制这三个过程,本章就这三个流程具体探讨一下.View的三大流程具体分析起来比較复杂,本文 ...
- Android应用层View绘制流程之measure,layout,draw三步曲
概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw.仅仅有把这三个基本流程搞清楚了, ...
- 2000条你应知的WPF小姿势 基础篇<69-73 WPF Freeze机制和Template>
在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000ThingsYou Should Know About C# 和 2,00 ...
随机推荐
- Windows环境搭建Web自己主动化測试框架Watir(基于Ruby)
web自己主动化測试一直是一个比較迫切的问题 图1-1 须要安装的工具 http://railsinstaller.org/ 由于安装Ruby还须要用到其它的一些开发工具集.所以建议从站点http:/ ...
- [Grid Layout] Use the repeat function to efficiently write grid-template values
We can use the repeat() function if we have repeating specifications for columns and rows. With the ...
- [Flow] Declare types for application
In Flow, you can make global declarion about types. Run: flow init It will generate .flowconfig file ...
- Python 库的使用 —— dis
dis:Disassembler of Python byte code into mnemonics. Java.Python.Ruby 1.9 这些语言均使用了栈机器型的 VM.因为是基于栈的实现 ...
- C#基础readonly 与const
readonly 与 const readonly是运行时常量,const是编译期常量(在编译过程中已经把使用该值的都用值替代,不分配内存)readonly灵活性高,const效率高 readonly ...
- 善用Linux与Windows中的筛选功能及其他有用功能
cmd中的检索目录结构是用 tree命令,检索本目录中这一级别的所有文件是dir,要是文件很多时需要用到检索功能 dir | find "abc" #####主要find之后要加双 ...
- javaScript实现简单网页倒计时代码
<div id="button"> <input type="button" value="同意" id="b0 ...
- 建立一个OTP应用
http://www.javaeye.com/topic/374167 以下是在erlang项目开发中的一些记录,即包含很多通俗易懂的原则,也包含一些似是而非的建议,比较混乱,还没有积累到一个可以分门 ...
- 利用SendMessage实现窗口拖动
原文:利用SendMessage实现窗口拖动 利用SendMessage实现窗口拖动 周银辉 想想以前用跟踪鼠标位 ...
- lucene 统计单词次数(词频tf)并进行排序
public class WordCount { static Directory directory; // 创建分词器 static Analyzer analyzer = new IKAnaly ...