借助 C++ 进行 Windows 开发

  • 2015/07/10

一个用于 DirectX 编程的现代 C++ 库

Kenny Kerr

下载代码示例

我写过很多 DirectX 代码,也写过很多关于 DirectX 的文章。我甚至还编写过关于 DirectX 的在线培训课程。它其实并不像某些开发人员所说的那么难以理解。学习曲线一定会有,但一旦您过了这道坎,就不难理解 DirectX 的工作方式及其为何要如此工作的原因了。不过我也承认,DirectX 系列 API 的易用性应该更高些。

几天前,我决定着手修补一下这个缺陷。我熬了一整夜,编写了一个小头文件。随后几晚,我又将代码行扩展到了近 5,000 行。我的目标是提供一些可借助 Direct2D 更方便地构建应用程序的东西,并向如今盛行的所有“C++ 很难”或“DirectX 很难”之类的论断发起挑战。我并没打算再开发一个笨重的 DirectX 包装。实际上,我决定借助 C++11 制作一套更简便的 DirectX API,同时不在核心 DirectX API 之上产生任何空间和时间开销。您可以在下面的网址找到我开发的这个库:dx.codeplex.com

这个库本身只包含一个名为 dx.h 的头文件,CodePlex 上的其余源文件提供了有关该头文件使用方法的示例。

在本专栏文章中,我将向您展示如何利用这个库更方便地执行各种与 DirectX 相关的常见活动。此外,我还将介绍这个库的设计方法,以便您了解 C++11 如何帮助提高传统 COM API 的易用性,而不必求助于 Microsoft .NET Framework 等会对性能产生很大影响的包装。

显然,我们的重点是 Direct2D。要借助 DirectX 开发类别最为广泛的应用程序和游戏,Direct2D 仍是最简单也最有效的方式。许多开发人员似乎加入到了两个对立的阵营中。他们中有 DirectX 铁杆开发人员:他们不断学习各种版本的 DirectX API。他们在 DirectX 多年来的发展中历经磨练,并且乐于成为这一进入门槛极高的“贵宾俱乐部”的一员(很少有开发人员能加入该俱乐部)。而在另一阵营的开发人员听到了 DirectX 很难的消息,不想跟 DirectX 扯上一丁点关系。不用说,他们往往会拒绝使用 C++。

我不属于任一阵营。我相信 C++ 和 DirectX 不必如此困难。在上月的专栏文章 (msdn.microsoft.com/magazine/dn198239) 中,我介绍了 Direct2D 1.1 和作为先决条件的 Direct3D 和 DirectX 图形基础结构 (DXGI) 代码,以创建一个设备并管理交换链。该代码利用 D3D11CreateDevice 函数创建 Direct3D 设备,适用于 GPU 或 CPU 呈现,长度约 35 行。不过,在我提供的小头文件的帮助下,可将其精简为:

c++
auto device = CreateDevice();

CreateDevice 函数返回一个 Device1 对象。 由于所有 Direct3D 定义都位于 Direct3D 命名空间内,所以也可以这样写(更加明确):

c++
Direct3D::Device1 device = Direct3D::CreateDevice();

Device1 对象不过是对 ID3D11­Device1 COM 接口指针(DirectX 11.1 版本引入的 Direct3D 设备接口)的包装。 Device1 类派生自 Device 类,后者是对原 ID3D11Device 接口的包装。 它代表一个引用,与直接获取该接口指针本身相比,不会带来任何额外开销。 请注意,Device1 及其父类 Device 是常规的 C++ 类,而不是接口。 您可以将它们看作智能指针,但这有些过于简单化。 当然,它们能够处理引用计数并提供“->”运算符直接调用您选择的方法,但在开始使用 dx.h 库提供的诸多非虚方法时,才是它们真正大放异彩之时。

例如: 通常,您可能需要 Direct3D 设备的 DXGI 接口来传递其他某种方法或函数。 不怕麻烦的话,可以这样做:

