如何正确实现MeasureOverride和ArrangeOverride?
因为我认为这是您要问的实际问题,所以我会尽力为您提供我对这个主题的了解。
在我们开始之前,您可能想先阅读 MS Docs 上的 Measuring and Arranging Children。它让您大致了解布局过程是如何工作的,尽管它并没有真正提供有关您应该如何实际实施 MeasureOverride 和 ArrangeOverride 的任何信息。
注意:为简洁起见,从现在开始,每当我说“控制”时,我的真正意思是“派生自 FrameworkElement 的任何类”。
1。影响控件布局的组件有哪些?
请务必注意,有许多参数会影响控件的大小和排列:
- 内容(即子控件)
- 显式宽度和高度
- 边距
- 水平和垂直对齐
- 布局变换
- 布局舍入
- 我可能忽略了其他一些事情
幸运的是,我们在实现自定义布局时唯一需要担心的组件是子控件。这是因为其他组件对所有控件都是通用的,并且完全由框架在MeasureOverride 和ArrangeOverride 之外处理。完全在外部是指输入和输出都经过调整以考虑这些组件。
事实上,如果您检查FrameworkElement API,您会注意到测量过程分为MeasureCore 和MeasureOverride,前者负责所有需要的更正,实际上您从不调用它们直接在子控件上-您调用Measure(Size),它可以发挥所有作用。 ArrangeCore 和 ArrangeOverride 也是如此。
2。如何实现MeasureOverride?
布局传递中测量阶段的目的是向父控件提供有关我们控件想要的大小的反馈。您可能会将其视为一个假设性问题:
鉴于有这么多可用空间,您需要容纳所有内容的最小空间是多少?
不用说,这(通常)是确定父控件大小所必需的 - 毕竟,我们(通常)测量子控件以确定我们控件的大小,不是吗?
输入
来自文档:
此元素可以赋予子元素的可用大小。无限可以
被指定为一个值以指示元素将调整为任何内容
可用。
availableSize 参数告诉我们有多少空间可供使用。请注意,这可能是一个任意值(包括无限的宽度和/或高度),并且您不应该期望在排列阶段获得完全相同的空间量。毕竟,父控件可能会以任何参数多次调用我们控件上的Measure(Size),然后在排列阶段完全忽略它。
如前所述,此参数已预先修正。例如:
- 如果父控件调用
Measure(100x100),并且我们的控件将边距设置为20(在每一侧),则availableSize的值将是60x60。
- 如果父控件调用
Measure(100x100),并且我们的控件将宽度设置为200,则availableSize 的值将是200x100(希望随着您继续阅读,原因会变得清晰)。
输出
来自文档:
此元素在布局期间根据其计算确定其所需的大小
子元素的大小。
生成的所需大小应该是容纳所有内容所需的最小大小。此值必须具有有限的宽度和高度。它通常在任一维度上都小于 availableSize,但不是必须的。
该值影响DesiredSize属性的值,并影响后续ArrangeOverride调用的finalSize参数值。
同样,返回的值随后会被调整,所以我们在确定这个值的时候,除了子控件外,不要关注任何东西。
与DesiredSize 属性值的关系
MeasureOverride 返回的大小会影响,但不一定会成为DesiredSize 的值。这里的关键是该属性并不是真正打算由控件本身使用,而是一种将所需大小传达给父控件的方式。注意Measure 不返回任何值——父控件需要访问DesiredSize 才能知道调用的结果。正因为如此,它的价值实际上是为家长控制而定制的。特别是,保证不超过作为Measure的参数传递的原始大小,无论孩子的MeasureOverride的结果如何。
你可能会问“为什么我们需要这个属性?难道我们不能简单地让Measure 返回大小吗?”。我认为这样做是出于优化原因:
- 通常我们需要在
ArrangeOverride 中访问孩子所需的大小,因此再次调用Measure(Size) 会触发子控件(及其后代)的冗余度量传递。
- 可以在不使度量无效的情况下使排列无效,这会触发布局传递跳过度量阶段并直接进入排列阶段。例如,如果我们对
StackPanel 中的控件重新排序,则子控件的总大小不会改变,只会改变它们的排列方式。
总结
从我们控制的角度来看,这是测量阶段的样子:
- 父控件在控件上调用
Measure(Size)。
-
MeasureCore 预先更正提供的尺寸以考虑边距等。
-
MeasureOverride 以调整后的 availableSize 调用。
- 我们执行自定义逻辑来确定所需的控件大小。
- 缓存生成的所需大小。后面用来调整
ArrangeOverride的finalSize参数。稍后会详细介绍。
- 返回的所需大小被剪裁不超过
availableSize。
- 剪裁后的所需尺寸经过后校正以考虑边距等(步骤 2. 已恢复)。
- 步骤 7 中的值设置为
DesiredSize 的值。
这个值可能会再次被剪裁以不超过作为Measure(Size) 参数传递的原始大小,但我认为这应该已经通过第 6 步得到保证。
3。如何实现ArrangeOverride?
布局过程中排列阶段的目的是相对于控件本身定位所有子控件。
输入
来自文档:
这个元素应该用来排列自己的父元素中的最后一个区域
和它的孩子。
finalSize 参数告诉我们需要多少空间来安排子控件。我们应该将其视为最终约束(因此得名),并且不要违反它。
它的值受父控件作为参数传递给Arrange(Rect) 的矩形大小的影响,而且如前所述,还受MeasureOverride 返回的所需大小的影响。具体来说,它是两个维度中的最大值,规则是该大小保证不小于所需大小(让我再次强调这与从 @987654384 返回的值有关@ 而不是 DesiredSize 的值)。参考this comment。
鉴于此,如果我们使用与测量相同的逻辑,我们不需要任何额外的预防措施来确保我们不会违反约束。
您可能想知道为什么DesiredSize 和finalSize 之间存在这种差异。嗯,这就是裁剪机制的好处。考虑一下 - 如果裁剪被禁用(例如Canvas),除非它们被正确排列,否则框架将如何呈现“溢出”的内容?
说实话,我不确定如果你违反了约束会发生什么。就我个人而言,如果您报告了所需的尺寸但无法适应它,我会认为这是一个错误。
输出
来自文档:
实际使用的尺寸。
这是我无知的边界,知识结束,猜测开始。
我不太确定这个值如何影响整个布局(和渲染)过程。我知道这会影响 RenderSize 属性的值 - 它成为初始值,稍后会对其进行修改以考虑裁剪、舍入等。但我不知道它可能有什么实际影响。
我个人对此的看法是,我们有机会在MeasureOverride 中挑剔;现在是时候将我们的言辞付诸行动了。如果我们被告知在给定大小内排列内容,那正是我们应该做的——在finalSize 内排列子控件,而不是更少,而不是更多。我们不必用子控件紧紧地覆盖整个区域,并且可能存在差距,但这些差距已被考虑在内,并且是我们控制的一部分。
话虽如此,我的建议是简单地返回finalSize,就好像对父控件说“这就是你指示我成为的人,所以我就是这样”。这种方法在股票 WPF 控件中似乎是臭名昭著的实践,例如:
4。结语
我想这就是我所知道的关于这个主题的全部内容,或者至少是我能想到的全部内容。我向你打赌甜甜圈还有更多,但我相信这应该足以让你继续前进,并使你能够创建一些非平凡的布局逻辑。
免责声明
提供的信息只是我对 WPF 布局过程的理解,不保证正确。它结合了多年来积累的经验,一些人在WPF .NET Core source code 周围闲逛,并以一种古老的“把意大利面扔到墙上,看看什么能坚持”的方式来玩代码。