借助 C++ 进行 Windows 开发

使用 Windows 组合引擎

Kenny Kerr

Kenny KerrWindows 组合引擎表示,每个 DirectX 应用程序不再需要有自己的交换链,进入甚至连最基本的结构都不需要构建的阶段。当然,您可以继续使用演示的交换链编写 Direct3D 和 Direct2D 应用程序,但您不必再如此操作。通过允许应用程序直接创建组合面,组合引擎可以让我们更接近本质 - GPU。

组合引擎的唯一目标是将不同的位图组合在一起。您可能要求生成不同的效果、转换,甚至动画,但终归是在组合位图。那些由 Direct2D 或者 Direct3D 之类提供的引擎本身没有图形渲染功能,并且也无法定义矢量或文本。它所关心的就是组合。给它提供位图的集合,它就可以将它们结合起来组成在一起,从而产生令人惊叹的效果。

这些位图可以采用任意几种形式。位图实际上可能是视频存储器。它甚至可能是一个交换链,这一点在我的 6 月专栏里已有说明 (msdn.microsoft.com/magazine/dn745861)。但如果您确实希望开始使用组合引擎,则需要看一看组合面。组合面是由组合引擎直接提供的位图,允许一定程度的优化,这是其他形式的位图很难做到的。

这个月,我将采用我以前专栏中使用的 alpha 混合窗口,并演示如何使用组合面而不是交换链来重现该窗口。还有一些有趣的优势,同时也是一些独特的挑战,特别是围绕处理设备丢失和根据显示器进行的 DPI 缩放方面的优势。但首先我需要重新审视窗口管理的问题。

在我以前的专栏中,我要么将 ATL 用于窗口管理,要么说明如何直接使用 Windows API 注册、创建和抽取窗口消息。这两种方法各有其优缺点。ATL 依然非常适用于窗口管理,但它正逐渐失去开发人员的青睐,同时 Microsoft 也早就停止对它的投资。另一方面,直接使用 RegisterClass 和 CreateWindow 创建窗口往往是有问题的,因为您无法轻松地将 C++ 对象与窗口句柄关联。如果您曾经想过安排这样一个组合,您可能会想了解 ATL 源代码,看看它如何实现这一点,结果发现里面有一些称为“thunk”甚至是汇编语言的东西发挥了奇妙的作用。

令人欣慰的是,它不是太难。ATL 固然可以生成非常高效的消息调度,但一个仅涉及标准 C++ 的简单解决方案就足以做得很好。我不想过多地讨论窗口程序技术的话题,所以我干脆直接带您看看图 1,它提供了一个简单的类模板,只要做些必要的安排,就可以将“此”指针与某个窗口关联。该模板使用 WM_NCCREATE 消息提取指针,并将其与窗口句柄存储在一起。它随后检索指针并发送消息到最低层派生的消息处理程序。

图 1 一个简单的窗口类模板

template <typename T>
struct Window
{
  HWND m_window = nullptr;
  static T * GetThisFromHandle(HWND window)
  {
    return reinterpret_cast<T *>(GetWindowLongPtr(window,
                                                  GWLP_USERDATA));
  }
  static LRESULT __stdcall WndProc(HWND   const window,
                                   UINT   const message,
                                   WPARAM const wparam,
                                   LPARAM const lparam)
  {
    ASSERT(window);
    if (WM_NCCREATE == message)
    {
        CREATESTRUCT * cs = reinterpret_cast<CREATESTRUCT *>(lparam);
        T * that = static_cast<T *>(cs->lpCreateParams);
        ASSERT(that);
        ASSERT(!that->m_window);
        that->m_window = window;
        SetWindowLongPtr(window,
                         GWLP_USERDATA,
                         reinterpret_cast<LONG_PTR>(that));
    }
    else if (T * that = GetThisFromHandle(window))
    {
      return that->MessageHandler(message,
                                  wparam,
                                  lparam);
    }
    return DefWindowProc(window,
                         message,
                         wparam,
                         lparam);
  }
  LRESULT MessageHandler(UINT   const message,
                         WPARAM const wparam,
                         LPARAM const lparam)
  {
    if (WM_DESTROY == message)
    {
      PostQuitMessage(0);
      return 0;
    }
    return DefWindowProc(m_window,
                         message,
                         wparam,
                         lparam);
  }
};

