想要使用 Direct2D 图形接口,首先需要理解几个概念:渲染目标(render target)、设备(device)和资源(resource)。

渲染目标 简单来说就是程序将要绘制的位置。典型的渲染目标就是窗口(客户区)。渲染目标也可以是在内存中的位图,它不会显示到屏幕上。代码中 ID2D1RenderTarget 接口代表渲染目标。

设备 是一种抽象的概念,它用来代表真正的,用来绘制像素的东西。一个硬件设备可以使用 GPU 提高绘图效率,而一个软件设备只能使用 CPU 来进行绘图。应用程序不能创建设备,相反的,当应用程序创建渲染目标的时候,设备会被隐式创建。每个渲染目标都会和一个特定的设备关联,可以是软件的,也可以是硬件的。

渲染流程

资源 是程序用来绘图的对象。这有一些在 Direct2D 中定义的资源例子:

  • Brush。用来绘制直线或者填充一个区域。画刷类型包括纯色画刷和梯度画刷。

  • Stroke Style。控制直线的外观。如虚线、实线等等。

  • Geometry。代表一组直线和曲线的结合。

  • Mesh。使用三角形构成的形状。网格数据可以直接被 GPU 使用,不像几何数据需要在渲染之前进行转换。

渲染目标本身也被视为一种资源。

一些资源支持硬件加速。这种资源类型总是会和特定的设备相关联。可以硬件(GPU),也可以是软件(CPU)。这种类型的资源叫做 设备相关资源(device-dependent resources)。如果设备突然失效,则设备相关资源必须重新创建。

另一些资源不管设备是否被使用,都会保存在内存中,这种资源称为 设备无关资源(device-independent resources)。这是因为它们无需和设备进行关联,当设备被改变的时候无需重新创建。Stroke styles 和 geometries 是设备无关的资源类型。

在 MSDN 中,每种资源都会表明具体的类型,是设备无关还是设备相关。每种资源类型都是来源于 ID2D1Resource 接口。例如:ID2D1Brush 接口代表画刷。

1. Direct2D 工厂对象

使用 Direct2D 之前,需要创建一个 Direct2D 工厂对象的实例,在计算机程序中,一个工厂是用来创建其它对象的类。Direct2D 工厂可以创建下面的对象:

  • 渲染目标。

  • 设备无关资源。如绘制样式或者几何图形。

类似画刷、位图等设备独立资源则是由设备目标对象来进行创建。

工厂创建模式

Direct2D 工厂对象可以通过 D2D1CreateFactory 函数创建。

ID2D1Factory *pFactory = NULL;
HRESULT hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory);

函数的第一个参数是一个标志,用来指定创建选项。D2D1_FACTORY_TYPE_SINGLE_THREADED 标记的含义是确保应用程序不会从多线程中调用 Direct2D。如果想要在多线程环境中使用,则需要指定 D2D1_FACTORY_TYPE_MULTI_THREADED 标记。如果程序仅仅使用单线程,指定前者会让程序更高效。

第二个参数需要传入一个 ID2D1Factory 的指针,如果 D2D1CreateFactory 调用成功,则该函数代表着返回的工厂对象。

你应该在 WM_PAINT 消息发送之前创建 Direct2D 工厂对象。WM_CREATE 消息处理函数是一个不错的选择。

case WM_CREATE:
    if (FAILED(D2D1CreateFactory(
            D2D1_FACTORY_TYPE_SINGLE_THREADED, &pFactory)))
    {
        return -1;  // Fail CreateWindowEx.
    }
    return 0;

2. 创建 Direct2D 资源

一个绘制椭圆的程序可能需要使用下面两种设备相关资源:

  • 一个关联程序窗口的渲染目标。

  • 一个用来绘制圆形的纯色画刷。

下面是这些资源对应的接口名称:

  • ID2D1HwndRenderTarget 接口代表渲染目标。

  • ID2D1SolidColorBrush 接口代表画刷。

接口可以定义在 MainWindow 类的成员变量中。

ID2D1HwndRenderTarget   *pRenderTarget;
ID2D1SolidColorBrush    *pBrush;

下面是创建这两种资源的代码:

HRESULT MainWindow::CreateGraphicsResources()
{
    HRESULT hr = S_OK;
    if (pRenderTarget == NULL)
    {
        RECT rc;
        GetClientRect(m_hwnd, &rc);

        D2D1_SIZE_U size = D2D1::SizeU(rc.right, rc.bottom);

        hr = pFactory->CreateHwndRenderTarget(
            D2D1::RenderTargetProperties(),
            D2D1::HwndRenderTargetProperties(m_hwnd, size),
            &pRenderTarget);

        if (SUCCEEDED(hr))
        {
            const D2D1_COLOR_F color = D2D1::ColorF(1.0f, 1.0f, 0);
            hr = pRenderTarget->CreateSolidColorBrush(color, &pBrush);

            if (SUCCEEDED(hr))
            {
                CalculateLayout();
            }
        }
    }
    return hr;
}

