分享

WPF:为什么需要Measure和Arrange两步?

 昵称11482448 2013-04-28
 

 

返回目录

温习Measure和Arrange过程

首先UIElement.Measure的参数是可用的空间(Size对象),这个空间通常代表着父控件留给你显示的可用空间,接着UIElement内部的MeasureCore会被调用,该方法会去决定调用MeasureOverride的大小参数。具体过程是先判断大小属性值是否被显示设置,如果是的话,直接使用设置的值,这就是为什么如果你强行设置一个控件的Width和Height后,它总会保持设置的大小(当然如果超出了父控件的规划大小,WPF会根据IsClipToBounds属性来决定是否裁剪它)。当然如果没有被显示设置,那么MeasureCore会根据可用大小和当前控件的属性(比如Margin)来决定最终传入MeasureOverride的可用大小参数。

 

接着MeasureOverride被调用,这个可以被子控件改写,返回的只会影响UIElement.DesiredSize属性。

这个DesiredSize是计算后的包括Margin的结果,而且被三个条件所制约(优先级由上到下):

  • Measure中传入的可用大小参数
  • 显示设置的属性大小
  • MeasureOverride返回的大小

 

让我们做一些示例:

<Grid Width="200" Height="200">

    <TextBlock Margin="50">A</TextBlock>

</Grid>

 

显然LayoutSlot大小为200*200。如果我们把TextBlock的Width和Height都设置成300,TextBlock的DesiredSize还会是200*200,显然他被Measure中的可用大小最先制约。

 

如果第一条限制通过的话,那么后续限制会被应用,比如我们把TextBlock的大小设置成10*10,那么此时TextBlock的DesiredSize会成为:110*100。此时DesiredSize是控件显示设置的大小加上Margin大小。10+50*2=110。

 

最后如果TextBlock的大小不被设置,那么DesiredSize就会使MeasureOverride的结果加上Margin,在我这里结果是:107.74*115.96。

 

 

Arrange的过程则是决定UIElement.RenderSize属性,整个过程和Measure类似但有不一样的地方。首先,传入Arrange的参数是LayoutSlot的位置和大小,是Rect对象。接着ArrangeCore做类似MeasureCore的工作去决定传入ArrangeOverride大小参数,这个参数是Size,这个Size代表着最终需要显示的可用大小。

 

ArrangeOverride返回的大小会影响RenderSize,但是注意不同于MeasureOverride那样,ArrangeOverride影响的RenderSize不会受到Arrange方法的参数的限制,也就是说RenderSize只会受到显示设置的属性大小和ArrangeOverride返回值这两个限制的制约。至于MeasureOverride和ArrangeOverride的返回结果都要受到显示设置的属性大小影响,这个也是情理之中的。想象一下,如果你设置了元素的Width和Height但是最终他有可能不会按照规定大小去显示,那将是多么疯狂的一件事啊。

 

 

返回目录

现有控件对Measure和Arrange的使用

为什么要用Measure和Arrange两个过程?我们从现有控件的执行就已找到诸多答案。

 

Canvas是基于坐标的控件容器,因此他不会因为自身的大小而限制子容器的大小。看这样一个代码:

<Canvas Width="30">

    <Button Width="300" Height="50"/>

</Canvas>

 

VS设计器很给力的显示出这个复杂情况:

image

 

Canvas只有30宽度,但是Button则需要300,同时Canvas不应该限制Button大小。那么此时在Measure阶段,Canvas并没有把MeasureOverride中的父控件可用大小传给Button,而是用Double.PositiveInfinity(代表无穷大)作为参数调用Button的Measure方法。然后Button设置好了自己的DesiredSize后,Canvas在Arrange过程中同样没有传入自己ArrangeOverride的参数,而是直接把Button的DesiredSize作为Button的Arrange方法参数,这样满足Button的任何大小要求。

 

类似的控件还有ScrollViewer。

 