系统假定派生类会创建一个窗口,并在调用 Create-Window 或 CreateWindowEx 函数时将此指针作为最后一个参数传递。 派生类可以简单地注册和创建窗口,然后用 MessageHandler 替代来响应窗口消息。 此重写依赖于编译时多形性,所以不需要虚函数。 但是,效果是一样的,因此您仍然需要留意重新进入。 图 2 显示依赖于窗口类模板的具体窗口类。 这个类在其构造函数中注册并创建窗口,但它依赖于它的基类所提供的窗口程序。

图 2 一个具体的窗口类

struct SampleWindow : Window<SampleWindow>
{
  SampleWindow()
  {
    WNDCLASS wc = {};
    wc.hCursor       = LoadCursor(nullptr, IDC_ARROW);
    wc.hInstance     = reinterpret_cast<HINSTANCE>(&__ImageBase);
    wc.lpszClassName = L"SampleWindow";
    wc.style         = CS_HREDRAW | CS_VREDRAW;
    wc.lpfnWndProc   = WndProc;
    RegisterClass(&wc);
    ASSERT(!m_window);
    VERIFY(CreateWindowEx(WS_EX_NOREDIRECTIONBITMAP,
                          wc.lpszClassName,
                          L"Window Title",
                          WS_OVERLAPPEDWINDOW | WS_VISIBLE,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          CW_USEDEFAULT, CW_USEDEFAULT,
                          nullptr,
                          nullptr,
                          wc.hInstance,
                          this));
    ASSERT(m_window);
  }
  LRESULT MessageHandler(UINT message,
                         WPARAM const wparam,
                         LPARAM const lparam)
  {
    if (WM_PAINT == message)
    {
      PaintHandler();
      return 0;
    }
    return __super::MessageHandler(message,
                                   wparam,
                                   lparam);
  }
  void PaintHandler()
  {
    // Render ...
  }
};

请注意,在图 2 的构造函数中,m_window 继承成员在调用 CreateWindow 之前尚未被初始化(一个 nullptr),但在此函数返回时,完成了初始化。 这可能看起来很奇妙,但早在 CreateWindow 返回之前,该窗口程序在消息开始到达时就将其挂接起来。 记住这一点很重要,因为通过使用如下代码可以重现与调用构造函数中的虚函数相同的危险效果。 如果您停止进一步的派生,只要做到将窗口创建拖出构造函数,这种形式的重新进入就不会使您出错。 下面是一个简单的 WinMain 函数,它可以创建窗口并抽取窗口消息:

int __stdcall wWinMain(HINSTANCE, HINSTANCE, PWSTR, int)
{
  SampleWindow window;
  MSG message;
  while (GetMessage(&message, nullptr, 0, 0))
  {
    DispatchMessage(&message);
  }
}

好了,回到现在的话题。 现在,我有一个简单的窗口类抽象,我可以用它来更轻松地管理生成一个 DirectX 应用程序所需的资源集合。 我还将演示如何正确处理 DPI 缩放。 虽然我在 2014 年 2 月的专栏中详细介绍过 DPI 缩放 (msdn.microsoft.com/magazine/dn574798),但当您将其与 DirectComposition API 结合起来时会存在一些独特的挑战。 我会从头讲起。 我需要包括缩放 API 的 shell:

#include <ShellScalingAPI.h> #pragma comment(lib, "shcore")

我现在可以开始组装我所需要的资源来实现我的窗口。 如果我有一个窗口类,我就可以简单地做出此类的成员。 首先,需要一个 Direct3D 设备:

ComPtr<ID3D11Device> m_device3d;

接着,需要一个 DirectComposition 设备:

ComPtr<IDCompositionDesktopDevice> m_device;

在我以前的专栏中,我使用了 IDCompositionDevice 接口来表示组合设备。 该接口最初在 Windows 8 中生成,但在 Windows 8.1 中取代为派生自称为 IDComposition­Device2 的新接口的 IDCompositionDesktopDevice。 这些与原始资料不相关。 IDComposition­Device2 接口用来创建大部分组合资源,并控制事务性组合。 IDCompositionDesktopDevice 接口增加了创建某个特定窗口的组合资源的功能。

我还需要一个组合目标、Visual 和表面:

ComPtr<IDCompositionTarget>  m_target;
ComPtr<IDCompositionVisual2> m_visual;
ComPtr<IDCompositionSurface> m_surface;

