原文: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. linux下Oracle11g RAC搭建(一)

    linux下Oracle11g RAC搭建(一) 文档说明 作者    深蓝 项目 Visualbox下模拟RAC搭建(双节点)(Redhat5+Oracle11G) 环境 RedHat Enterp ...

  2. Qt 学习: 视图选择 (QItemSelectionModel)

    博主QQ:1356438802 选择是视图中经常使用的一个操作.在列表.树或者表格中,通过鼠标点击能够选中某一项,被选中项会变成高亮或者反色.在 Qt 中,选择也是使用了一种模型.在 model/vi ...

  3. net的微服务架构

    net的微服务架构 眼下,做互联网应用,最火的架构是微服务,最热的研发管理就是DevOps, 没有之一.微服务.DevOps已经被大量应用,它们已经像传说中的那样,可以无所不能.特来电云平台,通过近两 ...

  4. [Django] The admin interface

    Now let's see how to access admin interface. 1. Create a super user which can access admin interface ...

  5. 剑指Offer面试题10(Java版):二进制中的1的个数

    题目:请实现一个函数,输入一个整数.输出该数二进制表示中1的个数. 比如把9表示成二进制是1001,有2位是1.因此假设输入9.该函数输出2. 1.可能引起死循环的解法 这是一道非常主要的考察二进制和 ...

  6. 【矩阵】概念的理解 —— span、基

    span:全部列向量的线性组合构成的集合: span[a1,-,an]={y∈Rm|y=∑k=1nckak}=S 注:ak∈Rm,共 n 个列向量: 集合 S 可以有不同的一组基,但是基中向量的个数是 ...

  7. matplotlib tricks(一)—— 多类别数据的 scatter(cmap)

    cmap 的选择: binary seismic Reds 多类别数据的 scatter(逐点散列),在 matplotlib 中的实现关键在于,color关键字的定义: def plot_scatt ...

  8. 【rlz000】字串找数

    Time Limit: 3 second Memory Limit: 2 MB 问题描述 输入一个字符串,内有数字和非数字字符.如A123X456Y7A,302ATB567BC,统计共有多少个整数, ...

  9. Canvas范围裁切和几何变换

    范围裁切 clipRect() canvas.save(); canvas.clipRect(left, top, right, bottom); canvas.drawBitmap(bitmap, ...

  10. CentOS查看系统信息和资源使用已经升级系统的命令

    1.查看系统版本: 1)cat /etc/redhat-release   2)uname -a 2.查看资源使用: top 3.升级所有包同时也升级软件和系统内核: yum -y update