Direct2D
D2D 是什么
Direct2D 是一种硬件加速的即时模式二维图形 API,可为二维几何对象、位图和文本提供高性能、高质量的呈现。Direct2D API 可与使用 GDI、GDI+ 或 Direct3D 的现有代码进行交互。(摘自百度百科)
D2D 适合谁
适合我。
想学就学吼啊。支持 GPU 加速吼啊。
开发环境
Visual Studio 2017 Community,安装了适用于桌面的 C++ 开发(仅安装了推荐选项)。
发布平台
至少 Windows 7。推荐 Windows 10。
入门
包含以下头文件:
#include <d2d1.h>
为了能够静态链接,链接以下库:
#pragma comment(lib, "d2d1.lib")
一、第一个 D2D 程序
1. 工厂
几乎所有 D2D 的资源都是工厂(factory)创建并维护的。它的数据类型叫作 ID2D1Factory
。
ID2D1Factory * pFactory;
只能保存工厂的指针,只能通过 D2D1CreateFactory
函数创建工厂:
OILearner::OILearner() : TApplication(L"OI Learner", L"6D0741A6-057B-4F88-A42B-FFA2280C4988")
{
if (FAILED(D2D1CreateFactory(
D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_MULTI_THREADED, &pFactory)))
throw;
}
(由于是边开发边学,我又很懒,因此我直接把我项目里的代码拷贝过来了。相信大家能够看出来哪些是关键代码)
D2D1CreateFactory
有多个重载函数,上面是最简单的一个。其中第一个参数用于指定工厂的类型,工厂分为单线程和多线程两种类型。
类型 | 枚举名 |
---|---|
单线程 | D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_SINGLE_THREADED |
多线程 | D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_MULTI_THREADED |
顾名思义,多线程的工厂会帮你确保线程安全,但单线程工厂需要你自己确保线程安全(但显然单线程工厂更灵活)。建议就用多线程工厂。
在程序退出时,需要调用成员函数 Release
释放资源。除了工厂,很多其它类型的资源也需要释放,以后不再强调:
OILearner::~OILearner()
{
#define SAFE_RELEASE(obj) if (obj) obj->Release(), obj = NULL
SAFE_RELEASE(pFactory);
#undef SAFE_RELEASE
}
不过需要注意的是,既然资源是由工厂管理的,那么理所应当地,工厂应该最后释放。
一般来说,一个程序只需要一个工厂,所以我把工厂初始化的工作放在了应用程序初始化的地方,而没有放在窗口初始化的地方。
2. 呈现器
呈现器(render target)相当于一个缓冲画板。由于是第一个 D2D 程序,资料又比较缺乏,我又很弱,因此就找别人的教程来,用别人教程中的“窗口呈现器”(ID2D1HwndRenderTarget
)为例。
ID2D1HwndRenderTarget * pRenderTarget;
工厂的存活周期应与应用程序的存活周期相同,而窗口呈现器应与窗口的存活周期相同。
BOOL MainWindow::OnCreate(HWND hwnd, LPCREATESTRUCT lpCreateStruct)
{
// 移动窗口位置
auto cxS = GetSystemMetrics(SM_CXSCREEN);
auto cyS = GetSystemMetrics(SM_CYSCREEN);
int iLeft = (cxS - cxMin) / 2;
int iTop = (cyS - cyMin) / 2;
MoveWindow(hwnd, iLeft, iTop, cxMin, cyMin, TRUE);
// 设置图标
SendMessage(hwnd, WM_SETICON, NULL, (LPARAM)app.GetIconLarge());
// 设置标题
SetWindowTextW(hwnd, app.GetApplicationName().c_str());
// 创建呈现器
if (FAILED(app.pFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(hwnd, D2D1::SizeU(cxMin, cyMin)),
&pRenderTarget)))
throw;
return TRUE;
}
VOID MainWindow::OnDestroy(HWND hwnd)
{
if (pRenderTarget)
{
pRenderTarget->Release();
pRenderTarget = NULL;
}
PostQuitMessage(0);
}
必须使用工厂提供的成员函数 CreateHwndRenderTarget
来创建窗口呈现器。它有三个参数,前两个参数应该分别由以下两个函数创建:
类型 | 使用函数 |
---|---|
D2D1_RENDER_TARGET_PROPERTIES |
D2D1::RenderTargetProperties |
D2D1_HWND_RENDER_TARGET_PROPERTIES |
D2D1::HwndRenderTargetProperties |
具体什么意思,现在我也不知道。可以随时在 IDE 中按下 F1 查询帮助。已经知道的是,这两个函数有很多默认参数。到目前为止我们全都使用默认参数即可(除了窗口句柄)。
窗口呈现器的内存开销是巨大的,可以通过诊断工具查看内存使用情况。
3. 渲染
对于窗口呈现器,我们需要在 WM_PAINT
中编写代码。它的用法与 BeginPaint
和 EndPaint
类似:
VOID MainWindow::OnPaint(HWND hwnd)
{
pRenderTarget->BeginDraw();
pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
ID2D1SolidColorBrush * brush;
pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &brush);
pRenderTarget->DrawRectangle(D2D1::RectF(20, 20, iWidth - 20, iHeight - 20), brush);
brush->Release();
pRenderTarget->EndDraw();
ValidateRect(hwnd, NULL); // note
}
VOID MainWindow::OnSize(HWND hwnd, UINT state, int cx, int cy)
{
iWidth = cx;
iHeight = cy;
if (pRenderTarget)
pRenderTarget->Resize(D2D1::SizeU(cx, cy));
}
注意,需要宣布窗口区域有效,否则将不断收到 OnPaint
消息。再次提醒,窗口呈现器的内存开销是巨大的。
4. 运行结果
(已在程序清单中将 DPI 识别等级修改为“高 DPI 识别”)
在 100% 的缩放比例下:
在 125% 的缩放比例下:
WTF?
原来 D2D 需要你在绘图时忽视 DPI。在你设置 DPI 识别为“高 DPI 识别”后,D2D 会帮你自动缩放(至少对窗口呈现器来说是如此)。仔细研究一下窗口呈现器的原理,发现它的绘图大致分为两步(不准确,但足以让人理解为什么会这样):
- 在缓存中根据给定的 Size 绘图。如果 DPI 大于 96,将 Size 按比例放大,将绘图指令也按比例放大;
- 调用
EndDraw
后,绘制到窗口上。将缓存中的内容缩放成客户区大小。
换句话说,D2D 有以下特征:
- 是双缓冲;
- 对固定分辨率支持十分良好;
对一般的 GUI 很不友好。
也就是说如果用 D2D 写一般的界面,不仅资源占用高,而且对高 DPI 的支持很麻烦。所以第一个程序写下来,我就打消了继续的念头……