c++
 dxdevice; HR(device->QueryInterface(dxdevice.GetAddressOf()));
">auto device = Direct3D::CreateDevice(); wrl::ComPtr<IDXGIDevice2> dxdevice; HR(device->QueryInterface(dxdevice.GetAddressOf()));

这当然可行,但现在您还必须直接处理 DXGI 设备接口。 另外,您还需要牢记,IDXGIDevice2 接口是 DirectX 11.1 版本的 DXGI 设备接口。 实际上,也可简单地调用 AsDxgi 方法:

c++
auto device = Direct3D::CreateDevice(); auto dxdevice = device.AsDxgi();

返回的 Device2 对象(此次是在 Dxgi 命名空间中定义的)包装 IDXGIDevice2 COM 接口指针,提供自己的一组非虚方法。 再举一个例子,您可能需要使用 DirectX“对象模型”访问 DXGI 工厂:

c++
auto device   = Direct3D::CreateDevice(); auto dxdevice = device.AsDxgi(); auto adapter  = dxdevice.GetAdapter(); auto factory  = adapter.GetParent();

当然,这是一种常见模式,是 Direct3D Device 类提供 GetDxgiFactory 方法作为捷径的一种手段:

c++
auto d3device = Direct3D::CreateDevice(); auto dxfactory = d3device.GetDxgiFactory();

因此,除了少量便捷的方法和函数(如 GetDxgiFactory)外,非虚方法以一对一形式映射到底层的 DirectX 接口方法和函数。 为了构建异常简便且高效的 DirectX 编程模型,我综合利用了几种方法(虽然看似不多)。 第一种方法是使用范围枚举。 DirectX 系列 API 定义了一个令人眼花缭乱的常量数组,其中有很多是传统的枚举、标志或常量。 它们不是强类型,很难找到并使用,而且与 Visual Studio IntelliSense 之间的配合不太好。 下面是创建 Direct2D 工厂所需的代码(请先忽略工厂选项):

c++
 factory; HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,                      factory.GetAddressOf()));
">wrl::ComPtr<ID2D1Factory1> factory; HR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED,                      factory.GetAddressOf()));

D2D1CreateFactory 函数的第一个参数是一个枚举,但由于它并非范围枚举,很难被 Visual Studio IntelliSense 发现。 这些老旧的枚举提供了些微的类型安全,但并不多。 充其量,您会在运行时遇到 E_INVALIDARG 结果代码。 我不知道您会怎么做,但我宁愿在编译时捕获此类错误,或者,更好的办法是根本不用它们:

c++
auto factory = CreateFactory(FactoryType::MultiThreaded);

之前的优点再次显现出来:我无需花费时间四处查找调用了哪个最新版本的 Direct2D 工厂接口。 当然,这里最大的优势还是效率。 DirectX API 不仅仅是创建和调用 COM 接口方法。 为了将各种各样的属性和参数包装到一起,DirectX API 采用了许多简陋的老式数据结构。 交换链的说明就是一个很好的示例。 它包含很多容易混淆的成员,我从来记不住该如何准备这个结构,更不用说和平台有关的细节了。 这里,我编写的库再次派上了用场,它提供了另一个结构,用来代替令人恐惧的 DXGI_SWAP_CHAIN_DESC1 结构:

c++
SwapChainDescription1 description;

在本例中,我提供了一个二进制兼容的替代品,以确保 DirectX 将其视作相同的类型,但您在使用时应该提供更具实用性的替代品。 它与 Microsoft .NET Framework 为其 P/Invoke 包装提供的替代品相似。 该默认构造函数提供的默认值适用于大多数桌面和 Windows 应用商店应用程序。 但您可能需要重写这些值,例如针对桌面应用程序作出如下改动,以便在调整大小时实现更平滑的呈现效果:

c++
SwapChainDescription1 description; description.SwapEffect = SwapEffect::Discard;

