WPF Layout 系统概述——Measure

原文: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"

        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: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"

        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: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"

        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: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阶段的。

原文地址:https://www.cnblogs.com/lonelyxmas/p/9091830.html

时间: 2024-11-05 22:34:05

WPF Layout 系统概述——Measure的相关文章

WPF Layout 系统概述——Arrange

原文:WPF Layout 系统概述--Arrange Arrange过程概述 普通基类属性对Arrange过程的影响 我们知道Measure过程是在确定DesiredSize的大小,以便Arrange过程参考这个DesiredSize,确定给MyPanel分配多少空间,但是DesiredSize只是作为参考,在有些用例下,MyPanelParent在调用MyPanel.Arrange的时候,会根据父的实际策略指定MyPanel.Arrange方法的参数,而不是直接指定MyPanel.Desir

Understanding the WPF Layout System

Many people don't understand how the WPF layout system works, or how that knowledge can help them in their projects. I intend to shine a little light on the mechanics behind all those cool layout controls in WPF. To explain the system, I will give a

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

概述 上一篇博文对DecorView和ViewRootImpl的关系进行了剖析,这篇文章主要是来剖析View绘制的三个基本流程:measure,layout,draw,只有把这三个基本流程搞清楚了,平时在自定义View的时候才会有清晰的思路!开始进入正题. View的measure过程 三个流程均是从ViewRootImpl的performTraversals方法开始的,如下所示: private void performTraversals() { ...... int childWidthM

2000条你应知的WPF小姿势 基础篇&lt;69-73 WPF Freeze机制和Template&gt;

在正文开始之前需要介绍一个人:Sean Sexton. 来自明尼苏达双城的软件工程师.最为出色的是他维护了两个博客:2,000ThingsYou Should Know About C# 和 2,000 Things You Should Know About WPF .他以类似微博式的150字简短语言来每天更新一条WPF和C#重要又容易被遗忘的知识.很希望能够分享给大家. 本系列我不仅会翻译他的每一个tip,也会加入自己开发之中的看法和见解.本系列我希望自己也能和他一样坚持下来,每天的进步才能

安卓坐标

1 说来说去都不如 画图示意 简单易懂啊!!!真是的! 来吧~~先上张图~~! 2 3 4 (一)首先明确一下 Android 中的坐标系统 : 5 屏幕的左上角是坐标系统原点(0,0) 6 原点向右延伸是X轴正方向,原点向下延伸是Y轴正方向 7 8 (二)关于Scroll: 屏幕显示的内容很多时,会有超出一屏的情况,于是就产生了Scroll的概念. 9 10 在View类中有个方法: 11 getScrollY() 英文原文描述是: 12 Return the scrolled top pos

Android应用:横竖屏切换总结

眨眼间,已经到了2016你年春节前,离上一篇博客的时间已经有6个月多,回想起这半年的种种,不得不说,学习和工作实在是太忙了,或许这就是程序员的真实写照吧. 写博客之初,主要的目的还是为了把自己的学习痕迹记录下来,写的东西比较基础,也不多,算是一种督促,希望能坚持地学习下去,因为学识不存在暴发户,靠的是积累.如果对自己过去半年的学习给个评价,我还是比较满意的,前期定下来的目标都基本都达到了.单凭这个,我就觉得今年的新年会是个好年. 说完过去,那么接着就是将来.因为现在的工作环境上外网不大方便,而且

Android横竖屏切换重载问题与小结

(转自:http://www.cnblogs.com/franksunny/p/3714442.html) (老样子,图片啥的详细文档,可以下载后观看 http://files.cnblogs.com/franksunny/635350788930000000.pdf) Android手机或平板都会存在横竖屏切换的功能,通常是由物理重力感应触发的,但是有时候也不尽然,通常在设置里面我们可以对手机的横竖屏切换进行关闭,操作界面如下 只需要点击下“屏幕旋转”按钮就可以关闭横竖屏切换了. 一.禁止AP

Android View 简析

基于android 4.4上源码分析: setContentView流程: getwindow() ->setContentView() -> installDecor() -> addView() getWindow()返回的是PhoneWindow installDecor()生成了window的rootView decorView addView() 会导致 decorView -> requestLayout() -> getViewRootImpl() ->

Android客户端性能优化(魅族资深工程师毫无保留奉献)

本文由魅族科技有限公司资深Android开发工程师degao(嵌入式企鹅圈原创团队成员)撰写,是degao在嵌入式企鹅圈发表的第一篇原创文章,毫无保留地总结分享其在领导魅族多个项目开发中的Android客户端性能优化经验,极具实践价值! 即日起,嵌入式企鹅圈将在之前五个专栏(Linux内核驱动情景分析.资源紧缺型SOC嵌入式架构设计.嵌入式交叉工具链及其应用.嵌入式设计和编程.微信硬件平台和物联网解决方案)新增Android开发专栏!更多Android.Linux.嵌入式和物联网原创技术分享敬请