原文: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"
Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">
    <Canvas>
        <my:MyPanelParent x:Name="myPanelParent1"
Height="400" Width="400" Background="Green" Canvas.Left="10" Canvas.Top="10">
            <my:MyPanel Margin="10"
x:Name="myPanel1" Background="Red" MinWidth="150" Width="200"  MaxWidth="250"/>
            <my:MyPanel Margin="10"
x:Name="myPanel2" Background="Red" MinWidth="150" Width="200" MaxWidth="250"/>
        </my:MyPanelParent>
    </Canvas>
</Window>
public class MyPanelParent:Panel
    {
        protected override System.Windows.Size
MeasureOverride(System.Windows.Size availableSize)
        {
            foreach (UIElement
item
in this.InternalChildren)
            {
                item.Measure(new Size(120,
120));
//这里是入口
            }
 
            return availableSize;
        }
 
        protected override System.Windows.Size
ArrangeOverride(System.Windows.Size finalSize)
        {
            double x
= 0;
            foreach (UIElement
item
in this.InternalChildren)
            {
                item.Arrange(new Rect(x,
0, item.DesiredSize.Width, item.DesiredSize.Height));
                x
+= item.DesiredSize.Width;
            }
 
            return finalSize;
        }
    }
 
    public class MyPanel
: Panel
    {
        protected override System.Windows.Size
MeasureOverride(System.Windows.Size availableSize)
        {
            foreach (UIElement
item
in this.InternalChildren)
            {
                item.Measure(availableSize);
            }
            return new Size(50,
50);
//MyPanel
返回它期望的大小
        }
 
        protected override System.Windows.Size
ArrangeOverride(System.Windows.Size finalSize)
        {
            double xCordinate
= 0;
            foreach (UIElement
item
in this.InternalChildren)
            {
                item.Arrange(new Rect(new Point(xCordinate,
0), item.DesiredSize));
                xCordinate
+= item.DesiredSize.Width;
            }
            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"
Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">
    <Canvas>
        <my:MyPanelParent x:Name="myPanelParent1"
Height="400" Width="400" Background="Lime" Canvas.Left="10" Canvas.Top="10">
            <my:MyPanel Margin="10"
x:Name="myPanel1" Background="Red" Width="200">
                <my:MyPanel.LayoutTransform>
                    <RotateTransform Angle="90"/>
                </my:MyPanel.LayoutTransform>
            </my:MyPanel>
            <my:MyPanel Margin="10"
x:Name="myPanel2" Background="Red" MinWidth="150" MaxWidth="250"/>
        </my:MyPanelParent>
    </Canvas>
</Window>
public class MyPanelParent:Panel
{
    protected override System.Windows.Size
MeasureOverride(System.Windows.Size availableSize)
    {
        foreach (UIElement
item
in this.InternalChildren)
        {
            item.Measure(new Size(1000,
800));
        }
 
        return availableSize;
    }
 
    protected override System.Windows.Size
ArrangeOverride(System.Windows.Size finalSize)
    {
        double x
= 0;
        foreach (UIElement
item
in this.InternalChildren)
        {
            item.Arrange(new Rect(x,
0, item.DesiredSize.Width, item.DesiredSize.Height));
            x
+= item.DesiredSize.Width;
        }
 
        return finalSize;
    }
  
public class MyPanel
: Panel
{
    protected override System.Windows.Size
MeasureOverride(System.Windows.Size availableSize)
    {
        foreach (UIElement
item
in this.InternalChildren)
        {
            item.Measure(availableSize);
        }
        return new Size(80,
50);
    }
 
    protected override System.Windows.Size
ArrangeOverride(System.Windows.Size finalSize)
    {
        double xCordinate
= 0;
        foreach (UIElement
item
in this.InternalChildren)
        {
            item.Arrange(new Rect(new Point(xCordinate,
0), item.DesiredSize));
            xCordinate
+= item.DesiredSize.Width;
        }
        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"
Height="522" Width="594" Loaded="Window_Loaded" xmlns:my="clr-namespace:WpfApplication1">
    <Canvas>
        <my:MyPanelParent x:Name="myPanelParent1"
Height="400" Width="400" Background="Lime" Canvas.Left="10" Canvas.Top="10">
            <my:MyPanel Margin="10"
x:Name="myPanel1" Background="Red" Width="200">
                <my:MyPanel.LayoutTransform>
                    <ScaleTransform ScaleX="2"
ScaleY="2"/>
                </my:MyPanel.LayoutTransform>
            </my:MyPanel>
            <my:MyPanel Margin="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:什么是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的更多相关文章

  1. WPF Layout 系统概述——Arrange

    原文:WPF Layout 系统概述--Arrange Arrange过程概述 普通基类属性对Arrange过程的影响 我们知道Measure过程是在确定DesiredSize的大小,以便Arrang ...

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

    前言 在WPF/Silverlight当中,如果已经存在的Element无法满足你特殊的需求,你可能想自定义Element,那么就有可能会面临重写MeasureOverride和ArrangeOver ...

  3. WPF Layout 系统概述 MeasureOverride和ArrangeOverride

    说的非常的好:多参考!!! https://blog.csdn.net/nncrystal/article/details/47416339 https://www.cnblogs.com/dingl ...

  4. WPF/Silverlight Layout 系统概述——Arrange(转)

    Arrange过程概述 普通基类属性对Arrange过程的影响 我们知道Measure过程是在确定DesiredSize的大小,以便Arrange过程参考这个DesiredSize,确定给MyPane ...

  5. Understanding the WPF Layout System

    Many people don't understand how the WPF layout system works, or how that knowledge can help them in ...

  6. 理解Android的layout和measure

    在Android UI开发中,总会有情况需要自定义View和View Group. 什么是View?就是Android中一个基本视图单位,一个Button是一个view, 一个Layout, 也是一个 ...

  7. Android自己定义view之measure、layout、draw三大流程

    自己定义view之measure.layout.draw三大流程 一个view要显示出来.须要经过測量.布局和绘制这三个过程,本章就这三个流程具体探讨一下.View的三大流程具体分析起来比較复杂,本文 ...

  8. Android应用层View绘制流程之measure,layout,draw三步曲

    概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw.仅仅有把这三个基本流程搞清楚了, ...

  9. 2000条你应知的WPF小姿势 基础篇<69-73 WPF Freeze机制和Template>

    在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000ThingsYou Should Know About C# 和 2,00 ...

随机推荐

  1. Windows环境搭建Web自己主动化測试框架Watir(基于Ruby)

    web自己主动化測试一直是一个比較迫切的问题 图1-1 须要安装的工具 http://railsinstaller.org/ 由于安装Ruby还须要用到其它的一些开发工具集.所以建议从站点http:/ ...

  2. [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  ...

  3. [Flow] Declare types for application

    In Flow, you can make global declarion about types. Run: flow init It will generate .flowconfig file ...

  4. Python 库的使用 —— dis

    dis:Disassembler of Python byte code into mnemonics. Java.Python.Ruby 1.9 这些语言均使用了栈机器型的 VM.因为是基于栈的实现 ...

  5. C#基础readonly 与const

    readonly 与 const readonly是运行时常量,const是编译期常量(在编译过程中已经把使用该值的都用值替代,不分配内存)readonly灵活性高,const效率高 readonly ...

  6. 善用Linux与Windows中的筛选功能及其他有用功能

    cmd中的检索目录结构是用 tree命令,检索本目录中这一级别的所有文件是dir,要是文件很多时需要用到检索功能 dir | find "abc" #####主要find之后要加双 ...

  7. javaScript实现简单网页倒计时代码

    <div id="button"> <input type="button" value="同意" id="b0 ...

  8. 建立一个OTP应用

    http://www.javaeye.com/topic/374167 以下是在erlang项目开发中的一些记录,即包含很多通俗易懂的原则,也包含一些似是而非的建议,比较混乱,还没有积累到一个可以分门 ...

  9. 利用SendMessage实现窗口拖动

    原文:利用SendMessage实现窗口拖动 利用SendMessage实现窗口拖动                                            周银辉 想想以前用跟踪鼠标位 ...

  10. lucene 统计单词次数(词频tf)并进行排序

    public class WordCount { static Directory directory; // 创建分词器 static Analyzer analyzer = new IKAnaly ...