首页 每日推荐 UI Composition 技术在 Win32 窗口中绘制文本 - liulun

UI Composition 技术在 Win32 窗口中绘制文本 - liulun

用 Composition 画一行字,本质上就是执行以下任务:

  1. 初始化 D2D 环境
  2. 初始化 DWrite 环境
  3. 用 DWrite 量出这文字有多大
  4. 按这个大小开一块 GPU 纹理(D2D)
  5. 把字画上去(D2D)
  6. 把纹理贴到一个 SpriteVisual 上

在做这些任务之前,我们先把 DWrite 和 D2D 环境准备好

初始化 D2D 环境

以下代码负责初始化 D2D 环境

// Microsoft::WRL::ComPtr<ID3D11Device> d3dDevice;
// Microsoft::WRL::ComPtr<IDXGIFactory2> dxgiFactory;
// Microsoft::WRL::ComPtr<ID2D1Device> d2dDevice;
// Microsoft::WRL::ComPtr<ID2D1DeviceContext> deviceContext;
// Microsoft::WRL::ComPtr<ID2D1Factory1> d2dFactory;

D3D_FEATURE_LEVEL levels[]{ D3D_FEATURE_LEVEL_11_1, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_10_0, D3D_FEATURE_LEVEL_10_1 };
auto count = std::size(levels);
auto hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT, levels, count, D3D11_SDK_VERSION, d3dDevice.ReleaseAndGetAddressOf(), nullptr, nullptr);
if (FAILED(hr)) {
	_ASSERT_EXPR(FALSE, L"D3D11CreateDevice,error");
	return;
}
D2D1_FACTORY_OPTIONS options{};
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, IID_ID2D1Factory1, &options, (void**)&d2dFactory);
if (FAILED(hr)) {
	_ASSERT_EXPR(FALSE, L"D2D1CreateFactory,error");
	return;
}
ComPtr<IDXGIDevice> dxgiDevice;
d3dDevice->QueryInterface(dxgiDevice.ReleaseAndGetAddressOf());
ComPtr<IDXGIAdapter> adapter;
if (SUCCEEDED(dxgiDevice->GetAdapter(adapter.GetAddressOf()))) {
	adapter->GetParent(__uuidof(IDXGIFactory2), reinterpret_cast<void**>(dxgiFactory.ReleaseAndGetAddressOf()));
}
hr = d2dFactory->CreateDevice(dxgiDevice.Get(), d2dDevice.ReleaseAndGetAddressOf());
if (FAILED(hr)) {
	_ASSERT_EXPR(FALSE, L"D2D1CreateFactory CreateDevice,error");
	return;
}
hr = d2dDevice->CreateDeviceContext(D2D1_DEVICE_CONTEXT_OPTIONS_NONE, deviceContext.ReleaseAndGetAddressOf());

这段代码初始化了 Direct3D 11 与 Direct2D 的互操作渲染管线。

它首先创建支持 BGRA 格式的 D3D11 硬件设备,这是两者共享纹理的前提,

然后创建 D2D1Factory1 并通过 DXGI Device 将 D2D 设备绑定到同一个 D3D11 设备上,最终生成 D2D1DeviceContext 作为实际绘图接口。
同时还顺带获取了 IDXGIFactory2,为后续创建交换链做准备。

整段代码就是让 D3D 和 D2D 共用同一块 GPU 资源,实现零拷贝的 3D/2D 混合渲染。

初始化 DWrite 环境

以下代码负责初始化 DWrite 环境

// std::unordered_map<std::wstring, Microsoft::WRL::ComPtr<IDWriteTextFormat>> customFormats;
// Microsoft::WRL::ComPtr<IDWriteInMemoryFontFileLoader> fontLoader;
// Microsoft::WRL::ComPtr<IDWriteFactory5> dwriteFactory;
// Microsoft::WRL::ComPtr<IDWriteTextFormat> baseTextFormat;
// Microsoft::WRL::ComPtr<IDWriteFontCollection1> customFontCollection;

auto hr = DWriteCreateFactory(DWRITE_FACTORY_TYPE_ISOLATED,__uuidof(IDWriteFactory5),reinterpret_cast<::IUnknown**>(dwriteFactory.ReleaseAndGetAddressOf()));
if (FAILED(hr)) {
    _ASSERT_EXPR(FALSE, L"DWriteCreateFactory, error");
    return;
}
hr = dwriteFactory->CreateTextFormat(L"Microsoft YaHei", nullptr,
    DWRITE_FONT_WEIGHT_NORMAL, DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
    12.f, L"zh-CN", baseTextFormat.GetAddressOf());

这段代码初始化了 DirectWrite 文本渲染引擎并创建了一个基础文本格式对象。

首先通过 DWriteCreateFactory 创建一个隔离模式的 IDWriteFactory5 实例,隔离模式确保字体集合独立,不受系统全局状态影响。
随后调用 CreateTextFormat 创建一个 12pt、常规字重的"微软雅黑"中文文本格式对象。

这个 baseTextFormat 可作为后续文本布局和绘制的基准模板,在实际使用时还可按需调整字号、对齐方式等属性。

创建文本布局对象

创建文本布局对象的代码如下所示:

// std::wstring text;
// Microsoft::WRL::ComPtr<IDWriteTextLayout> textLayout;
dwriteFactory->CreateTextLayout(text.data(), (UINT32)text.length(), format, FLT_MAX, FLT_MAX, textLayout.ReleaseAndGetAddressOf());
textLayout->SetFontSize(fontSize * win->dpi, { 0, INT_MAX });
textLayout->SetFontFamilyName(fontFamily.data(), { 0, INT_MAX });