组合目标表示在桌面窗口和可视化树之间的绑定。 实际上,我可以将两个可视化树与一个给定的窗口关联起来,但详细的讲解将在以后的专栏中进行介绍。 Visual 表示可视化树中的节点。 我将在后续的专栏中探讨 Visual 主题,所以现在只是一个单一的根 Visual。 在这里,我只使用 IDCompositionVisual2 接口,它派生自我之前专栏中所使用的 IDCompositionVisual 接口。 最后,有一个表示与 Visual 相关联的内容或位图的表面。 在我以前的专栏中,我只是用一个交换链作为可视化的内容,但马上我将向您演示如何创建一个组合面。

为了说明实际中如何呈现内容和管理渲染资源,我需要更多的成员变量:

ComPtr<ID2D1SolidColorBrush> m_brush;
D2D_SIZE_F                   m_size;
D2D_POINT_2F                 m_dpi;
SampleWindow() :
  m_size(),
  m_dpi()
{
  // RegisterClass / CreateWindowEx as before
}

Direct2D 纯色画笔创建成本不高,但许多其他的渲染资源可不是那么轻巧。 我将使用这个画笔说明如何创建渲染循环外部的渲染资源。 DirectComposition API 还可以选择性地接管 Direct2D 渲染目标的创建。 这可以让您将 Direct2D 的组合面作为目标,不过这也意味着您丢失了一点上下文信息。 具体来说,您可以不用再在渲染目标中缓存适宜的 DPI 缩放因数,因为 DirectComposition 会根据您的需要来创建它。 此外,您可以不再依赖于渲染目标的 GetSize 方法来报告窗口的大小。 不过不要担心,马上我会告诉您如何弥补这些缺点。

因为存在着依赖于 Direct3D 设备的应用程序,所以在假设该设备随时会丢失的情况下,我需要小心管理驻留在物理设备上的资源。 GPU 可能会挂起、重启、被删除或者直接崩溃。 此外,我必须小心,避免在创建设备堆栈之前对可能到达的窗口消息作出不适当的响应。 我将使用 Direct3D 设备指针来指示设备是否已创建:

bool IsDeviceCreated() const { return m_device3d; }

这只是有助于明确该查询。 我也用这个指针来启动设备堆栈重置,以强制所有设备相关的资源得以重新创建:

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

同样,这只是有助于明确此操作。 我可以在这里释放所有设备相关的资源,但是这不是绝对必要的,并且添加或删除不同的资源时维护会迅速成为一个令人头痛的问题。 设备创建过程的主要内容位于另一个辅助方法中:

void CreateDeviceResources() { if (IsDeviceCreated()) return;
 // 创建设备和资源 ...
}

正是在此处的 CreateDeviceResources 方法中,我可以创建或重新创建设备堆栈、硬件设备,以及窗口要求的各种资源。 首先,我会创建所有事项都重置的 Direct3D 设备:

