XAML Composition 互操作行为的变化

本文基于 Github Wiki 中的 XAML Composition Interop Behavior Changes 一文以及个人试验写成,试验时所用 SDK 版本为 14393,不排除在后续版本中 XAML-Composition 互操作行为会发生其它变化。


XAML-Composition 互操作行为变化

从 14332 版 SDK 开始,ElementCompositionPreview.GetElementVisual 方法返回的 Visual 由 XAML 和调用者共用了,因而有关 Visual 的一系列属性的取值和赋值行为也就发生了改变。本文旨在解释从 10586(11月更新) 到 14322(周年更新)及以上版本之间 Visual 行为上的变化。这中变化只在你将项目目标平台版本设置为 14322 或更高时才会发生。而面向 10586 版的应用哪怕是运行在安装了周年更新的系统环境上也会保持过去的行为。

有一点需要特别指出,这种变化只会发生在 ElementCompositionPreview.GetElementVisual 方法返回的 Visual 上,而不会发生在开发者自行创建的 Visual 上。

Build 10586 (十一月更新)

在 10586 版 SDK 中, ElementCompositionPreview.GetElementVisual 方法返回的 Visual 仅由调用者控制。通过对应的 Visual 对一个 UIElement 进行的操作纯粹只会对 XAML 施加增量影响。这是因为返回的 Visual (在底层)是 UIElement 的 Visual 作为根 Visual 的子项。下图描绘了这种情况的简化视觉树:

11 月更新中的 XAML 树

这种情况下,XAML 布局控制着其对应的根 Visual,而开发者控制的则是另外的子 Visual。

Build 14332+ (周年更新)

在 14332 版及后续版本中, ElementCompositionPreview.GetElementVisual 方法返回的 Visual与 XAML 布局操作的 Visual 相同。这意味着不同于 11 月更新,现在通过对应的 Visual 对一个 UIElement 元素进行的操作会绝对地改变 XAML 布局。下图描绘了周年更新中这种情况的简化视觉树:

周年更新中的 XAML 树

因为调用者和 XAML 都在操作同一个 Visual,XAML 有可能会覆盖调用者的赋值。以下是 XAML 可能设置的属性:

  • Offset
  • Size
  • Opacity
  • TransformMatrix
  • Clip
  • CompositeMode

XAML 对一个互操作 Visual 属性进行更新的规则如下:

  1. XAML 在布局过程中会覆写互操作 Visual 的属性值。

  2. XAML 不会读回由程序代码直接对互操作 Visual 的属性赋的值。

  3. XAML 只在新值不等于上次赋的旧值时,才会对互操作 Visual 的属性赋值。亦即如果 XAML 一侧的属性值没有发生变化,则 XAML 不会去更改 Visual 一侧的属性值。

  4. XAML 一侧的上次赋值的值默认与互操作 Visual 属性的默认值一致。也就是说如果 XAML 一侧的属性值保持在默认值不变,则 XAML 不会去更新 Visual 一侧的属性值(例如 XAML 布局中的 offset,对应 Visual 中的 Visual.Offset,默认值为 [0,0])。

  5. Visual 一侧的属性值不会覆写到 XAML 一侧。

  6. UI 元素最终呈现的效果取决于最后生效的值(Visual 一侧的取值或 XAML 覆写 Visual 的值)。

XAML 与 Visual 互操作时规则示意图

举例说明这一系列规则的影响结果,假设我们有一个 Canvas 容器控件,其中有一个 Button

例如:

    Visual myButtonVisual = ElementCompositionPreview.GetElementVisual(myButton);
    myButton.Opacity = 1.0;
    myButtonVisual.Opacity = 0.5;
    myButtonVisual.StartAnimation("Opacity", …);

在该例中,XAML 不知道程序代码直接将 Visual 的 Opacity 属性设置为了 0.5,还开始了一个动画,因而根据规则2, XAML 不会对 Visual 做任何事。根据规则3, XAML 始终认为它的上次赋值是默认值 1.0,所以 XAML 不会尝试去修改 Visual 一侧的属性值(即便在后续异步地修改了 Visual 一侧的 Opacity值,XAML 也还是会认为 1.0 保持没变 )。

这同样适用于布局结果,例如:

    Canvas.SetLeft(myButton, 5);
    Canvas.SetTop(myButton, 5);

根据规则1规则3,该例中 XAML 之后将 Visual 一侧的 Offset 属性覆写为:

 new Vector3(5,5,0);

而如果再之后程序代码又直接对 Visual 的 Offset 赋值:

    myButtonVisual.Offset = new Vector3(20,20,0);

则 20,20 这个值则会生效,因为根据规则2 XAML 始终认为布局输出值为 5,5。

然而对于非零布局位置取值,还有一点需要格外注意,XAML 探测的是上一次对 Visual 一侧属性的赋值,而布局位置取值服从于布局舍入 (layout-rounding)1,我们就必须要考虑 DPI 缩放的情况。

以 DPI 缩放比为 1.0 开始举例:

    Canvas.SetLeft(myButton, 5);
    Canvas.SetTop(myButton, 5);

该例中 DPI 缩放比为 1.0, XAML 会做如下舍入计算和赋值操作:

    LayoutRound(5, 1.0) = floor(5 * 1.0 + 0.5) / 1.0 = 5.0
    myButtonVisual.Offset = new Vector3(5.0,5.0,0);

如果程序代码之后修改了 Visual 一侧的属性:

    myButtonVisual.Offset = new Vector3(20,20,0);

则 Visual 一侧的 Offset 属性取值为 20,20。

但如果现在我们假设 DPI 缩放比动态地改编为 2.5 呢?XAML 布局这时再次接入进行如下计算和操作:

    LayoutRound(5, 2.5) = floor(5 * 2.5 + 0.5) / 2.5 = 5.2 
    myButtonVisual.Offset = new Vector3(5.2,5.2,0);

现在,因为 XAML 一侧的新值 (5.2, 5.2, 0) 不等于旧值 (5.0,5.0,0.0) ,根据规则3 XAML 就会覆写 Visual 一侧的 Offset 属性值为 20,20。

XAML 也会对 Size,也就是尺寸大小进行布局舍入,所以对于非零尺寸值,当 DPI 缩放比改变时 Visual 一侧的 Size 属性也会受到类似影响。

即便 DPI 缩放比不变,同样的行为也可能影响 Size。如果程序代码直接对 Visual.Size 属性赋值,但 XAML 元素的 Size 是窗口大小的应变量(也就是 XAML 元素的尺寸跟着窗口尺寸变化而变化),那么根据规则3 Visual 一侧的 Size 尺寸也会被 XAML 在布局过程后覆写。

如果你想要对互操作 Visual 的 OffsetSize 直接进行操作,最安全的方式是在 XAML 中使用一种将元素放置于 0,0 位置或缩放为 0,0 尺寸的布局容器。


  1. “布局舍入”是 WPF 4.0 中开始引入的一个概念,“布局舍入”使 XAML 对布局呈现时存在的任何非整数值进行舍入处理,以便强制将元素对齐像素边界。

Void_CE

继续阅读此作者的更多文章