zoukankan      html  css  js  c++  java
  • WPF/Silverlight Layout 系统概述——Measure

    前言

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

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

    Layout Process Overview

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

    子过程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的表现为:

    Measure_Window_Appearance

    分析一下设置:

    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 Flow1

    看了上图,有些人可能会看出一些端倪,也可能还不是很清晰,我按照自己的理解总结一下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;
            }
        }
    

    运行的表现为:

    Measure_Window_Transform

    分析一下设置:

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

    我们在进行WPF/Silverlight开发时,还可以借助一些工具来助力开发过程。ComponentOne Studio Enterprise 是一款专注于企业应用的.NET全功能控件套包,支持WinForms、WPF、UWP、Xamarin、ASP.NET MVC等多个平台,帮助在缩减成本的同时,提前交付丰富的桌面、Web和移动企业应用。



    本文是由葡萄城技术开发团队发布,转载请注明出处:葡萄城官网


  • 相关阅读:
    一行代码更改博客园皮肤
    fatal: refusing to merge unrelated histories
    使用 netcat 传输大文件
    linux 命令后台运行
    .net core 使用 Nlog 配置文件
    .net core 使用 Nlog 集成 exceptionless 配置文件
    Mysql不同字符串格式的连表查询
    Mongodb between 时间范围
    VS Code 使用 Debugger for Chrome 调试vue
    css权重说明
  • 原文地址:https://www.cnblogs.com/powertoolsteam/p/1932036.html
Copyright © 2011-2022 走看看