借助 C++ 进行 Windows 开发

Windows 运行时的呈现

Kenny Kerr

我的上一个专栏中讨论了 Windows 运行时 (WinRT) 应用程序模型 (msdn.microsoft.com/magazine/dn342867)。我演示了如何通过标准 C++ 和经典 COM 来编写 Windows 应用商店或 Windows Phone 应用程序,其中仅使用了一些 WinRT API 函数。毫无疑问,您不必使用 C++/CX 或 C# 这样的语言投射。能够绕过这些抽象概念是一种强大的功能,同时也是一种了解这项技术工作方式的很好的方法。

我在 2013 年 5 月的专栏中介绍了 Direct2D 1.1 并演示了如何使用它在桌面应用程序中进行呈现 (msdn.microsoft.com/magazine/dn198239)。接下来的专栏介绍了 dx.codeplex.com 上提供的 dx.h 库,这可以大幅简化 C++ 中的 DirectX 编程 (msdn.microsoft.com/magazine/dn201741)。

上个专栏中的代码对于实现基于 CoreWindow 的应用程序已经足够,但未提供任何呈现。

本月,我将演示如何利用这种基本的框架并添加呈现支持。WinRT 应用程序模型针对使用 DirectX 呈现进行了优化。我将向您演示,如何利用在之前专栏中学到的有关 Direct2D 和 Direct3D 呈现的内容,将其应用到基于 CoreWindow 的 WinRT 应用程序,具体而言,通过 dx.h 库使用 Direct2D 1.1。大多数情况下,不论您的目标是桌面还是 Windows 运行时,需要编写的实际 Direct2D 和 Direct3D 绘制命令是相同的。但是,其中有一些细微的差别,当然,使其完全运转起来从一开始就有很大差别。因此,我将继续上一次的内容,演示如何在屏幕上显示一些像素!

为了正确支持呈现,窗口必须能够意识到特定事件。至少这包括窗口的可见性和大小的更改,以及对用户所选择的逻辑显示 DPI 配置的更改。在上次专栏中介绍的 Activated 事件中,这些新事件都通过 COM 接口回调报告给应用程序。ICoreWindow 接口提供注册 VisibilityChanged 和 SizeChanged 事件的方法,但首先我需要实现相应的处理程序。我需要实现的两个 COM 接口与 Activated 事件处理程序及其 Microsoft 接口定义语言 (MIDL) 生成的类模板非常相似:

 

typedef ITypedEventHandler<CoreWindow *, VisibilityChangedEventArgs *>
  IVisibilityChangedEventHandler;
typedef ITypedEventHandler<CoreWindow *, WindowSizeChangedEventArgs *>
  IWindowSizeChangedEventHandler;

接下来必须实现的 COM 接口称为 IDisplayPropertiesEventHandler,谢天谢地这个接口已经定义了。 我只需将相关的头文件包括在其中:

#include <Windows.Graphics.Display.h>

此外,相关类型在以下命名空间中定义:

using namespace ABI::Windows::Graphics::Display;

根据这些定义,我可以更新上次专栏中介绍的 SampleWindow 类,也从这三个接口继承:

struct SampleWindow :
  ...
  IVisibilityChangedEventHandler,
  IWindowSizeChangedEventHandler,
  IDisplayPropertiesEventHandler

同时还需要记住更新我的 QueryInterface 实现以指示对这些接口的支持。 这些内容将让您自行完成。 当然,如我上次所说,Windows 运行时并不关心在哪里实现这些 COM 接口回调。 它遵循的原则是,Windows 运行时不假定我的应用程序 IFrameworkView(SampleWindow 类实现的主要接口)也实现这些回调接口。 因此,虽然 QueryInterface 确实会正确处理这些接口的查询,不过 Windows 运行时不会为它们进行查询。 相反,我需要注册相应事件,而最佳位置是在 IFrameworkView Load 方法的实现中。 提醒一下,Load 方法是应该将所有代码粘贴到这里的方法,以便准备应用程序进行初始呈现。 接下来在 Load 方法中注册 VisibilityChanged 和 SizeChanged 事件:

EventRegistrationToken token;
HR(m_window->add_VisibilityChanged(this, &token));
HR(m_window->add_SizeChanged(this, &token));

这会明确告诉 Windows 运行时在哪里查找前两个接口实现。 第三个也是最后一个接口,它针对 LogicalDpiChanged 事件,但此事件注册由 IDisplayPropertiesStatics 接口提供。 此静态接口由 WinRT DisplayProperties 类实现。 我只需使用 GetActivationFactory 函数模板来获取它(在我最近的专栏中可以找到 GetActivationFactory 的实现):

ComPtr<IDisplayPropertiesStatics> m_displayProperties;
m_displayProperties = GetActivationFactory<IDisplayPropertiesStatics>(
  RuntimeClass_Windows_Graphics_Display_DisplayProperties);

