【WPF】WPF 布局系统(测量和排序)

官网链接

概览

 

 

 

 

 

一、布局系统

 负责 WPF 中的布局和定位的子系统称为布局系统。布局系统不仅负责在设计时设计用户界面,还负责在运行时管理元素的呈现。布局系统还管理元素的事件处理。

二、布局槽(Layout Slot)和布局剪辑(Layout Clip)

在 WPF 中,每个元素都是在表示元素边界的矩形内定义的。此矩形称为布局槽。此矩形的实际大小由布局系统在运行时根据屏幕大小、父属性和元素属性(如边框、宽度、高度、边距和填充)进行计算后计算。计算元素的布局属性后,元素的最终可见区域称为布局剪辑

 

 

                                       (布局槽)                                                                      (布局剪辑) 

获取布局槽和布局剪辑:

Geometry clipGeometry = LayoutInformation.GetLayoutClip(textBlock1);  
  
Rect layoutRectangle = LayoutInformation.GetLayoutSlot(textBlock1); textBlock1.Text = "Layout: " + layoutRectangle.ToString();  

 

 

三、WPF 布局原则

WinForm的布局是采用基于坐标的方式,当窗口内容发生变化时,里面的控件不会随之动态调整,用户体验不够好。而WPF采用了基于流的布局方式,像Web开发模式。流式布局特点是:所有的元素总是默认地自动向左上角靠近,在设计时,通过控制元素相对位置的方式使其达到预计的效果,即元素的位置依赖于相邻元素的位置和尺寸。

四、合成布局元素

WPF的合成布局模型是用来满足广泛的应用场景布局,允许某种布局控件被嵌套在其他布局控件中。合成布局模型通过布局契约来实现子控件和父布局控件间的通信问题。布局契约包括两种设计思想,即根据内容调整尺寸和两段布局。
1.根据内容调整尺寸
根据内容调整尺寸,即每个控件都根据内容来确定控件大小,这个设计思想应用于UI中的所有控件。例如,窗口能够调整大小来适应它们内部的控件,文本框控件能调整尺寸来适应它内部的文本。当然每个元素会被询问其期望的尺寸大小,以确保根据内容调整尺寸的设计思想能够实施。

2.两段布局

两段布局是指在两个完全不同的阶段来确定控件的最佳尺寸。在这两个阶段布局模型让父布局控件和子控件达成元素最后尺寸的约定。两个阶段分别是测量(Measure) 和排列(Arrange) 。测量阶段需要做的主要工作是:对整个UI页面的检测,并询问每个元素的期望尺寸(Desired Size) ,元素返回一个可用的尺寸(Avilable Size),当所有的元索都被询问并测量好以后,就进入到排列阶段。在排列阶段,父元素通知每个子元素的实际尺寸(Actual Size)和位置。在两段布局中。父元素和子元素需要协商出需要的尺寸大小,涉及可用尺寸 ,期望尺寸、实际尺寸,在此辨析3个尺寸。其中,可用尺寸是测量阶段的初始约束值,即父元素愿意给子元素的最大空间值;期望尺寸是子元素想要的尺寸;实际尺寸是父元元素分配给子元素的最终尺寸。这3个尺寸要符合下面的不等式条件:

    Desired Size ≤ Actual Size ≤ Available Size

了解WPF合成布局模型,学习WPF布局机制,才能理解合成布局模型的来龙去陆页面布局时做到得心应手。

五、测量(Measure)和排列(arrange)

  我们已经知道,WPF界面上的每个元素的边界框尺寸和排列是WPF自动计算出来的。那么这个智能布局的过程是怎么样呢?两个步骤:measure and arrange. Layout系统要为每个Panel中的每个子元素进行这两个步骤的处理。由于Panel是可以嵌套的,可以推断这是个递归的处理过程。Layout处理的过程如下图所示:
单个元素测量(measure)过程:传入可用大小(父类容器提供),返回期望大小(自身),

 


我们在学习WPF框架的时候已经知道,所有UI元素的根元素是UIElement类型,UIElement中定义了一些基本的关于UI显示的属性(如clip和Visibility)。在UIElement.Measure(Size availableSize)方法执行阶段,就是要对这些基本属性做评估,得到合适的Size。同样,FrameworkElement.MeasureCore(Size availableSize)方法评估在FrameworkElement中定义且有可能影响UI布局的属性,得出更合适的Size。这个Size将被传递给FrameworkElement.MeasureOverride(Size availableSize),WPF提供的Panel类型(如Grid)中就会重写该方法来处理,处理完后将得到一个系统期望的Size(可称之为DesiredSize),Layout系统将按照这个Size来显示该Element。到此测量(Measure)阶段结束。当Size确定以后,把Size包装为Rect实例,传递给UIElement.Arrange(Rect finalRect),开始进行排列(arrange)处理,根据Size参数,Arrange方法将为元素创建边界框(就是第一节说的那个元素边界框),边框信息将会打包到Rect实例传递给FrameworkElement.ArrangeCore(Rect finalRect),ArrangeCore将继续评估DesiredSize,将会计算边界留白(Margin,Pading..)等信息,得到arrangeSize,并传递FrameworkElement.ArrangeOverride(Size finalSize),这个方法又是可重写的,WPF提供的Panel类型(如Grid)中就会重写该方法来处理,最终得到finalSize。得到finalSize后,ArrangeOverride执行完毕,控制权回到ArrangeCore方法,ArrangeCore把该Element放到它的边界框中。到此,该Element的Layout处理完成。