还有Canvas的MeasureOverride总是返回0*0。也就是说如果你没有直接设置Canvas的大小的话,他的DesiredSize总会是0*0。因此如果你放在StackPanel中,他不会被显示,因为StackPanel会根据方向的不同只显示控件DesiredSize(另一个方向无限制),显然0*0的DesiredSize会使Canvas不可见。如果你把Canvas放在Grid中,那么Canvas的大小就是Grid所给的大小,因为Grid会向已有大小都交给子成员,如果你把Canvas放在ScrollViewer中,那么Canvas也会无限大,因为ScrollViewer本身就是无限大。

也可以通过改写MeasureOverride来创建一个DesiredSize返回最小值的Canvas,可以参考这篇文章:WPF:一个估量最小大小的Canvas

 

还有TextBlock总会已单行的形式延伸,只有设置了TextWrapping属性后,TextBlock才会根据父控件的可用空间限制来执行换行规划。

 

这些成果都是Measure和Arrange配合的结果。

 

当然绝大多数情况,如果你不是写自定义的Panel控件的话,是不需要太多关心Measure和Arrange(这也使许多人都不是很了解Measure和Arrange)。同时WPF的诸多内置类型都有MeasureOverride和ArrangeOverride的执行,可以参考这篇文章:

WPF中内置类型的MeasureOverride和ArrangeOverride表现

 

比如Control类型的执行都是对第一个Visual Child进行测量。那么我们可以直接创建一个类型继承自Control,然后把一个Visual加入到VisualChildren中,不用改写MeasureOverride和ArrangeOverride,一个显示另一个控件的自定义Control就实现了:

class ContentButton : Control

{

    Button btn;

 

    public ContentButton()

    {

        btn = new Button() { Content = "Mgen" };

        base.AddVisualChild(btn);

    }

 

    protected override Visual GetVisualChild(int index)

    {

        return btn;

    }

 

    protected override int VisualChildrenCount

    {

        get

        {

            return 1;

        }

    }

}

 

把这个自定义控件放在StackPanel中,背后的Button会直接显示出来:

image

 

 

 

返回目录

自定义控件演示Measure和Arrange

下面自定义一个控件演示有趣的Measure和Arrange。

我们创建一个在MeasureOverride中总返回50*50。然后在Arrange中总返回100*100的控件:

class MyControl : Control

{

    protected override void OnRender(DrawingContext drawingContext)

    {

        base.OnRender(drawingContext);

        drawingContext.DrawRectangle(Brushes.Red, null, new Rect(new Point(), RenderSize));

    }

 

    protected override Size ArrangeOverride(Size arrangeBounds)

    {

        base.ArrangeOverride(arrangeBounds);

        return new Size(100, 100);

    }

 

    protected override Size MeasureOverride(Size constraint)

    {

        base.MeasureOverride(constraint);

        return new Size(50, 50);

    }

}

 

在StackPanel中,控件会被显示成100*50的。因为横向StackPanel不做大小限制,所以DesiredSize被应用,而纵向对大小进行限制,RenderSize被应用。

在Grid或者其他不限制大小的容器中,大小会是100*100的,同时控件不会根据可用控件的改变而放大或者缩小,因为ArrangeOverride没有使用arrangeBounds参数,而总是返回100*100的Size。

 

 

还可以创建一个纵向的StackPanel,但是不尝试拉伸子成员的横向宽度,效果如下:

image

 

代码:

class MyPanel : Panel

{

    protected override Size MeasureOverride(Size availableSize)

    {

        var retSize = new Size();

        foreach (UIElement ui in InternalChildren)

        {

            ui.Measure(new Size(availableSize.Width, availableSize.Height));

            retSize.Height += ui.DesiredSize.Height;

            retSize.Width = Math.Max(retSize.Width, ui.DesiredSize.Width);

        }

        return retSize;

    }

 

    protected override Size ArrangeOverride(Size finalSize)

    {

        var next = new Point();

        foreach (UIElement ui in InternalChildren)

        {

            ui.Arrange(new Rect(next, ui.DesiredSize));

            next.Y += ui.RenderSize.Height;

 

        }

        return finalSize;

    } 


    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多