Direct2D 是一种硬件加速的即时模式二维图形 API,可为二维几何对象、位图和文本提供高性能、高质量的呈现。Direct2D API 可与使用 GDI、GDI+ 或 Direct3D 的现有代码进行交互。(摘自百度百科)
适合我。
想学就学吼啊。支持 GPU 加速吼啊。
Visual Studio 2019 Community,安装了适用于桌面的 C++ 开发(仅安装了推荐选项)。
至少 Windows 7。推荐 Windows 10。
包含以下头文件:
#include
#include
为了能够静态链接,链接以下库:
#pragma comment(lib, "d2d1.lib")
这篇博客的完整源码可以在 Github 上获取!戳我。
几乎所有 D2D 的资源都是*工厂(factory)*创建并维护的。它的数据类型叫作 ID2D1Factory
。
ID2D1Factory * pFactory;
只能保存工厂的指针,只能通过 D2D1CreateFactory
函数创建工厂:
int D2DDemo::HelloDirect2D::Main::OnExecute()
{
CreateD2DFactory();
auto window{ std::make_unique() };
window->Create(hInstance, nullptr);
ShowWindow(window->GetHwnd(), SW_SHOW);
UpdateWindow(window->GetHwnd());
return window->MessageLoop();
}
void D2DDemo::HelloDirect2D::Main::CreateD2DFactory()
{
if (FAILED(D2D1CreateFactory(
D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_MULTI_THREADED, &pFactory)))
throw std::runtime_error("Fail to D2D1CreateFactory.");
}
D2D1CreateFactory
有多个重载函数,上面是最简单的一个。其中第一个参数用于指定工厂的类型,工厂分为单线程和多线程两种类型。
类型 | 枚举名 |
---|---|
单线程 | D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_SINGLE_THREADED |
多线程 | D2D1_FACTORY_TYPE::D2D1_FACTORY_TYPE_MULTI_THREADED |
顾名思义,多线程的工厂会帮你确保线程安全,但单线程工厂需要你自己确保线程安全(但显然单线程工厂更灵活)。建议就用多线程工厂。
在程序退出时,需要调用成员函数 Release
释放资源。除了工厂,很多其它类型的资源也需要释放,以后不再强调:
D2DDemo::HelloDirect2D::Main::~Main()
{
if (pFactory) pFactory->Release();
}
不过需要注意的是,既然资源是由工厂管理的,那么理所应当地,工厂应该最后释放。
一般来说,一个程序只需要一个工厂,所以我把工厂初始化的工作放在了应用程序初始化的地方,而没有放在窗口初始化的地方。
*呈现器(render target)*相当于一个缓冲画板。由于是第一个 D2D 程序,资料又比较缺乏,我又很弱,因此就找别人的教程来,用别人教程中的“窗口呈现器”(ID2D1HwndRenderTarget
)为例。
ID2D1HwndRenderTarget * pRenderTarget;
工厂的存活周期应与应用程序的存活周期相同,而窗口呈现器应与窗口的存活周期相同。
void D2DDemo::HelloDirect2D::Window::MainWindow::CreateD2DRenderTarget()
{
if (FAILED(Main::App().pFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(GetHwnd(), D2D1::SizeU(width, height)),
&pRenderTarget)))
throw std::runtime_error("Fail to CreateHwndRenderTarget.");
pRenderTarget->SetDpi(USER_DEFAULT_SCREEN_DPI, USER_DEFAULT_SCREEN_DPI); // 自己处理高 DPI 的情况,需要加上这句代码。
}
void D2DDemo::HelloDirect2D::Window::MainWindow::ReleaseD2DRenderTarget()
{
if (pRenderTarget)
{
pRenderTarget->Release();
pRenderTarget = nullptr;
}
}
必须使用工厂提供的成员函数 CreateHwndRenderTarget
来创建窗口呈现器。它有三个参数,前两个参数应该分别由以下两个函数创建:
类型 | 使用函数 |
---|---|
D2D1_RENDER_TARGET_PROPERTIES |
D2D1::RenderTargetProperties |
D2D1_HWND_RENDER_TARGET_PROPERTIES |
D2D1::HwndRenderTargetProperties |
具体什么意思,现在我也不知道。可以随时在 IDE 中按下 F1 查询帮助。已经知道的是,这两个函数有很多默认参数。到目前为止我们全都使用默认参数即可(除了窗口句柄)。
窗口呈现器的内存开销是巨大的,可以通过诊断工具查看内存使用情况。
对于窗口呈现器,我们需要在 WM_PAINT
中编写代码。它的用法与 BeginPaint
和 EndPaint
类似。首先调用 BeginDraw
方法,绘制完成后,再调用 EndDraw
方法:
LRESULT D2DDemo::HelloDirect2D::Window::MainWindow::WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
HANDLE_MSG(hwnd, WM_CREATE, [this](HWND hwnd, LPCREATESTRUCT lpCreateStruct)->BOOL
{
CreateD2DRenderTarget();
return TRUE;
});
HANDLE_MSG(hwnd, WM_DESTROY, [this](HWND hwnd)->void
{
PostQuitMessage(0);
});
HANDLE_MSG(hwnd, WM_SIZE, [this](HWND hwnd, UINT state, int cx, int cy)->void
{
pRenderTarget->Resize(D2D1::SizeU(cx, cy));
});
HANDLE_MSG(hwnd, WM_PAINT, [this](HWND hwnd)->void
{
pRenderTarget->BeginDraw();
pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
DrawRectangle();
pRenderTarget->EndDraw();
ValidateRect(hwnd, NULL); // note
});
default:
return DefWindowProcW(hwnd, message, wParam, lParam);
}
return 0;
}
void D2DDemo::HelloDirect2D::Window::MainWindow::DrawRectangle()
{
ID2D1SolidColorBrush* brush{};
pRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black), &brush);
pRenderTarget->DrawRectangle(D2D1::RectF(20, 20, width - 20, height - 20), brush);
brush->Release();
}
注意,需要宣布窗口区域有效,否则将不断收到 WM_PAINT
消息。
使用 Direct2D 是不需要双缓冲的,在调用 EndDraw
方法之前都没有画到窗口上。
这一节内容我们主要了解 Render Target 的一点点画图函数和画刷。先看程序的运行效果:
ID2D1LinearGradientBrush* brush{};
void RandomBrush(ID2D1HwndRenderTarget* pRenderTarget)
{
int maxx = GetSystemMetrics(SM_CXSCREEN);
int maxy = GetSystemMetrics(SM_CYSCREEN);
static std::default_random_engine engine;
std::uniform_real_distribution dis(0.0, 1.0);
D2D1_GRADIENT_STOP stops[2]{
D2D1::GradientStop(0, D2D1::ColorF(dis(engine), dis(engine), dis(engine), dis(engine))),
D2D1::GradientStop(1, D2D1::ColorF(dis(engine), dis(engine), dis(engine), dis(engine)))
};
ID2D1GradientStopCollection* collection{};
if (!SUCCEEDED(pRenderTarget->CreateGradientStopCollection(stops, std::size(stops), &collection)))
throw std::runtime_error("Fail to CreateGradientStopCollection.");
if (!SUCCEEDED(pRenderTarget->CreateLinearGradientBrush(D2D1::LinearGradientBrushProperties(D2D1::Point2F(0, 0), D2D1::Point2F(maxx, maxy)),
collection, &brush)))
throw std::runtime_error("Fail to CreateLinearGradientBrush.");
if (collection) // ID2D1GradientStopCollection 创建完 brush 就可以销毁了
{
collection->Release();
collection = nullptr;
}
}
要创建渐变画刷,在 Direct2D 中,需要提供渐变起点、终点和渐变点,渐变点用一个 ID2D1GradientStopCollection
指针指定,而 ID2D1GradientStopCollection
又需要使用 CreateGradientStopCollection
方法创建。
从上面的代码可以看出 D2D 的命名规则:I
开头的类型是需要 Release
的 COM 类型,不是 I
开头的类型是诸如点的简单类型,无需 Release
,命名空间 D2D1
中的函数是用于创建简单类型的辅助函数。
根据上面的命名思想,我们很容易就能找到绘制椭圆的函数。见下面的绘制代码:
void D2DDemo::RandomGraphics::Window::MainWindow::Paint()
{
pRenderTarget->Clear(D2D1::ColorF(D2D1::ColorF::White));
for (const auto& t : elements)
{
auto center = D2D1::Point2F(t.lt.x + t.rb.x >> 1, t.lt.y + t.rb.y >> 1);
int cx = t.rb.x - t.lt.x;
int cy = t.rb.y - t.lt.y;
switch (t.ele)
{
case RandomElement::Element::Ellipse:
{
pRenderTarget->FillEllipse(D2D1::Ellipse(center, cx / 2, cy / 2), t.brush);
break;
}
case RandomElement::Element::Rectangle:
{
pRenderTarget->FillRectangle(D2D1::RectF(t.lt.x, t.lt.y, t.rb.x, t.rb.y), t.brush);
break;
}
default:
break;
}
}
}
利用 IntelliSense,可以很容易地了解参数信息。
这里稍微修改了一下 CreateD2DRenderTarget
函数:
void D2DDemo::RandomGraphics::Window::MainWindow::CreateD2DRenderTarget()
{
if (FAILED(Main::App().pFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(GetHwnd(), D2D1::SizeU(1920, 1080)), // 这里指定了初始大小
&pRenderTarget)))
throw std::runtime_error("Fail to CreateHwndRenderTarget.");
pRenderTarget->SetDpi(USER_DEFAULT_SCREEN_DPI, USER_DEFAULT_SCREEN_DPI);
}
本质上,由于下面代码的存在:
HANDLE_MSG(hwnd, WM_SIZE, [this](HWND hwnd, UINT state, int cx, int cy)->void
{
pRenderTarget->Resize(D2D1::SizeU(cx, cy)); // 窗口刚创建好就会受到这个消息
});
上面的修改就没有任何效果。但是我们可以注释 Resize
这句代码,可以发现图形会随着窗口大小的变化进行缩放。这说明了 D2D 的 HwndRenderTarget
会自动帮我们把绘画内容填充满整个窗口。它甚至会帮助我们处理 DPI 的问题,这也是为什么我们需要调用 SetDpi
方法。
PeekMessageW
搭建消息循环int D2DDemo::ProcessRing::Window::MainWindow::PeekMessageLoop()
{
MSG msg;
while (true)
{
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break;
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
else
OnPaint(GetHwnd());
}
return msg.wParam;
}
具体原理可以参考《Windows 程序设计(第 5 版)》。