Direct2D 学习笔记

文章目录

  • Direct2D
    • D2D 是什么
    • D2D 适合谁
    • 开发环境
    • 发布平台
    • 入门
    • 我能找到例子吗
    • 一、第一个 D2D 程序——Hello, Direct2D
      • 1. 工厂
      • 2. 呈现器
      • 3. 渲染
      • 4. 运行结果
    • 二、Direct2D 画图实践——Random Graphics
      • 1. 创建渐变画刷
      • 2. 绘制椭圆
      • 3. Resize 函数
    • 三、PeekMessage——Process Ring
      • 1. 使用 `PeekMessageW` 搭建消息循环
      • 2. 运行结果

Direct2D

D2D 是什么

Direct2D 是一种硬件加速的即时模式二维图形 API,可为二维几何对象、位图和文本提供高性能、高质量的呈现。Direct2D API 可与使用 GDI、GDI+ 或 Direct3D 的现有代码进行交互。(摘自百度百科)

D2D 适合谁

适合我。

想学就学吼啊。支持 GPU 加速吼啊。

开发环境

Visual Studio 2019 Community,安装了适用于桌面的 C++ 开发(仅安装了推荐选项)。

发布平台

至少 Windows 7。推荐 Windows 10。

入门

包含以下头文件:

#include 
#include 

为了能够静态链接,链接以下库:

#pragma comment(lib, "d2d1.lib")

我能找到例子吗

这篇博客的完整源码可以在 Github 上获取!戳我。

一、第一个 D2D 程序——Hello, Direct2D

1. 工厂

几乎所有 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();
}

不过需要注意的是,既然资源是由工厂管理的,那么理所应当地,工厂应该最后释放。

一般来说,一个程序只需要一个工厂,所以我把工厂初始化的工作放在了应用程序初始化的地方,而没有放在窗口初始化的地方。

2. 呈现器

*呈现器(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 查询帮助。已经知道的是,这两个函数有很多默认参数。到目前为止我们全都使用默认参数即可(除了窗口句柄)。

窗口呈现器的内存开销是巨大的,可以通过诊断工具查看内存使用情况。

3. 渲染

对于窗口呈现器,我们需要在 WM_PAINT 中编写代码。它的用法与 BeginPaintEndPaint 类似。首先调用 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 方法之前都没有画到窗口上。

4. 运行结果

Direct2D 学习笔记_第1张图片

二、Direct2D 画图实践——Random Graphics

这一节内容我们主要了解 Render Target 的一点点画图函数和画刷。先看程序的运行效果:
Direct2D 学习笔记_第2张图片

1. 创建渐变画刷

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 中的函数是用于创建简单类型的辅助函数。

2. 绘制椭圆

根据上面的命名思想,我们很容易就能找到绘制椭圆的函数。见下面的绘制代码:

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,可以很容易地了解参数信息。

3. Resize 函数

这里稍微修改了一下 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 方法。

三、PeekMessage——Process Ring

1. 使用 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 版)》。

2. 运行结果

Direct2D 学习笔记_第3张图片

你可能感兴趣的:(Windows)