使用 ID2D1Factory::CreateHwndRenderTarget 工厂函数创建关联窗口的渲染目标。

  • 第一个参数指定所有渲染目标共有的一些设置选项,这里使用 D2D1::RenderTargetProperties 辅助函数返回一些默认选项。

  • 第二个参数指定渲染目标包含的窗口句柄和窗口大小。

  • 第三个参数是要返回的渲染目标的指针。

使用渲染目标的成员函数 ID2D1RenderTarget::CreateSolidColorBrush 创建纯色画刷。这个颜色使用 D2D1_COLOR_F 值来定义。

如果渲染目标已经存在,则CreateGraphicsResources 函数不会做任何逻辑操作,直接返回 S_OK。

3. 椭圆绘制案例

当你的程序创建图形资源以后,就可以进行绘制了。

绘制椭圆

下面是绘制一个椭圆的代码:

void MainWindow::OnPaint()
{
    HRESULT hr = CreateGraphicsResources();
    if (SUCCEEDED(hr))
    {
        PAINTSTRUCT ps;
        BeginPaint(m_hwnd, &ps);

        pRenderTarget->BeginDraw();

        pRenderTarget->Clear( D2D1::ColorF(D2D1::ColorF::SkyBlue) );
        pRenderTarget->FillEllipse(ellipse, pBrush);

        hr = pRenderTarget->EndDraw();
        if (FAILED(hr) || hr == D2DERR_RECREATE_TARGET)
        {
            DiscardGraphicsResources();
        }
        EndPaint(m_hwnd, &ps);
    }
}

程序逻辑很简单:

  • 使用纯色画刷填充背景色。
  • 绘制一个圆。

因为绘制的目标是窗口,所以可以在 WM_PAINT 消息响应函数中执行逻辑。下面就是窗口过程中的具体代码:

LRESULT MainWindow::HandleMessage(UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_PAINT:
        OnPaint();
        return 0;

        // Other messages not shown...
    }

    return DefWindowProc(m_hwnd, uMsg, wParam, lParam);
}

所有的绘图操作都会使用 ID2D1RenderTarget 接口。这个程序的 OnPaint 代码逻辑如下:

  • 调用 ID2D1RenderTarget::BeginDraw 开始绘制。

  • 调用 ID2D1RenderTarget::Clear 方法为渲染目标填充一个纯色。这个颜色使用 D2D1_COLOR_F 结构指定。你可以使用 D2D1::ColorF 初始化结构体。

  • 调用 ID2D1RenderTarget::FillEllipse 函数绘制一个椭圆,使用指定的画刷进行填充颜色。这个椭圆需要指定x轴和y轴的半径,如果二者相同,绘制的结果将是一个圆。

  • 调用 ID2D1RenderTarget::EndDraw 结束绘制。所有的绘制操作必须在 BeginDraw 和 EndDraw 之间。

BeginDraw、Clear、FillEllipse 函数返回的类型为void。如果执行它们的过程中发生错误,错误的具体信息会通过 EndDraw 函数返回。

设备可能会缓冲绘制命令,直到执行 EndDraw 函数后才会开始绘制。你可以强制让绘图命令立即执行,只需要调用 ID2D1RenderTarget::Flush 函数即可。但是这可能会降低程序性能。

4. 处理设备丢失

当程序在运行过程中,图形设备可能因为某些原因而失效。例如,设备显示分辨率被改变,或者用户移除显示适配器(显示器)等等。如果设备丢失,那么渲染目标也会变得无效,顺带着所有的设备相关的资源都会失效。Direct2D 当接收到设备丢失的信号后,会在调用 EndDraw 函数后返回 D2DERR_RECREATE_TARGET 代码。如果你收到这个错误码,你必须重新创建渲染目标以及设备相关资源。

释放资源的简单那代码:

void MainWindow::DiscardGraphicsResources()
{
    SafeRelease(&pRenderTarget);
    SafeRelease(&pBrush);
}

资源创建操作是比较昂贵的操作(创建资源的操作比较耗时,这也是为什么在玩游戏的时候切换窗口造成游戏卡顿的原因),所以不要在 WM_PAINT 消息响应函数中执行。资源被创建后,应该缓存它们的指针,直到设备失效或者不在使用它们为止。

5. Direct2D 渲染循环

不管你将要绘制什么,程序逻辑中都会执行一个类似下面的简单循环逻辑:

  1. 创建设备独立资源。

  2. 渲染场景

    • 检测渲染目标是否存在,如果不存在则创建渲染目标和设备相关资源。

    • 调用 ID2D1RenderTarget::BeginDraw 函数。

    • 发出绘制命令。

    • 调用 ID2D1RenderTarget::EndDraw 函数。

    • 如果 EndDraw 函数返回 D2DERR_RECREATE_TARGET ,则丢弃渲染目标和设备相关资源。

  3. 每当场景需要更新或者重绘的时候,重新返回第2步。

如果渲染目标是一个窗口,当接收到 WM_PAINT 消息的时候就会执行第2步。

这个循环展示了如果处理设备丢失以及丢弃设备相关资源以及重新创建的基本逻辑。