顺便提一下,在编写 Windows Phone 8 应用程序时,也需要使用这种交换效果,但不允许在 Windows 应用商店应用程序中使用。 自己去想吧。

许多最好的库能够引领您快速、方便地找到可行的解决方案。 让我们看一个具体的示例。 Direct2D 提供了一个线性渐变画笔。 该画笔的创建过程包含三个逻辑步骤: 定义渐变停止点,创建渐变停止点集合,然后创建该集合的线性渐变画笔。 图 1 是直接使用 Direct2D API 的示例。

图 1 创建线性渐变画笔(比较麻烦的方式)

c++
 collection; HR(target->CreateGradientStopCollection(stops,    _countof(stops),    collection.GetAddressOf())); wrl::ComPtr brush; HR(target->CreateLinearGradientBrush(D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES(),    collection.Get(),    brush.GetAddressOf()));
">D2D1_GRADIENT_STOP stops[] = {   { 0.0f, COLOR_WHITE },   { 1.0f, COLOR_BLUE }, }; wrl::ComPtr<ID2D1GradientStopCollection> collection; HR(target->CreateGradientStopCollection(stops,    _countof(stops),    collection.GetAddressOf())); wrl::ComPtr<ID2D1LinearGradientBrush> brush; HR(target->CreateLinearGradientBrush(D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES(),    collection.Get(),    brush.GetAddressOf()));

借助 dx.h 就直观了许多:

c++
GradientStop stops[] = {   GradientStop(0.0f, COLOR_WHITE),   GradientStop(1.0f, COLOR_BLUE), }; auto collection = target.CreateGradientStopCollection(stops); auto brush = target.CreateLinearGradientBrush(collection);

虽然这并未大幅缩短图 1 所示代码的长度,但这段代码显然更易编写,首次使用也不易出错,何况还有 IntelliSense 的帮助。 该库采用了多种方法来构建令人愉悦的编程模型。 在本例中,我们使用了一个函数模板来重载 CreateGradientStopCollection 方法,从而缩减了编译时的 GradientStop 数组的大小,因此,无需使用 _countof 宏。

如何进行错误处理? 构建此类精简编程模型的先决条件之一就是采用实现错误传播的异常。 请考虑我之前提到的 Direct3D Device AsDxgi 方法的定义:

c++
 Dxgi::Device2 {   Dxgi::Device2 result;   HR(m_ptr.CopyTo(result.GetAddressOf()));   return result; }
">inline auto Device::AsDxgi() const -> Dxgi::Device2 {   Dxgi::Device2 result;   HR(m_ptr.CopyTo(result.GetAddressOf()));   return result; }

这是库中的一种极为常见的模式。 首先,请注意,该方法为常量类型。 库中的几乎所有方法都为常量类型,这是因为唯一的数据成员是底层的 ComPtr,我们无需修改它。 在方法体中,您可以看到最终的 Device 对象是如何产生的。 库中的所有类都提供移动语义,因此,即使这看似需要执行一系列复制操作(通过推理,是一系列 AddRef/Release 对),但实际上不会在运行时产生任何此类操作。 代码中间将表达式括起来的 HR 是一个内联函数,它会在结果不为 S_OK 时引发异常。 最后,库总是尝试返回最具体的类,以避免调用方不得不执行额外的 QueryInterface 调用来公开更多功能。

上面的示例使用了 ComPtr CopyTo 方法,它实际上只是调用了 QueryInterface。 下面是来自 Direct2D 的另一个示例:

c++
 Bitmap {   Bitmap result;   (*this)->GetBitmap(result.GetAddressOf());   return result; }
">inline auto BitmapBrush::GetBitmap() const -> Bitmap {   Bitmap result;   (*this)->GetBitmap(result.GetAddressOf());   return result; }

这个示例有些不同之处:它直接调用了位于底层 COM 接口上的方法。 事实上,这种模式构成了库中的大部分代码。 此处,我返回的是画笔绘制所用的位图。 许多 Direct2D 方法与此处的示例一样返回 void,因此,无需借助 HR 函数检查结果。 但是,对 GetBitmap 方法的间接调用可能并不那么明显。