测量和排列子元素

布局系统为 Children 集合的每个成员完成两个过程:一个测量过程和一个排列过程。 每个子 Panel 都提供自己的 MeasureOverrideArrangeOverride 方法来实现自己的特定布局行为。

在测量过程中,会计算 Children 集合的每个成员。 此过程从调用 Measure 方法开始。 需要在父 Panel 元素的实现中调用此方法,而不必为要出现的布局显式调用该方法。

首先,计算 UIElement 的本机大小属性,例如 ClipVisibility。 这将生成一个名为 constraintSize 的值,该值将传递给 MeasureCore

其次,处理在 FrameworkElement 上定义的框架属性,这会影响 constraintSize 的值。 这些属性通常描述基础 UIElement 的大小调整特性,例如其 HeightWidthMarginStyle。 其中每个属性都可以更改显示元素所需的空间。 然后使用 constraintSize 作为参数调用 MeasureOverride

HeightWidth 属性与 ActualHeightActualWidth 属性有所不同。 例如,ActualHeight 属性是基于其他高度输入和布局系统的计算值。 该值是由布局系统本身基于实际呈现的传递设置的,因此可能稍微小于属性(例如作为输入更改基础的 Height)的设置值。

因为 ActualHeight 是计算所得的值,所以你应该知道,由于布局系统各种操作的结果,该值可能有多次或递增的报告的更改。 布局系统可能会计算子元素所需的测量空间、父元素的约束等。

测量过程的最终目标是让子元素确定其 DesiredSize,这发生在 MeasureCore 调用期间。 Measure 存储 DesiredSize 值,供在内容排列过程中使用。

排列过程从调用 Arrange 方法开始。 在排列过程中,父 Panel 元素会生成一个表示子元素边界的矩形。 该值将传递给 ArrangeCore 方法进行处理。

ArrangeCore 方法计算子元素的 DesiredSize,并且计算可能影响元素呈现大小的任何其他边距。 ArrangeCore 生成一个 arrangeSize,后者作为参数传递给 PanelArrangeOverride 方法。 ArrangeOverride 生成子元素的 finalSize。 最后,ArrangeCore 方法执行偏移量属性(如边距和对齐)的最终计算,并将子元素放在其布局槽内。 子元素不需要(并且通常不)填充整个分配空间。 然后将控件返回给父级 Panel,布局过程即告完成。

布局性能注意事项

布局是一个递归过程。 Children 集合中的每个子元素都会在每次调用布局系统期间得到处理。 因此,应避免在不必要时触发布局系统。 以下注意事项有助于实现更好的性能。

  • 应注意哪些属性值更改会强制执行布局系统的递归更新。

    如果依赖属性的值可能导致布局系统被初始化,则会使用公共标志对该依赖属性进行标记。 AffectsMeasureAffectsArrange 提供了有用的线索,说明哪些属性值更改会强制执行布局系统的递归更新。 一般来说,任何可能影响元素边界框大小的属性都应将 AffectsMeasure 标志设置为 True。 有关详细信息,请参阅依赖项属性概述

  • 如果可能,请使用 RenderTransform 而不是 LayoutTransform

    LayoutTransform 是一种影响用户界面 (UI) 内容的非常有用的方式。 但是,如果转换效果不需要影响其他元素的位置,则最好改用 RenderTransform,因为 RenderTransform 不会调用布局系统。 LayoutTransform 会应用其转换,并强制执行递归布局更新以获得受影响元素的新位置。

  • 避免对 UpdateLayout 进行不必要的调用。

    UpdateLayout 方法会强制执行递归布局更新,通常是不必要的。 除非你确定需要进行完整更新,否则请依赖布局系统为你调用此方法。

  • 在处理大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而不是常规的 StackPanel

    通过虚拟化子集合,VirtualizingStackPanel 仅在内存中保留当前位于父级视区内的对象。 因此,在大多数情况下,性能得到显著提高。

 

WPF 中修改哪些依赖属性会导致元素重新测量Measure

一组 RenderTransform,Opacity,Visibility Hidden -> Visible 不会导致元素重新测量。

Margin, Foreground, Background, Collapsed -> Visible 这些都会导致重测。

 

不同尺寸属性的含义

当我开始使用 WPF 时,我经常错误地认为它Control.Width会告诉我屏幕上控件的宽度是多少,这根本不是真的:

  • Width: 可用于建议控件应具有的宽度的属性,可以在 XAML 中设置,默认为double.Nan(即未使用)。
  • Height: 可用于建议控件应具有的高度的属性,可以在 XAML 中设置,默认为double.Nan(即未使用)。
  • DesiredSize:在加边距结束时您的控件请求的大小将MeasureOverride(), 被添加,并且 Width/Height、MinWidth/MinHeight 和 MaxWidth/MaxHeight 被强制执行(如果已定义)。 DesiredSize 被父母用来安排。
  • RenderSize:父级为您提供的渲染控制大小。RenderSize 不同,DesiredSize 因为 a)DesiredSize 有,Margin RenderSize 没有 b) 父级可能决定给子级一个不同于要求的大小。
  • ActualWidth: 实际上是RenderSize.Width
  • ActualHeight: 实际上是RenderSize.Height

 

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

 

posted @ 2022-11-27 05:16  小林野夫  阅读(638)  评论(0编辑  收藏  举报
原文链接:https://www.cnblogs.com/cdaniu/