HR(D3D11CreateDevice(nullptr,    
// 适配卡 D3D_DRIVER_TYPE_HARDWARE, nullptr,    
// 模块 D3D11_CREATE_DEVICE_BGRA_SUPPORT, 
nullptr, 0, // 最高可用功能级别 D3D11_SDK_VERSION,
 m_device3d.GetAddressOf(), nullptr,   
 // 实际功能级别 nullptr));  // 设备上下文

请注意 m_device3d 成员捕获产生的接口指针的方式。 现在,我需要查询设备的 DXGI 接口:

ComPtr<IDXGIDevice> devicex; HR(m_device3d.As(&devicex));

在我以前的专栏中,正是在这一点上,我创建了 DXGI 工厂和用于组合的交换链。 我创建了一个交换链,并将其包裹在一个 Direct2D 位图中,针对该位图使用设备上下文等。 在这里,我要执行的操作完全不同。 在创建了 Direct3D 设备之后,我现在要创建一个指向它的 Direct2D 设备,然后创建指向 Direct2D 设备的 DirectComposition 设备。 这是一个 Direct2D 设备:

ComPtr<ID2D1Device> device2d; HR(D2D1CreateDevice(devicex.Get(), nullptr,
 // 默认属性 device2d.GetAddressOf()));

我使用由 Direct2D API 提供的帮助程序函数,而不是人们更熟悉的 Direct2D 工厂对象。 产生的 Direct2D 设备简单地从 DXGI 设备继承了线程模型,但是您也可以重写它并打开调试跟踪。 这是一个 DirectComposition 设备:

HR(DCompositionCreateDevice2( device2d.Get(),
 __uuidof(m_device), reinterpret_cast<void 
**>(m_device.ReleaseAndGetAddressOf())));

在设备丢失后,我小心地使用 m_device 成员的 ReleaseAndGet­AddressOf 方法来支持设备堆叠的重建。 正如我在以前的专栏中所做的,如果给定了组合设备,我现在就可以创建组合目标:

HR(m_device->CreateTargetForHwnd
(m_window, true, // 
最顶层 m_target.ReleaseAndGetAddressOf()));

和根 Visual:

HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));

现在是时候将重点放在取代交换链的组合面上了。 同样,当我调用 CreateSwapChainForComposition 方法时,DXGI 工厂不知道交换链的缓冲区应该是多大,DirectComposition 设备也不知道基础面应该是多大。 我需要查询窗口的工作区的大小,并使用这些信息来告知表面的创建:

RECT rect = {}; VERIFY(GetClientRect(m_window, &rect));

RECT 结构有左、上、右、下的成员,我可以使用这些来确定使用物理像素创建的表面所需的大小:

HR(m_device->CreateSurface(rect.right - rect.left, 
rect.bottom - rect.top, DXGI_FORMAT_B8G8R8A8_UNORM, 
DXGI_ALPHA_MODE_PREMULTIPLIED, 
m_surface.ReleaseAndGetAddressOf()));

请记住,实际表面很可能小于请求的大小。 这是因为该组合引擎可能为了提高效率而采用了池分配或循环分配的方式。 这并不是问题,但它确实会影响所产生的设备上下文,因为您将无法依靠其 GetSize 方法,马上我们就来详细探讨这部分内容。

幸运的是,用于 CreateSurface 方法的参数是 DXGI_SWAP_CHAIN_DESC1 结构的许多旋钮和转盘的简化。 按照此大小,我指定了像素格式和 Alpha 模式,且组合设备返回一个指向新创建的组合面的指针。 然后,我可以简单地将这种表面设置为我的 Visual 对象的内容,并将该 Visual 设置为我的组合目标的根:

HR(m_visual->SetContent(m_surface.Get()));
 HR(m_target->SetRoot(m_visual.Get()));

但是,我不需要在此阶段调用组合设备的 Commit 方法。 我将要更新渲染循环中的组合面,但这些更改将只有在调用 Commit 方法时生效。 此时,组合引擎已准备开始渲染,但仍然有一些收尾事宜需要注意。 它们与组合无关,但却与正确高效地使用 Direct2D 进行渲染有关。 首先,任何特定的渲染目标资源(如位图和画笔)应该在渲染循环外部创建。 因为 DirectComposition 创建了渲染目标,这可能会有点麻烦。 幸运的是,唯一的要求是:在与最终渲染目标相同的地址空间中创建这些资源,所以我只需在此创建一个丢弃设备上下文即可创建这样的资源:

ComPtr<ID2D1DeviceContext> dc;
 HR(device2d->CreateDeviceContext
(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
 dc.GetAddressOf()));

那么我可以用这个渲染目标来创建应用程序的单个画笔:

D2D_COLOR_F const color =
 ColorF(0.26f, 0.56f, 0.87f, 0.5f); 
HR(dc->CreateSolidColorBrush
(color, m_brush.ReleaseAndGetAddressOf()));

然后,设备上下文被丢弃,但画笔仍留在我的渲染循环中可以重复使用。 这有点违反直觉,但马上会派上用场。 我在渲染前需要做的最后一件事是填充 m_size 和 m_dpi 这两个成员变量。 传统上,Direct2D 渲染目标的 GetSize 方法提供渲染目标的逻辑像素大小,也称为与设备无关的像素。 这个逻辑大小可以用来解释有效的 DPI,所以我会首先处理它。 正如我在 2014 年 2 月专栏中对高 DPI 应用程序说明的那样,我可以通过先确定给定窗口主要位于哪个监视器,然后获得那个监视器的有效 DPI,从而查询到特定窗口的有效 DPI。 该代码类似于下面这样:

HMONITOR const monitor = MonitorFromWindow(m_window,
                     MONITOR_DEFAULTTONEAREST);
unsigned x = 0;
unsigned y = 0;
HR(GetDpiForMonitor(monitor,
                    MDT_EFFECTIVE_DPI,
                    &x,
                    &y));

然后我可以在我的 m_dpi 成员中缓存这些值,以便我可以很容易地更新在我的渲染循环中由 DirectComposition API 提供的设备上下文:

m_dpi.x = static_cast<float>(x); 
m_dpi.y = static_cast<float>(y);

现在,以逻辑像素的方式计算客户端的逻辑大小只不过是一件采用以物理像素保留大小的 RECT 结构以及纳入我现在在处理的有效 DPI 值因素的简单事情:

m_size.width  = (rect.right - rect.left)
 * 96 / m_dpi.x; m_size.height = 
(rect.bottom - rect.top) * 96 / m_dpi.y;

这就是对 CreateDeviceResources 方法及其角色作用的概括总结。 您可以在全面演示 CreateDeviceResources 方法的图 3 中看到它是如何呈现的。

图 3 创建设备堆栈

void CreateDeviceResources()
{
  if (IsDeviceCreated()) return;
  HR(D3D11CreateDevice(nullptr,    // Adapter
                       D3D_DRIVER_TYPE_HARDWARE,
                       nullptr,    // Module
                       D3D11_CREATE_DEVICE_BGRA_SUPPORT,
                       nullptr, 0, // Highest available feature level
                       D3D11_SDK_VERSION,
                       m_device3d.GetAddressOf(),
                       nullptr,    // Actual feature level
                       nullptr));  // Device context
  ComPtr<IDXGIDevice> devicex;
  HR(m_device3d.As(&devicex));
  ComPtr<ID2D1Device> device2d;
  HR(D2D1CreateDevice(devicex.Get(),
                      nullptr, // Default properties
                      device2d.GetAddressOf()));
  HR(DCompositionCreateDevice2(
     device2d.Get(),
     __uuidof(m_device),
     reinterpret_cast<void **>(m_device.ReleaseAndGetAddressOf())));
  HR(m_device->CreateTargetForHwnd(m_window,
                                   true, // Top most
                                   m_target.ReleaseAndGetAddressOf()));
  HR(m_device->CreateVisual(m_visual.ReleaseAndGetAddressOf()));
  RECT rect = {};
  VERIFY(GetClientRect(m_window,
                       &rect));
  HR(m_device->CreateSurface(rect.right - rect.left,
                             rect.bottom - rect.top,
                             DXGI_FORMAT_B8G8R8A8_UNORM,
                             DXGI_ALPHA_MODE_PREMULTIPLIED,
                             m_surface.ReleaseAndGetAddressOf()));
  HR(m_visual->SetContent(m_surface.Get()));
  HR(m_target->SetRoot(m_visual.Get()));
  ComPtr<ID2D1DeviceContext> dc;
  HR(device2d->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE,
                                   dc.GetAddressOf()));
  D2D_COLOR_F const color = ColorF(0.26f,
                                   0.56f,
                                   0.87f,
                                   0.5f);
  HR(dc->CreateSolidColorBrush(color,
                               m_brush.ReleaseAndGetAddressOf()));
  HMONITOR const monitor = MonitorFromWindow(m_window,
                                             MONITOR_DEFAULTTONEAREST);
  unsigned x = 0;
  unsigned y = 0;
  HR(GetDpiForMonitor(monitor,
                      MDT_EFFECTIVE_DPI,
                      &x,
                      &y));
  m_dpi.x = static_cast<float>(x);
  m_dpi.y = static_cast<float>(y);
  m_size.width  = (rect.right - rect.left) * 96 / m_dpi.x;
  m_size.height = (rect.bottom - rect.top) * 96 / m_dpi.y;
}

在执行消息处理程序之前,我需要替代窗口类模板的 MessageHandler 来指示我想处理哪些消息。 至少,我需要处理将在其中提供绘图命令的 WM_PAINT 消息;将在其中调整表面大小的 WM_SIZE 消息;以及在其中更新窗口有效 DPI 和大小的 WM_DPICHANGED 消息。 图 4 显示 MessageHandler。正如您所期望的,它只是将消息转发到相应的处理程序。

图 4 消息调度

LRESULT MessageHandler(UINT message,
                       WPARAM const wparam,
                       LPARAM const lparam)
{
  if (WM_PAINT == message)
  {
    PaintHandler();
    return 0;
  }
  if (WM_SIZE == message)
  {
    SizeHandler(wparam, lparam);
    return 0;
  }
  if (WM_DPICHANGED == message)
  {
    DpiHandler(wparam, lparam);
    return 0;
  }
  return __super::MessageHandler(message,
                                 wparam,
                                 lparam);
}

在我进入绘图序列之前,我会在 WM_PAINT 处理程序中创建所需的设备资源。 请记住,如果该设备已经存在,那么 CreateDeviceResources 不会进行任何操作:

void PaintHandler()
{
  try
  {
    CreateDeviceResources();
    // Drawing commands ...
}

通过这种方式,我可以通过 ReleaseDeviceResources 方法释放 Direct3D 设备指针简单地对设备丢失作出回应,下一次将围绕 WM_PAINT 处理程序对其全部进行重新创建。 整个过程封闭在一个 try 块中,这样就可以对任何设备故障可靠地进行处理。 为了开始绘制到组合面,我需要调用它的 BeginDraw 方法:

ComPtr<ID2D1DeviceContext> dc;
POINT offset = {};
HR(m_surface->BeginDraw(nullptr, // Entire surface
                        __uuidof(dc),
                        reinterpret_cast<void **>(dc.GetAddressOf()),
                        &offset));

BeginDraw 返回一个设备上下文(即 Direct2D 渲染目标),我将使用它来批处理实际的绘图命令。 DirectComposition API 使用我原先在创建组合设备时提供的 Direct2D 设备在此创建并返回设备上下文。 我可以选择提供一个以物理像素计算的 RECT 结构来剪切表面,也可以选择指定一个 nullptr 来允许无限制地访问绘图面。 BeginDraw 方法也会再次返回一个以物理像素计算的偏移,以表明预期的绘图表面的由来。 这并不一定是表面的左上角,必须要小心地调整或改变任何绘图命令使他们正确地偏移。

组合面也可以应用 EndDraw 方法。这两种方法取代 Direct2D BeginDraw 和 EndDraw 方法。 您不得调用设备上下文上对应的方法,因为 DirectComposition API 已为您处理好此事项。 显然,DirectComposition API 还确保了设备上下文将组合面选为其目标。 此外,重要的一点是,您不必保留设备上下文,但总结绘图后要及时将其释放。 此外,不能保证表面会保留之前可能已绘制的帧的内容,因此在总结之前需要仔细清除目标或重新绘制每个像素。

产生的设备上下文已准备就绪,但没有应用窗口的有效 DPI 缩放因素。 现在,我可以使用先前在我的 CreateDeviceResources 方法中计算出的 DPI 值更新设备上下文:

dc->SetDpi(m_dpi.x,
           m_dpi.y);

我也可以只使用平移转换矩阵来调整 DirectComposition API 所需的由绘图命令给出的偏移量。 我只需小心地将偏移量转换为逻辑像素即可,因为这是 Direct2D 的假设内容:

dc->SetTransform(Matrix3x2F::Translation(offset.x * 96 / m_dpi.x,
                                         offset.y * 96 / m_dpi.y));

现在,我可以清除目标并绘制特定应用程序的内容。 在这里,我使用之前在 CreateDeviceResources 方法中创建的与设备相关的画笔绘制了一个简单的矩形:

dc->Clear();
D2D_RECT_F const rect = RectF(100.0f,
                              100.0f,
                              m_size.width - 100.0f,
                              m_size.height - 100.0f);
dc->DrawRectangle(rect,
                  m_brush.Get(),
                  50.0f);

我凭借的是缓存的 m_size 成员,而不是 GetSize 方法所报告的任何大小,因为后者报告的是基础面的大小,而不是工作区的大小。

总结绘图序列涉及多个步骤。 首先,我需要调用表面上的 EndDraw 方法。 这将告知 Direct2D 完成所有批处理绘图命令,并将其写入组合面。 然后,表面完成组合的准备,但在调用组合设备上的 Commit 方法之前不组合。 在这一点上,任何对可视化树的更改(包括任何更新的表面)都成批进行处理,并在一个单一的事务性单元中供组合引擎使用。 这样就完成了渲染过程。 剩下的唯一问题是 Direct3D 设备是否已经丢失。 Commit 方法将报告任何失败,并且 catch 块将释放设备。 如果一切顺利的话,我可以告知 Windows,通过使用 ValidateRect 函数验证窗口的整个工作区,我已经成功地绘制了窗口。 否则,我需要释放设备。 有可能与以下情形类似:

// Drawing commands ...
  HR(m_surface->EndDraw());
  HR(m_device->Commit());
  VERIFY(ValidateRect(m_window, nullptr));
}
catch (ComException const & e)
{
  ReleaseDeviceResources();
}

我并不需要明确地重新绘制,因为如果我不通过验证工作区来做出响应,Windows 就只会继续发送 WM_PAINT 消息。 WM_SIZE 处理程序负责调整组合面的尺寸,也负责更新渲染目标的缓存大小。 如果没有创建该设备或窗口被最小化,我就不会做出反应:

void SizeHandler(WPARAM const wparam,
                 LPARAM const lparam)
{
  try
  {
    if (!IsDeviceCreated()) return;
    if (SIZE_MINIMIZED == wparam) return;
    // ...
}

一个窗口通常会先收到 WM_SIZE 消息,然后才有机会创建设备堆栈。 当发生这种情况时,我只是忽略该消息。 如果 WM_SIZE 消息是一个最小化的窗口的结果,我也会忽略该消息。 我不希望在这种情况下对表面的大小做不必要的调整。 由于有 WM_PAINT 处理程序,WM_SIZE 句柄就在一个 try 块中执行其相关操作。 经过调整大小或在这种情况下重新创建,表面很可能会由于设备丢失而导致失败,而这应该会导致设备堆栈被重新创建。 但是,首先,我可以提取工作区的新大小:

unsigned const width  = LOWORD(lparam);
unsigned const height = HIWORD(lparam);

并更新以逻辑像素计算的缓存大小:

m_size.width  = width  * 96 / m_dpi.x;
m_size.height = height * 96 / m_dpi.y;

组合面是不可调整大小的。 我使用了可能被称为非虚拟的表面。 组合引擎还提供了可调整大小的虚拟表面,但我会在后续的专栏中对此进行详细介绍。 在这里,我可以只是简单地释放当前表面并重新创建它。 由于对可视化树的更改在提交更改之前没有体现出来,因此当表面被丢弃和重新创建时,用户将不会遇到任何闪烁的情况。 有可能与以下情形类似:

HR(m_device->CreateSurface(width,
                           height,
                           DXGI_FORMAT_B8G8R8A8_UNORM,
                           DXGI_ALPHA_MODE_PREMULTIPLIED,
                           m_surface.ReleaseAndGetAddressOf()));
HR(m_visual->SetContent(m_surface.Get()));

然后,我就可以通过释放设备资源来应对任何故障,以致下一个 WM_PAINT 消息将导致他们被重新创建:

// ...
}
catch (ComException const & e)
{
  ReleaseDeviceResources();
}

这是针对 WM_SIZE 处理程序。 最终必要的步骤是执行 WM_DPICHANGED 处理程序,以更新窗口的有效 DPI 和大小。 该消息的 WPARAM 提供了新的 DPI 值,LPARAM 则提供了新的大小。 我简单地更新窗口的 m_dpi 成员变量,然后调用 SetWindowPos 方法来更新窗口的大小。 然后,窗口会收到另一个 WM_SIZE 消息,我的 WM_SIZE 处理程序将用其调整 m_size 成员,并重新创建表面。 图 5 提供了如何处理这些 WM_DPICHANGED 消息的示例。

图 5 处理 DPI 更新

void DpiHandler(WPARAM const wparam,
                LPARAM const lparam)
{
  m_dpi.x = LOWORD(wparam);
  m_dpi.y = HIWORD(wparam);
  RECT const & rect = *reinterpret_cast<RECT const *>(lparam);
  VERIFY(SetWindowPos(m_window,
                      0, // No relative window
                      rect.left,
                      rect.top,
                      rect.right - rect.left,
                      rect.bottom - rect.top,
                      SWP_NOACTIVATE | SWP_NOZORDER));
}

我很高兴地看到 DirectX 系列成员一起更为紧密地协作,提高了互操作性和性能,这主要归功于 Direct2D 和 DirectComposition 之间的深度集成。 我希望您与我一样,因有可能使用 DirectX 构建丰富的本机应用程序而感到兴奋不已。

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

衷心感谢以下 Microsoft 技术专家对本文的审阅: Leonardo Blanco 和 James Clarke