我在开发这个库的早期版本时,不得不在依靠编译器取巧还是依靠 COM 取巧之间作出选择。 我的早期尝试包括借助 C++ 的模板取巧,特别是类型特性,但也包括编译器类型特性(也称作内部类型特性)。 起初,这很有趣,但事实很快证明,我是在自找麻烦。

您会发现,该库将 COM 接口之间的从属关系模型构建为具体的类。 COM 接口只能直接继承另一个接口。 除 IUnknown 自身之外,每个 COM 接口都必须直接继承另一个接口。 最终,这导致所有方式又回到了 IUnknown 类型层次结构。 开始时,我为每个 COM 接口定义了一个类。 RenderTarget 类包含 ID2D1RenderTarget 接口指针。 DeviceContext 类包含 ID2D1DeviceContext 接口指针。 这看似十分合理,但当您需要将 DeviceContext 用作 RenderTarget 时,情况发生了改变。 毕竟,ID2D1DeviceContext 接口派生自 ID2D1RenderTarget 接口。 因此,将 DeviceContext 作为引用参数传递给接收 RenderTarget 的方法顺理成章。

但遗憾的是,C++ 类型系统不这样认为。 使用这种方法时,DeviceContext 实际上无法派生自 RenderTarget,否则它会保有两个引用。 我曾试图将移动语义和内部类型特性结合起来,以根据需要正确地移动引用。 我差点就取得了成功,但有些情况会引入额外的 AddRef/Release 对。 最终,事实证明这太过复杂,我需要一种更简洁的解决方案。

与 C++ 不同,COM 拥有定义明确的二进制协定。 毕竟,这才是 COM 的真正意义。 只要遵守约定规则,COM 绝不会让您失望。 可以这么说,您可以通过 COM 取巧,并使用 C++ 来增强您的优势,而不是跟它对着干。 这意味着,每个 C++ 类并不保有强类型化的 COM 接口指针,而只是一个指向 IUnknown 的泛型引用。 C++ 会将类型安全这一特性补回来,包括其针对类继承的规则(以及最新的移动语义),让您能够再次将这些 COM“对象”用作 C++ 类。 从概念上讲,我开始时这样做:

c++
 ptr; }; class DeviceContext { ComPtr ptr; };
">class RenderTarget { ComPtr<ID2D1RenderTarget> ptr; }; class DeviceContext { ComPtr<ID2D1DeviceContext> ptr; };

但最终得到的是:

c++
 ptr; }; class RenderTarget : public Object {}; class DeviceContext : public RenderTarget {};
">class Object { ComPtr<IUnknown> ptr; }; class RenderTarget : public Object {}; class DeviceContext : public RenderTarget {};

由于 COM 接口及其关系所暗示的逻辑层次结构现在被一个 C++ 对象模型具体化了,因此,这整个编程模型将极其自然而实用。 关于这个库,还有很多功能值得研习,因此,我鼓励您仔细查阅一下源代码。 到撰写本文时为止,该库已涵盖几乎所有 Direct2D 和 Windows 动画管理器,以及一些 DirectWrite、Windows 图像处理组件 (WIC)、Direct3D 和 DXGI 的有用代码块。 另外,我会定期添加更多功能,所以也请您不时回来看看。 请尽情体验吧!

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

衷心感谢以下技术专家对本文的审阅: Worachai Chaoweeraprasit (Microsoft)
Worachai Chaoweeraprasit (Microsoft),wchao@microsoft.com

Worachai Chaoweeraprasit 是 Direct2D 和 DirectWrite 的开发组组长。 他对 2D 矢量图形的速度和质量,以及屏幕文本的可读性异常痴迷。 闲暇时,他喜欢在家陪两位“小大人”嬉戏、玩耍。

(0)

相关推荐