成员变量保留此接口指针,在窗口的生命周期中,我需要在不同点上调用它。 现在,我可以在 Load 方法中注册 LogicalDpiChanged 事件:

HR(m_displayProperties->add_LogicalDpiChanged(this, &token));

稍后将返回到这三个接口的实现。 现在该是准备 DirectX 基础结构的时候了。 我将需要标准的设备资源处理程序集,这些在以前的专栏中已经多次讨论过:

void CreateDeviceIndependentResources() {}
void CreateDeviceSizeResources() {}
void CreateDeviceResources() {}
void ReleaseDeviceResources() {}

在第一个方法中,我可以创建或加载任何并非特定于底层 Direct3D 呈现设备的资源。 接下来两个用于创建特定于设备的资源。 最好是将特定于窗口大小的资源与并非特定于窗口大小的资源分隔开。 最后,必须释放所有设备资源。 剩余的 DirectX 基础结构根据应用程序的特定需求,依赖于应用程序来正确实现这四个方法。 它在应用程序中为我提供单独的点来管理呈现资源以及这些资源的有效创建和回收。

现在我可以引入 dx.h 来处理所有的 DirectX 繁重任务:

#include "dx.h"

每个 Direct2D 应用程序都以 Direct2D 工厂开始:

Factory1 m_factory;

您可以在 Direct2D 命名空间中找到此项,通常我采用以下方法包含它:

using namespace KennyKerr;
using namespace KennyKerr::Direct2D;

dx.h 库为 Direct2D、Direct­Write、Direct3D 和 Microsoft DirectX 图形基础结构 (DXGI) 等提供了独立的命名空间。 我的大部分应用程序会频繁使用 Direct2D,因此这对我而言是颇有意义。 当然,您可以采用任何对您的应用程序有意义的方法来管理命名空间。

m_factory 成员变量表示 Direct2D 1.1 工厂。 它用于创建呈现目标,并根据需要创建其他多种与设备无关的资源。 我将创建 Direct2D 工厂,然后可以在 Load 方法的最后一步中创建与设备无关的任意资源:

m_factory = CreateFactory();
CreateDeviceIndependentResources();

Load 方法返回后,WinRT CoreApplication 类立即调用 IFrameworkView Run 方法。

在我的上个专栏中,SampleWindow Run 方法的实现通过在 CoreWindow 调度程序上调用 ProcessEvents 方法即可阻止。 如果应用程序只需要基于各种事件执行不频繁的呈现,采用这种方法阻止便已足够。 可能您要实现一个游戏,或者您的应用程序只需要一些高分辨率的动画。 另一种极端情况是使用连续的动画循环,不过您可能希望更为智能化一点。 我将实现一些折中处理这两种情况的内容。 首先,我添加一个成员变量以便跟踪窗口是否可见。 这可以在窗口实际上对用户不可见时限制呈现:

bool m_visible;
SampleWindow() : m_visible(true) {}

接下来,我可以重写 Run 方法,如图 1 中所示。

图 1:动态呈现循环

auto __stdcall Run() -> HRESULT override
{
  ComPtr<ICoreDispatcher> dispatcher;
  HR(m_window->get_Dispatcher(dispatcher.GetAddressOf()));
  while (true)
  {
    if (m_visible)
    {
      Render();
      HR(dispatcher->
        ProcessEvents(CoreProcessEventsOption_ProcessAllIfPresent));
    }
    else
    {
      HR(dispatcher->
        ProcessEvents(CoreProcessEventsOption_ProcessOneAndAllPending));
    }
  }
  return S_OK;
}

与之前一样,Run 方法接收 CoreWindow 调度程序。 然后,它进入无限循环,连续呈现和处理队列中可能存在的任何窗口消息(Windows 运行时称之为“事件”)。 但是,如果窗口不可见,则将阻止,直至有消息到达。 应用程序如何得知窗口可见性的变化? 这正是使用 IVisibilityChangedEventHandler 接口的原因。 现在,我可以实现其 Invoke 方法以更新 m_visible 成员变量:

auto __stdcall Invoke(ICoreWindow *,
  IVisibilityChangedEventArgs * args) -> HRESULT override
{
  unsigned char visible;
  HR(args->get_Visible(&visible));
  m_visible = 0 != visible;
  return S_OK;
}

MIDL 生成的接口使用 unsigned char 作为可移植的布尔数据类型。 我只需使用提供的 IVisibilityChangedEventArgs 接口指针获取窗口当前的可见性,然后相应地更新成员变量。 在窗口隐藏或显示时将引发此事件,这比为桌面应用程序实现这此事件略微简单,因为在桌面上需要考虑多种情形,包括应用程序关闭和电源管理,更不用说切换窗口。

