WINDOWS 10 2015 年特别版
# 图形和动画 - Windows 组合支持 10 倍缩放
作者 [Kenny Kerr](https://msdn.microsoft.com/zh-cn/magazine/mt149362?author=Kenny+Kerr) | Windows 2015
Windows 组合引擎,又称桌面窗口管理器 (DWM),为 Windows 10 提供了新 API。DirectComposition 是组合的主要接口,但是作为经典 COM API,它对于一般水平的应用开发人员而言很大程度上是难以访问的。全新 Windows 组合 API 基于 Windows 运行时 (WinRT) 构建,并且通过将 Direct2D 和 Direct3D 提供的即时模式图形领域与如今动画及效果有很大改进的保留可视化树进行混合,为高性能渲染奠定了基础。
我首次撰写 DWM 文章要追溯到 2006 年,当时 Windows Vista 还在测试中 ([goo.gl/19jCyR](http://goo.gl/19jCyR))。它允许您控制给定窗口模糊效果的范围,并创建与桌面完美混合的自定义镶边。图 1 显示了 Windows 7 中这一实现的高度。可以使用 Direct3D 和 Direct2D 制作硬件加速渲染,从而为您的应用打造绝妙的视觉效果 ([goo.gl/IufcN1](http://goo.gl/IufcN1))。您甚至可以将 GDI 和 USER 控件的旧领域与 DWM 混合 ([goo.gl/9ITISE](http://goo.gl/9ITISE))。然而,任何敏锐的观察者都会说 DWM 可以提供的更多 — 更多。Windows 7 中的 Windows Flip 3D 功能就是有力的佐证。
![](https://box.kancloud.cn/2016-01-08_568f406e112fd.png)
图 1 Windows Aero
Windows 8 为 DWM 引入称为 DirectComposition 的新 API,它的名称正是向激发其设计灵感的经典 COM API 的 DirectX 系列致敬。DirectComposition 开始向开发人员提供比 DWM 能够提供的画面更加清晰的画面。此外,它还提供了改进的术语。DWM 确实是 Windows 组合引擎,能够在 Windows Vista 和 Windows 7 中制作炫丽的效果,因为它从根本上改变了桌面窗口的渲染方式。默认情况下,组合引擎为每个顶层窗口创建了重定向设计面。我在 2014 年 6 月专栏中对此进行了详细介绍 ([goo.gl/oMlVa4](http://goo.gl/oMlVa4))。这些重定向设计面构成了可视化树的一部分,并且 DirectComposition 允许应用利用这一相同技术为高性能图形提供轻量保留模式 API。DirectComposition 提供了可视化树与设计面管理,允许应用将效果与动画的制作任务转移给组合引擎。我在 2014 年 8 月 ([goo.gl/CNwnWR](http://goo.gl/CNwnWR)) 和 9 月专栏 ([goo.gl/y7ZMLL](http://goo.gl/y7ZMLL)) 中介绍了这些功能。我甚至制作了有关使用 DirectComposition for Pluralsight 实现高性能渲染的课程 ([goo.gl/fgg0XN](http://goo.gl/fgg0XN))。
Windows 8 首次推出 DirectComposition,以及对 API 其余 DirectX 系列的一些显著改进,而且,它还引领了一个全新的 Windows API 时代,将永远改变开发人员看待操作系统的方式。Windows 运行时的推出举世瞩目。Microsoft 预示了全新的构建应用和访问操作系统服务的方式,这将最终终结所谓的 Win32 API,Win32 API 一直以来主导着构建应用和与操作系统交互的方式。Windows 8 起步艰难,但是 Windows 8.1 修复了很多问题,并且 Windows 10 如今提供了更为全面的 API,将满足对构建适用于 Windows 的一级应用(甚至是重要的应用)感兴趣的更多开发人员。
Windows 10 于 2015 年 7 月推出,预告了尚未准备制作的新组合 API。其仍会有变动,因此不能在提交至 Windows 应用商店的通用 Universal Windows 应用中使用。这也正是因为现在可进行制作的组合 API 发生了显著的变化,并且是向好的方向转变。此 Windows 10 更新也是有史以来第一次同一组合 API 在所有规格的产品上可用,这也进一步证明了通用 Windows 平台的通用性。无论您面向的是多显示器的桌面平台,还是置于口袋中的小巧智能手机,这一组合均采用相同的工作方式。
当然,对于 Windows 运行时,每个人都喜欢的一个优点就是,它最终兑现了 Windows 通用语言运行时的承诺。如果您更喜欢用 C# 编码,则可以通过内置于 Microsoft .NET Framework 的支持直接使用 Windows 运行时。如果与我一样,您更喜欢使用 C++,则可以使用没有中间媒介或者成本高昂的抽象化的 Windows 运行时。Windows 运行时构建于 COM 而非 .NET,因此非常适于使用 C++。我将使用针对 Windows 运行时、标准 C++ 语言投射的现代 C++ ([moderncpp.com](http://moderncpp.com/)),但是您可以选择自己喜欢的语言,因为不管怎样,API 都是相同的。我也会提供一些 C# 示例来说明 Windows 运行时如何无缝支持不同的语言。
Windows 组合 API 远离其 DirectX 根。DirectComposition 提供了一个设备对象(效仿 Direct3D 和 Direct2D 设备),而新的 Windows 组合 API 则以排序器开始。但是,其目的相同,都是充当组合资源的工厂。除此之外,Windows 组合与 DirectComposition 非常类似。有一个代表窗口及其可视化树之间关系的组合目标。您离视觉对象越近,差异就越明显。DirectComposition 视觉对象具有提供某种位图的内容属性。位图为以下三项之一:组合设计面、DXGI 交换链或另一窗口的重定向设计面。一个典型的 DirectComposition 应用程序由视觉对象和设计面构成,设计面充当不同视觉对象的内容或位图。如图 2所示,组合可视化树稍微有点怪异。新的视觉对象没有内容属性,而是使用组合画笔渲染。这就造就了更灵活的抽象化。虽然画笔只能像以前那样渲染位图,但至少从概念上而言,可以更高效地创建纯色画笔并可定义更详尽的画笔,采用的方式与 Direct2D 提供可视作图像的效果的方式一样。合适的抽象化非常重要。
![](https://box.kancloud.cn/2016-01-08_568f406e3d503.png)
图 2 Windows 组合可视化树
让我们看一些实际示例,来说明这一切的工作原理并让您大致了解一下可能的情况。同样,您可以选择自己喜欢的 WinRT 语言投射。您可以使用现代 C++ 创建排序器,如下所示:
~~~
using namespace Windows::UI::Composition;
Compositor compositor;
~~~
同样,您可以使用 C# 执行相同的操作:
~~~
using Windows.UI.Composition;
Compositor compositor = new Compositor();
~~~
您甚至可以使用 C++/CX 提供的更炫丽的语法:
~~~
using namespace Windows::UI::Composition;
Compositor ^ compositor = ref new Compositor();
~~~
从 API 的角度看,所有这些都是相同的,仅仅在语言投射方面有一些差异。如今,您基本上可以采用两种方式编写通用 Windows 应用。或许最常见的方法是使用操作系统的 Windows.UI.Xaml 命名空间。如果 XAML 对于您的应用没有那么重要,您还可以直接使用基础应用模型(不依赖于 XAML)。我在 2013 年 8 月专栏 ([goo.gl/GI3OKP](http://goo.gl/GI3OKP)) 中介绍了 WinRT 应用模型。如果使用这种方法,您只需以最小程度实现 IFrameworkView 和 IFrameworkViewSource 接口即可直接开始操作了。图 3 在提供了一个使用 C# 的基本大纲,可方便您入手。Windows 组合还提供了与 XAML 的深入集成,但是让我们先看看简单的无 XAML 的应用吧,因为它可以提供一个更简单的场所来了解组合。我将在本文稍后部分再返回到 XAML。
图 3 C# Windows 运行时应用模型
~~~
using Windows.ApplicationModel.Core;
using Windows.UI.Core;
class View : IFrameworkView, IFrameworkViewSource
{
static void Main()
{
CoreApplication.Run(new View());
}
public IFrameworkView CreateView()
{
return this;
}
public void SetWindow(CoreWindow window)
{
// Prepare composition resources here...
}
public void Run()
{
CoreWindow window = CoreWindow.GetForCurrentThread();
window.Activate();
window.Dispatcher.ProcessEvents(CoreProcessEventsOption.ProcessUntilQuit);
}
public void Initialize(CoreApplicationView applicationView) { }
public void Load(string entryPoint) { }
public void Uninitialize() { }
}
~~~
它位于应用的 SetWindow 方法(参见图 3)内,其中应构造排序器。事实上,这是应用生命周期中最早发生这种情况的点,因为排序器依赖窗口的调度程序,而这是窗口和调度程序最终都存在的点。然后,可通过创建组合目标在排序器与应用视图之间建立一种关系:
~~~
CompositionTarget m_target = nullptr;
// ...
m_target = compositor.CreateTargetForCurrentView();
~~~
应用保持组合目标处于活动状态很重要,所以请务必使其成为 IFrameworkView 实现的成员变量。正如我先前所说,组合目标代表窗口或视图与其可视化树之间的关系。您能对组合目标执行的操作就是,设置根视觉对象。通常,这将会是一个容器视觉对象:
~~~
ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root(root);
~~~
在此我使用的是 C++,其缺少属性语言支持,因此根属性被投射为访问器方法。C# 与属性语法的新增部分非常相似:
~~~
ContainerVisual root = compositor.CreateContainerVisual();
m_target.Root = root;
~~~
DirectComposition 只提供了一种视觉对象,它支持各种设计面来表示位图内容。Windows 组合提供了表示各种视觉对象、画笔和动画的小类层次结构,然而只有一种设计面,且只能使用 C++ 创建,因为它属于供 XAML 等框架以及更有经验的应用开发人员使用的 Windows 组合 interop API。
视觉对象类层次结构如图 4 所示。CompositionObject 是由排序器支持的资源。所有组合对象可能已为其属性设置动画。视觉对象提供了大量属性,用于控制视觉对象相对位置、外观、裁剪和渲染选项等许多方面。它包含转换矩阵属性,以及用于缩放和旋转的快捷方式。这的确是一个非常强大的基类。相反,ContainerVisual 是一个相对简单的类,只添加了 Children 属性。虽然您可以直接创建容器视觉对象,但是 SpriteVisual 添加了关联画笔的功能,以便视觉对象可以确实地渲染其自己的像素。
![](https://box.kancloud.cn/2016-01-08_568f406e55e13.png)
图 4 组合视觉对象
给定一个根容器视觉对象,我可以创建任意数量的子视觉对象:
~~~
VisualCollection children = root.Children();
~~~
这些还可以是容器视觉对象,但是他们更可能是子画面视觉对象。我可以使用 C++ 中的 for 循环添加三个视觉对象作为根视觉对象的子项:
~~~
using namespace Windows::Foundation::Numerics;
for (unsigned i = 0; i != 3; ++i)
{
SpriteVisual visual = compositor.CreateSpriteVisual();
visual.Size(Vector2{ 300.0f, 200.0f });
visual.Offset(Vector3{ 50 + 20.0f * i, 50 + 20.0f * i });
children.InsertAtTop(visual);
}
~~~
您可以在图 5 中轻松想象应用窗口,然而该代码不会导致渲染任何内容,因为没有画笔与这些视觉对象关联。画笔类层次结构如图 6 所示。CompositionBrush 只是画笔的一个基类,不提供其自己的任何功能。CompositionColorBrush 是最简单的种类,只提供一个颜色属性用于渲染纯色视觉对象。这可能听起来没什么让人振奋的,但是不要忘了,您可以将动画连接到该颜色属性。CompositionEffectBrush 和 CompositionSurfaceBrush 类相关联,但却是更复杂的画笔,因为它们由其他资源支持。对于任何附加的视觉对象,CompositionSurfaceBrush 都会渲染一个组合设计面。它具有控制位图绘制的各种属性,例如内插、对齐和拉伸,更不用说设计面本身了。CompositionEffectBrush 采用多种设计面画笔制作各种效果。
![](https://box.kancloud.cn/2016-01-08_568f406e652fa.png)
图 5 窗口中的子视觉对象
![](https://box.kancloud.cn/2016-01-08_568f406e81b6e.png)
图 6 组合画笔
创建和应用彩色画笔简单直接。下面是使用 C# 的一个例子:
~~~
using Windows.UI;
CompositionColorBrush brush = compositor.CreateColorBrush();
brush.Color = Color.FromArgb(0xDC, 0x5B, 0x9B, 0xD5);
visual.Brush = brush;
~~~
颜色结构由 Windows.UI 命名空间提供,并显示 alpha、红色、绿色和蓝色作为 8 位颜色值,不同于 DirectComposition 和 Direct2D 对浮点颜色值的偏好。对于视觉对象和画笔,该方法一个比较好的功能是,可以随时更改颜色属性,而且引用相同画笔的所有视觉对象都将自动进行更新。我之前的确暗示过,颜色属性甚至可以制作动画。那么它的工作原理是什么? 接下来我们谈谈动画类。
动画类层次结构如图 7 所示。CompositionAnimation 基类提供了存储命名值以用于表达式的功能。我稍后将详细讨论表达式。KeyFrameAnimation 提供了典型的基于关键帧的动画属性,如持续时间、迭代和停止行为。各种关键帧动画类提供了用于插入关键帧的特定于类型的方法,以及特于定类型的动画属性。例如,ColorKeyFrameAnimation 允许您插入具有颜色值的关键帧以及一个用于控制颜色空间以使其在关键帧之间插入的属性。
![](https://box.kancloud.cn/2016-01-08_568f406e91937.png)
图 7 组合动画
创建动画对象,然后将该动画应用至特定组合对象,这简直太容易了。假设我想为某视觉对象的不透明度制作动画。我可以直接使用 C++ 中的标量值将视觉对象的不透明度设置为 50%,如下所示:
~~~
visual.Opacity(0.5f);
~~~
或者,我可以使用关键帧创建一个标量动画对象,以制作一个从 0.0 至 1.0 的动画变量,代表 0% 到 100%的不透明度:
~~~
ScalarKeyFrameAnimation animation =
compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(0.0f, 0.0f); // Optional
animation.InsertKeyFrame(1.0f, 1.0f);
~~~
InsertKeyFrame 的第一个参数是从动画 (0.0) 开始到动画 (1.0) 结束的相对偏移。第二个参数是动画时间线中该点的动画变量的值。因此,该动画将在动画期间将值从 0.0 顺利转变为 1.0。然后,我可以设置该动画的总体持续时间,如下所示:
~~~
using namespace Windows::Foundation;
animation.Duration(TimeSpan::FromSeconds(1));
~~~
动画准备好后,我只需将其连接至我选择的组合对象和属性:
~~~
visual.StartAnimation(L"Opacity", animation);
~~~
StartAnimation 方法实际上继承自 CompositionObject 基类,表示您可以为各种类的属性制作动画。这也是与 DirectComposition 相背离的一点。在 DirectComposition 中,每个可制作动画的属性为标量值和动画对象提供了过多负载。Windows 组合可提供更加丰富的属性系统,这就引入了一些非常有趣的功能。尤其是,它支持写入文本表达式,减少了为制作更有趣的动画和效果所需编写的代码量。这些表达式在运行时进行分析,由 Windows 组合引擎编译,然后高效执行。
假设您需要沿 Y 轴旋转某视觉对象,让其深度表现出来。视觉对象的 RotationAngle 属性(以弧度测量)是不够的,因为它无法制作出包含角度的转换。随着视觉对象旋转,离人眼最近的边会显得较大,而相反方向的边则会显得较小。图 8 显示了说明这一行为的多个旋转对象。
![](https://box.kancloud.cn/2016-01-08_568f406ea8151.png)
图 8 旋转视觉对象
您如何实现此类动画效果? 那么,我们来看看针对旋转角度的标量关键帧动画:
~~~
ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();
animation.InsertKeyFrame(1.0f, 2.0f * Math::Pi,
compositor.CreateLinearEasingFunction());
animation.Duration(TimeSpan::FromSeconds(2));
animation.IterationBehavior(AnimationIterationBehavior::Forever);
~~~
线性缓动函数替代了默认的加速/减速功能,以制作持续的旋转动作。然后,我需要使用可以从表达式中引用的属性定义自定义对象。排序器针对这一目的提供了一个属性集:
~~~
CompositionPropertySet rotation = compositor.CreatePropertySet();
rotation.InsertScalar(L"Angle", 0.0f);
~~~
一个属性集也是一个组合对象,所以我可以使用 StartAnimation 方法为我的自定义属性制作动画,就像为任意内置属性制作动画那样简单:
~~~
rotation.StartAnimation(L"Angle", animation);
~~~
我现在有一个 Angle 属性在移动中的对象。现在,我需要定义一个转换矩阵来制作想要的效果,同时委派给针对旋转角度本身的此动画属性。输入表达式:
~~~
ExpressionAnimation expression =
compositor.CreateExpressionAnimation(
L"pre * Matrix4x4.CreateFromAxisAngle(axis, rotation.Angle) * post");
~~~
动画表达式不是关键帧动画对象,因此没有动画变量可能会改变的相对关键帧偏移(基于某些内插功能)。表达式反而仅仅是指可能在更传统的意义上为自身制作动画的参数。当然,由我来定义什么是“前”、“轴”、“旋转”和“后”。我们来看看轴参数:
~~~
expression.SetVector3Parameter(L"axis", Vector3{ 0.0f, 1.0f, 0.0f });
~~~
表达式中的 CreateFromAxisAngle 方法预计旋转某轴并因此围绕 Y 轴定义该轴。它还预计旋转角度,为此我们可以遵从旋转属性集及其动画“Angle”属性:
~~~
expression.SetReferenceParameter(L"rotation", rotation);
~~~
要确保旋转发生在视觉对象中央而不是左边,我需要预先将 CreateFromAxisAngle 创建的旋转矩阵乘以逻辑上将轴移动到旋转点的转换:
~~~
expression.SetMatrix4x4Parameter(
L"pre", Matrix4x4::Translation(-width / 2.0f, -height / 2.0f, 0.0f));
~~~
切记,矩阵相乘不是交替的,所以前矩阵和后矩阵确实就是那样。最后,在旋转矩阵之后,我可以添加一些角度,然后将视觉对象还原至其原始位置:
~~~
expression.SetMatrix4x4Parameter(
L"post", Matrix4x4::PerspectiveProjection(width * 2.0f) *
Matrix4x4::Translation(width / 2.0f, height / 2.0f, 0.0f));
~~~
这满足表达式提及到的所有参数,我现在只需使用表达式动画即可通过 TransformMatrix 属性来为视觉对象制作动画:
~~~
visual.StartAnimation(L"TransformMatrix", expression);
~~~
我已经探讨了创建、填充视觉对象并为其制作动画的各种方式,那么我如果要直接渲染视觉对象的话,该怎么办? DirectComposition 同时提供了预先分配的设计面和稀疏分配的位图(叫做虚拟设计面,按需分配并可调整大小)。Windows 组合似乎没有提供创建设计面的功能。有一个 CompositionDrawingSurface 类,但如果没有外界协助,则无法创建。答案来自 Windows 组合 interop API。如果您只有组件的 Windows 元数据,则 WinRT 类可能会实现无法直接看到的其他 COM 接口。根据对这些掩蔽的接口的了解,您可以在 C++ 中轻松查询它们。当然,这会导致要做的工作更多一点,因为您脱离了 Windows 组合 API 提供给主流开发人员的清洁抽象。我要做的第一件事情是,创建一个渲染设备,并且我将使用 Direct3D 11,因为 Windows 组合尚不支持 Direct3D 12:
~~~
ComPtr<ID3D11Device> direct3dDevice;
~~~
然后,我将准备设备创建标记:
~~~
unsigned flags = D3D11_CREATE_DEVICE_BGRA_SUPPORT |
D3D11_CREATE_DEVICE_SINGLETHREADED;
#ifdef _DEBUG
flags |= D3D11_CREATE_DEVICE_DEBUG;
#endif
~~~
BGRA 支持允许我使用更易上手的 Direct2D API 来通过此设备进行渲染,然后 D3D11CreateDevice 函数自行创建硬件设备:
~~~
check(D3D11CreateDevice(nullptr, // Adapter
D3D_DRIVER_TYPE_HARDWARE,
nullptr, // Module
flags,
nullptr, 0, // Highest available feature level
D3D11_SDK_VERSION,
set(direct3dDevice),
nullptr, // Actual feature level
nullptr)); // Device context
~~~
之后,我需要查询设备的 DXGI 接口,因为这是我创建 Direct2D 设备所需要的:
~~~
ComPtr<IDXGIDevice3> dxgiDevice = direct3dDevice.As<IDXGIDevice3>();
~~~
现在可以创建 Direct2D 设备了:
~~~
ComPtr<ID2D1Device> direct2dDevice;
~~~
此时,我将再次为增加的诊断启用调试层:
~~~
D2D1_CREATION_PROPERTIES properties = {};
#ifdef _DEBUG
properties.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION;
#endif
~~~
我可以先创建一个 Direct2D 工厂来创建该设备。如果我需要创建任何独立于设备的资源,这将会非常有用。在这里我将只使用 D2D1CreateDevice 函数提供的快捷方式:
~~~
check(D2D1CreateDevice(get(dxgiDevice), properties, set(direct2dDevice)));
~~~
渲染设备准备好了。我有了可以根据自身需要随意进行渲染的 Direct2D 设备。现在,我需要将此渲染设备告诉 Windows 组合引擎。这正是那些掩蔽的接口进入的地方。对于我自始至终都在使用的排序器,我可以查询 ICompositorInterop 接口:
~~~
namespace abi = ABI::Windows::UI::Composition;
ComPtr<abi::ICompositorInterop> compositorInterop;
check(compositor->QueryInterface(set(compositorInterop)));
~~~
ICompositorInterop 提供了从 DXGI 设计面创建组合设计面的方法(如果您想要在组合可视化树中包含现有交换链,这绝对方便),但是它还提供了一些更有趣的别的方法。其 CreateGraphicsDevice 方法将创建一个给出渲染设备的 CompositionGraphicsDevice 对象。CompositionGraphicsDevice 类是 Windows 组合 API 中的一个常规类,而不是掩蔽的接口,但是它未提供构造函数,因此您需要使用 C++ 和 ICompositorInterop 接口来创建它:
~~~
CompositionGraphicsDevice device = nullptr;
check(compositorInterop->CreateGraphicsDevice(get(direct2dDevice), set(device)));
~~~
由于 CompositionGraphicsDevice 是 WinRT 类型,因此我可以再次使用现代 C++,而非借助指针以及手动错误处理。而且是 CompositionGraphicsDevice 最终允许我创建组合设计面的:
~~~
using namespace Windows::Graphics::DirectX;
CompositionDrawingSurface surface =
compositionDevice.CreateDrawingSurface(Size{ 100, 100 },
DirectXPixelFormat::B8G8R8A8UIntNormalized,
CompositionAlphaMode::Premultiplied);
~~~
在此,我将创建一个大小为 100 x 100 像素的设计面。注意,这表示实际像素,而非其余 Windows 组合假设并提供的逻辑和 DPI 感知坐标。该设计面还提供了 Direct2D 支持的 32 位 alpha 混合渲染。当然,Direct3D 和 Direct2D 尚未通过 Windows 运行时提供,因此又回到掩蔽的接口来实际绘制到此设计面:
~~~
ComPtr<abi::ICompositionDrawingSurfaceInterop> surfaceInterop;
check(surface->QueryInterface(set(surfaceInterop)));
~~~
Windows 组合很像在它之前的 DirectComposition,它在 ICompositionDrawingSurfaceInterop 接口上提供 BeginDraw 和 EndDraw 方法,这些方法将纳入典型调用并将典型方法替代为通过相同名称执行的 Direct2D 方法调用:
~~~
ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
check(surfaceInterop->BeginDraw(nullptr, // Update rect
__uuidof(dc),
reinterpret_cast<void **>(set(dc)),
&offset));
~~~
Windows 组合获取在创建组合设备时提供的原始渲染设备,并使用它创建设备上下文或渲染目标。我可以选择以物理像素提供一个剪切矩形,但是在此我只选择对渲染设计面进行不受限访问。BeginDraw 也会再次返回一个以物理像素计算的偏移,以表明预期的绘图表面的由来。这并不一定是渲染目标的左上角,必须要小心地调整或转换任何绘图命令使他们正确地接纳此偏移。同样,不要在渲染目标上调用 BeginDraw,因为 Windows 组合已经为您执行该操作。该渲染目标逻辑上由组合 API 拥有,必须小心谨慎,不要在调用 EndDraw 后按住它。渲染目标现已准备好,但不了解视图的逻辑或有效 DPI。我可以使用 Windows::Graphics::Display 命名空间获取当前视图的逻辑 DPI 并设置 Direct2D 将用于进行渲染的 DPI:
~~~
using namespace Windows::Graphics::Display;
DisplayInformation display = DisplayInformation::GetForCurrentView();
float const dpi = display.LogicalDpi();
dc->SetDpi(dpi, dpi);
~~~
开始渲染之前的最后一步是,通过某种方法处理组合偏移。一个简单的解决办法就是使用偏移制作转换矩阵。不过记住,Direct2D 以逻辑像素计算,所以我不仅需要使用偏移,还需要使用新确立的 DPI 值:
~~~
dc->SetTransform(D2D1::Matrix3x2F::Translation(offset.x * 96.0f / dpi,
offset.y * 96.0f / dpi));
~~~
此时,在设计面的 interop 接口上调用 EndDraw 方法之前,您可以尽情地绘制,以确保所有成批的 Direct2D 绘制命令得到处理,且对设计面所做的更改也反映在组合可视化树中:
~~~
check(surfaceInterop->EndDraw());
~~~
当然,我尚未将设计面与视觉对象关联,并且正如我所提到的,视觉对象不再提供内容属性,且必须使用画笔渲染。庆幸的是,排序器将创建一个画笔来表示预先存在的设计面:
~~~
CompositionSurfaceBrush brush = compositor.CreateSurfaceBrush(surface);
~~~
然后,我可以创建一个常规的子画面画笔,并使用此画笔让视觉对象发光:
~~~
SpriteVisual visual = compositor.CreateSpriteVisual();
visual.Brush(brush);
visual.Size(Vector2{ ... });
~~~
如果这样的互操作性对于您来说不够,您甚至可以采用 XAML 元素并检索基础组合视觉对象。下面是使用 C# 的一个例子:
~~~
using Windows.UI.Xaml.Hosting;
Visual visual = ElementCompositionPreview.GetElementVisual(button);
~~~
尽管 ElementCompositionPreview 看起来像是临时的状态,而事实上它已做好制作准备,可被已提交到 Windows 应用商店的应用使用。对于任意 UI 元素,静态 GetElementVisual 方法将从基础组合可视化树中返回视觉对象。注意,它返回 Visual,而非 ContainerVisual 或 SpriteVisual,因此您不能直接使用视觉对象子项或应用画笔,但是您可以调整 Windows 组合提供的许多视觉对象属性。ElementCompositionPreview 帮助程序类提供了一些其他的静态方法,用于以控制的方式添加子视觉对象。您可以更改视觉对象的偏移,以及类似于将继续在 XAML 级别运行的 UI 点击测试这样的功能。您甚至可以直接使用 Windows 组合应用动画,而不用破坏构建于其上的 XAML 基础架构。让我们创建一个简单的标量动画来旋转按钮。我需要从视觉对象中检索排序器,然后像以前一样创建一个动画对象:
~~~
Compositor compositor = visual.Compositor;
ScalarKeyFrameAnimation animation = compositor.CreateScalarKeyFrameAnimation();
~~~
让我们构建一个简单的动画来始终使用线性缓动函数缓慢旋转按钮:
~~~
animation.InsertKeyFrame(1.0f, (float) (2 * Math.PI),
compositor.CreateLinearEasingFunction());
~~~
然后,我可以指明一次旋转花费 3 秒并一直继续:
~~~
animation.Duration = TimeSpan.FromSeconds(3);
animation.IterationBehavior = AnimationIterationBehavior.Forever;
~~~
最后,我可以简单地将动画连接到 XAML 提供的视觉对象,指示组合引擎对其 RotationAngle 属性制作动画:
~~~
visual.StartAnimation("RotationAngle", animation);
~~~
尽管您可能只用 XAML 就能实现这一目标,但 Windows 组合引擎提供了更强大的功能与灵活性,因为它驻留在更低的抽象级别,无疑会提供更出色的性能。再举个例子,Windows 组合提供了 XAML 目前不支持的四元数动画。
关于 Windows 组合引擎还有很多话题可以谈。依我个人浅见,这是迄今最具突破性的 WinRT API。可供您任意使用的功能无比强大,而且,与很多其他大型 UI 和图形 API 不同,它不会影响性能,甚至不会占用您太多学习时间。在许多方面,Windows 组合都展现出 Windows 平台的美妙与震撼。
您可以在 Twitter [@WinComposition](https://twitter.com/@WinComposition) 找到 Windows Composition 团队。
* * *
Kenny Kerr *是加拿大的计算机程序员,是 Pluralsight 的作者,也是一名 Microsoft MVP。他的博客网址是 [kennykerr.ca](http://kennykerr.ca/),您可以通过 Twitter [@kennykerr](https://twitter.com/@kennykerr) 关注他。*
- 介绍
- Microsoft .NET - .NET 和通用 Windows 平台开发
- 图形和动画 - Windows 组合支持 10 倍缩放
- 应用生命周期 - 通过后台任务和扩展执行使应用处于活动状态
- 通知 - Windows 10 中的自适应和交互式通知
- 应用集成 - 在 Windows 10 上链接和集成应用
- Visual Studio 工具 - NuGet 功能增强了 Windows 10 的开发功能
- UI 设计 - 通用 Windows 应用的响应式设计
- UI 设计 - 适用于 Windows 10 的自适应应用
- 数字墨迹 - Windows 10 中的墨迹交互
- 游戏开发 - 使用 Unity 为通用 Windows 平台编写游戏
- 结束语 - 欢迎使用 Windows 10 应用开发
- 编者寄语 - 从 3.0 开始的发展之路