这段代码首先调用 CreateTextLayout 根据传入的字符串、文本格式和无限大的布局尺寸生成 IDWriteTextLayout 实例,表示不对文本进行换行或裁剪约束。
随后通过 SetFontSize 和 SetFontFamilyName 对全文范围({0, INT_MAX})统一设置字号和字体族,其中字号乘以 DPI 实现了高 DPI 屏幕下的自适应缩放。

我们在初始化 DWrite 环境时已经给出了默认字体和字号,现在又覆盖了这些默认属性。(可以自由使用系统内置字体)

测量文本

以下是测量文本的代码

DWRITE_TEXT_METRICS m;
textLayout->GetMetrics(&m);
w = std::ceil(m.width);
h = std::ceil(m.height);

这段代码用于获取文本布局的实际渲染尺寸。

调用 GetMetrics 填充 DWRITE_TEXT_METRICS 结构体,其中包含了文本在指定格式和字体下的精确宽度和高度(含行间距、字距等排版度量)。
随后对宽高分别向上取整(std::ceil),确保分配的像素区域能完整容纳文本,避免因浮点精度截断导致文字边缘被裁剪。

得到的 pxW 和 pxH 通常用于创建匹配大小的渲染目标、纹理或 UI 控件尺寸。

创建渲染表面

以下代码负责创建渲染表面:

// winrt::Windows::UI::Composition::CompositionDrawingSurface surface{ nullptr };
Composition::CompositionGraphicsDevice graphicsDevice{ nullptr };
auto interop = comp.as<ABI::Windows::UI::Composition::ICompositorInterop>();
interop->CreateGraphicsDevice(d2dDevice.Get(), reinterpret_cast<ABI::Windows::UI::Composition::ICompositionGraphicsDevice**>(winrt::put_abi(graphicsDevice)));
surface = graphicsDevice.CreateDrawingSurface(
	winrt::Windows::Foundation::Size{ w, h },
	winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
	winrt::Windows::Graphics::DirectX::DirectXAlphaMode::Premultiplied
);
auto brush = win->compositor.CreateSurfaceBrush(surface);
visual.Brush(brush);

这段代码使用前面文章介绍的 win->compositor 创建一个与 Direct2D 互操作的绘图表面,以便将 D2D 渲染内容合成到 Composition 视觉树中。

  1. 获取互操作接口:通过 comp.as<ICompositorInterop>() 获取 COM 互操作接口,这是连接 WinRT Composition API 与底层 DirectX 资源的桥梁。
  2. 创建图形设备:调用 CreateGraphicsDevice 将现有的 ID2D1Device 包装为 CompositionGraphicsDevice,使 Composition 系统能够复用同一个 D2D 设备和上下文进行绘制。
  3. 创建绘图表面:在返回的 graphicsDevice 上调用 CreateDrawingSurface,创建一个指定宽高、像素格式为 B8G8R8A8UIntNormalized、Alpha 模式为预乘(Premultiplied)的离屏渲染目标。

这个 surface 随后可作为 SpriteVisual 或 ShapeVisual 的画刷源,实现高性能的自定义 2D 内容合成,同时保持与 Composition 动画、特效系统的无缝集成。

最后两行代码将之前创建的 D2D 绘图表面封装为画刷,并将其绑定到目标 Visual 上。由此建立起动态渲染链路,后续对表面的任何重绘都会自动反映在视觉树的最终输出中。

绘制文本

以下为绘制文本的代码:

auto s = surface.as<ABI::Windows::UI::Composition::ICompositionDrawingSurfaceInterop>();
ComPtr<ID2D1DeviceContext> ctx;
POINT offset{};
s->BeginDraw(nullptr, __uuidof(ID2D1DeviceContext), reinterpret_cast<void**>(ctx.GetAddressOf()), &offset);
auto trans = D2D1::Matrix3x2F::Translation((float)offset.x, (float)offset.y);
ctx->SetTransform(trans);
ctx->Clear(0);
ComPtr<ID2D1SolidColorBrush> brush;
ctx->CreateSolidColorBrush(color.getD2DColor(), brush.GetAddressOf());
ctx->DrawTextLayout({ 0.f, 0.f }, textLayout.Get(), brush.Get());
s->EndDraw();

这段代码通过 COM 互操作接口在 CompositionDrawingSurface 上执行了一次完整的 Direct2D 文本绘制流程:

  1. 获取绘图上下文:将 WinRT Surface 转换为 ICompositionDrawingSurfaceInterop,调用 BeginDraw 获取绑定的 ID2D1DeviceContext 及系统返回的像素偏移量(offset),该偏移用于处理 DPI 缩放或表面子区域更新时的坐标对齐。
  2. 设置变换与清空:应用 offset 平移变换确保绘制位置正确;随后以透明色清空表面,因为背景由外层容器负责,此处仅保留文字内容。
  3. 绘制文本并提交:创建指定颜色的实心画刷,从原点 (0,0) 开始绘制预计算好尺寸的 textLayout,最后调用 EndDraw 提交绘制命令,使新内容在下一帧自动合成到视觉树中。

总结一下:

核心思路是通过初始化 D3D11/D2D 共享设备与隔离式 DWrite 环境,精确测量文本尺寸后创建匹配大小的 CompositionDrawingSurface,

再经由 COM 互操作接口获取 D2D 上下文完成文字绘制,

最终将绘图表面封装为画刷并绑定至 SpriteVisual,

从而建立起一条从 GPU 离屏渲染到视觉树动态合成的零拷贝、高 DPI 自适应渲染链路。

单凭上述代码还不足以完成下图的应用,后面的文章我们还会介绍更多内容