接下来,我需要实现通过 Run 方法调用的 Render 方法,如图 1 中所示。 在此时按需创建呈现堆栈并且实际执行绘制命令。 图 2 中显示了基本框架。

图 2 Render 方法摘要

void Render()
{
  if (!m_target)
  {
    // Prepare render target ...
  }
  m_target.BeginDraw();
  Draw();
  m_target.EndDraw();
  auto const hr = m_swapChain.Present();
  if (S_OK != hr && DXGI_STATUS_OCCLUDED != hr)
  {
    ReleaseDevice();
  }
}

Render 方法应该比较眼熟。 它的基本表单与之前在 Direct2D 1.1 中概述的相同。 开始时根据需要创建呈现目标。 后面紧跟的是实际绘制命令,位于对 BeginDraw 和 EndDraw 的调用之间。 由于呈现目标是 Direct2D 设备上下文,实际获取呈现在屏幕上的像素涉及到呈现交换链。 说到这一点,我需要添加呈现 Direct2D 1.1 设备上下文的 dx.h 类型以及交换链的 DirectX 11.1 版本。 后者在 Dxgi 命名空间中提供:

DeviceContext m_target;
Dxgi::SwapChain1 m_swapChain;

最后,在呈现失败时,Render 方法将调用 ReleaseDevice:

void ReleaseDevice()
{
  m_target.Reset();
  m_swapChain.Reset();
  ReleaseDeviceResources();
}

这负责释放呈现目标和交换链。 它还调用 ReleaseDeviceResources 以允许释放任何特定于设备的资源,例如画笔、位图或效果。 此 ReleaseDevice 方法看上去可能无关紧要,但在 DirectX 应用程序中对于可靠处理设备丢失非常重要。 如果不能正确释放所有设备资源(任何由 GPU 支持的资源),则应用程序将无法从设备丢失中恢复,并且会崩溃。

接下来,我需要准备呈现目标,这是我在图 2 所示的 Render 方法中没有涉及的一点。 首先是创建 Direct3D 设备(dx.h 库确实也简化了接下来的几个步骤):

auto device = Direct3D::CreateDevice();

在使用 Direct3D 设备时,我可以转到 Direct2D 工厂以创建 Direct2D 设备和 Direct2D 设备上下文:

m_target = m_factory.CreateDevice(device).CreateDeviceContext();

接下来,我需要创建窗口的交换链。 我将首先从 Direct3D 设备中检索 DXGI 工厂:

auto dxgi = device.GetDxgiFactory();

然后,可以为应用程序的 CoreWindow 创建一个交换链:

m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());

这里再次强调,dx.h 库可以自动为我填充 DXGI_SWAP_CHAIN_DESC1 结构,大幅简化了工作。 然后,我将调用 CreateDeviceSwapChainBitmap 方法以创建 Direct2D 位图,该位图将呈现交换链的后台缓冲区:

void CreateDeviceSwapChainBitmap()
{
  BitmapProperties1 props(BitmapOptions::Target | BitmapOptions::CannotDraw,
    PixelFormat(Dxgi::Format::B8G8R8A8_UNORM, AlphaMode::Ignore));
  auto bitmap =
    m_target.CreateBitmapFromDxgiSurface(m_swapChain, props);
  m_target.SetTarget(bitmap);
}

此方法首先需要以 Direct2D 可以理解的方法描述交换链的后台缓冲区。 BitmapProperties1 是 Direct2D D2D1_BITMAP_PROPERTIES1 结构的 dx.h 版本。 BitmapOptions::Target 常量指示位图将用作设备上下文的目标。 Bitmap­Options::CannotDraw 常量关系到一个实际情况:交换链的后台缓冲区只能用作其他绘制操作的输出,不能用作输入。 PixelFormat 是 Direct2D D2D1_PIXEL_FORMAT 结构的 dx.h 版本。

定义位图属性之后,CreateBitmapFromDxgiSurface 方法将检索交换链的后台缓冲区,并创建 Direct2D 位图来代表它。 采用这种方法,只需通过 SetTarget 定位位图,Direct2D 设备上下文就可以直接呈现到交换链中。

回到 Render 方法,我只需告知 Direct2D 如何根据用户的 DPI 配置来缩放任意绘制命令:

float dpi;
HR(m_displayProperties->get_LogicalDpi(&dpi));
m_target.SetDpi(dpi);

然后,我将调用应用程序的设备资源处理程序,根据需要创建任意资源。 作为总结,图 3 提供了 Render 方法的完整设备初始化序列。

图 3 准备呈现目标

void Render()
{
  if (!m_target)
  {
    auto device = Direct3D::CreateDevice();
    m_target = m_factory.CreateDevice(device).CreateDeviceContext();
    auto dxgi = device.GetDxgiFactory();
    m_swapChain = dxgi.CreateSwapChainForCoreWindow(device, m_window.Get());
    CreateDeviceSwapChainBitmap();
    float dpi;
    HR(m_displayProperties->get_LogicalDpi(&dpi));
    m_target.SetDpi(dpi);
    CreateDeviceResources();
    CreateDeviceSizeResources();
  }
  // Drawing and presentation ... see Figure 2

虽然 DPI 缩放在 Direct2D 设备上下文创建之后立即正确应用,在用户更改了此设置时也需要进行更新。 可以为运行的应用程序更改 DPI 缩放的功能是 Windows 8 中的新增功能。 这正是 IDisplayPropertiesEventHandler 接口的作用。 现在,我只需实现其 Invoke 方法并相应地更新设备。 下面是 LogicalDpiChanged 事件处理程序:

auto __stdcall Invoke(IInspectable *) -> HRESULT override
{
  if (m_target)
  {
    float dpi;
    HR(m_displayProperties->get_LogicalDpi(&dpi));
    m_target.SetDpi(dpi);
    CreateDeviceSizeResources();
    Render();
  }
  return S_OK;
}

假定目标(设备上下文)已创建,它将检索当前逻辑 DPI 值并简单地将其转发到 Direct2D。 然后调用应用程序,在重新呈现之前重新创建任何特定于设备大小的资源。 采用这种方法,我的应用程序可以动态地响应显示设备 DPI 配置的变化。 窗口必须动态处理的最后一种更改是对窗口大小的更改。 我已经完成事件注册,因此只需添加 IWindowSizeChangedEventHandler Invoke 方法的实现来表示 SizeChanged 事件处理程序:

auto __stdcall Invoke(ICoreWindow *,
  IWindowSizeChangedEventArgs *) -> HRESULT override
{
  if (m_target)
  {
    ResizeSwapChainBitmap();
    Render();
  }
  return S_OK;
}

唯一剩下的任务就是通过 ResizeSwapChainBitmap 方法调整交换链位图的大小。 再次强调,这是需要谨慎处理的内容。 调整交换链缓冲区的大小,只有在正确进行时,才会是有效的操作。 首先,要使此操作成功,我需要确保已经释放了对这些缓冲区的所有引用。 这些可以是应用程序直接或间接持有的引用。 在本例中,引用由 Direct2D 设备上下文持有。 目标图像是我创建用于包装交换链的后台缓冲区的 Direct2D 位图。 释放此项相当简单:

m_target.SetTarget();

接下来可以调用交换链的 ResizeBuffers 方法以执行所有繁重的任务,然后根据需要调用应用程序的设备资源处理程序。 图 4 显示了如何一起完成这些任务。

图 4 交换链大小调整

void ResizeSwapChainBitmap()
{
  m_target.SetTarget();
  if (S_OK == m_swapChain.ResizeBuffers())
  {
    CreateDeviceSwapChainBitmap();
    CreateDeviceSizeResources();
  }
  else
  {
    ReleaseDevice();
  }
}

现在,您可以添加一些绘制命令,这些命令将由 DirectX 高效地呈现给 CoreWindow 的目标。 举一个简单例子,您可能希望在 CreateDeviceResources 处理程序中创建一个纯色画笔,并将其分配到成员变量,如下所示:

SolidColorBrush m_brush;
m_brush = m_target.CreateSolidColorBrush(Color(1.0f, 0.0f, 0.0f));

在窗口的 Draw 方法中,我首先使用白色来清除窗口的背景:

m_target.Clear(Color(1.0f, 1.0f, 1.0f));

然后,可以使用画笔绘制简单的红色矩形,如下所示:

RectF rect (100.0f, 100.0f, 200.0f, 200.0f);
m_target.DrawRectangle(rect, m_brush);

为了确保应用程序可以从设备丢失中正常恢复,我必须确保应用程序在正确时间释放画笔:

void ReleaseDeviceResources()
{
  m_brush.Reset();
}

这就是使用 DirectX 呈现基于 CoreWindow 的应用程序所要采取的步骤。 当然,将这些内容与我在 2013 年 5 月的专栏相比,您会惊喜地发现,得益于 dx.h 库,这些工作相比与 DirectX 相关的代码编写已经简单了许多。 不过实际上仍有大量的样板代码,主要与实现 COM 接口相关。 在此处可加入 C++/CX,来简化应用程序中使用的 WinRT API。 它隐藏了一部分样板 COM 代码,我在上两期专栏中已经演示过。

Kenny Kerr 是加拿大的一名计算机程序员,他是 Pluralsight 的作者,也是 Microsoft MVP。他的博客网址是 kennykerr.ca,您可以通过 Twitter twitter.com/kennykerr 关注他。