如何用Direct2D高效绘制带轮廓的文本?DrawGlyphRun怎么用?
高效渲染带轮廓效果的Direct2D文本方案
你说得对,DrawText确实在性能和效果灵活性上有局限——要实现高效的带轮廓文本渲染,DrawGlyphRun + FillGlyphRun是最优选择。这个方案不仅性能更优,还能完全控制文本的填充、轮廓细节,而且不需要依赖ATL,也不用指定字体文件路径,和你之前用CreateTextFormat的工作流程完全兼容。
核心思路
DrawText是封装好的高层API,内部会做很多额外的排版和渲染逻辑;而DrawGlyphRun是底层API,直接操作字形(Glyph)数据,能跳过冗余步骤提升性能。要实现轮廓效果,我们只需要:
- 用
IDWriteTextLayout把字符串转换成可直接渲染的字形序列(DWRITE_GLYPH_RUN)——这一步可以复用你已有的IDWriteTextFormat,不需要额外指定字体路径。 - 先绘制文本的轮廓(用
DrawGlyphRun并设置线宽),再绘制文本的填充(用FillGlyphRun),通过分层渲染实现轮廓效果。
具体实现步骤
1. 准备文本布局对象
首先用你已有的IDWriteTextFormat创建IDWriteTextLayout,它会帮我们完成文本的排版计算,生成对应的字形序列:
IDWriteTextLayout* textLayout = nullptr; HRESULT hr = m_WriteFactory->CreateTextLayout( wtext, wcslen(wtext), d2d_text_format, // 复用你之前创建的文本格式 800.0f, // 布局宽度,和你之前DrawText的Rect宽度一致 600.0f, // 布局高度,和你之前DrawText的Rect高度一致 &textLayout ); if (FAILED(hr)) { /* 这里添加错误处理逻辑 */ }
2. 收集字形序列(GlyphRun)
我们需要从IDWriteTextLayout中提取所有字形信息,这里可以实现一个简单的IDWriteTextRenderer子类来收集数据:
class GlyphRunCollector : public IDWriteTextRenderer { public: std::vector<std::tuple<DWRITE_GLYPH_RUN, D2D1_POINT_2F>> glyphRuns; // IUnknown接口实现 HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override { if (riid == __uuidof(IDWriteTextRenderer) || riid == __uuidof(IUnknown)) { *ppvObject = this; AddRef(); return S_OK; } *ppvObject = nullptr; return E_NOINTERFACE; } ULONG STDMETHODCALLTYPE AddRef() override { return InterlockedIncrement(&m_refCount); } ULONG STDMETHODCALLTYPE Release() override { ULONG count = InterlockedDecrement(&m_refCount); if (count == 0) delete this; return count; } // 核心方法:收集GlyphRun和位置信息 HRESULT STDMETHODCALLTYPE DrawGlyphRun( void* clientDrawingContext, FLOAT baselineOriginX, FLOAT baselineOriginY, DWRITE_MEASURING_MODE measuringMode, DWRITE_GLYPH_RUN const* glyphRun, DWRITE_GLYPH_RUN_DESCRIPTION const* glyphRunDescription, IUnknown* clientDrawingEffect ) override { glyphRuns.emplace_back(*glyphRun, D2D1::Point2F(baselineOriginX, baselineOriginY)); return S_OK; } // 其他未使用的接口方法直接返回S_OK HRESULT STDMETHODCALLTYPE DrawUnderline(...) override { return S_OK; } HRESULT STDMETHODCALLTYPE DrawStrikethrough(...) override { return S_OK; } HRESULT STDMETHODCALLTYPE DrawInlineObject(...) override { return S_OK; } HRESULT STDMETHODCALLTYPE IsPixelSnappingDisabled(...) override { return S_OK; } HRESULT STDMETHODCALLTYPE GetCurrentTransform(...) override { return S_OK; } HRESULT STDMETHODCALLTYPE GetPixelsPerDip(...) override { return S_OK; } private: LONG m_refCount = 0; };
然后用这个收集器提取字形数据:
GlyphRunCollector collector; textLayout->Draw( nullptr, &collector, x, // 文本起始X坐标,和你之前DrawText的x参数一致 y // 文本起始Y坐标,和你之前DrawText的y参数一致 );
3. 绘制带轮廓的文本
创建填充和轮廓画刷,然后先绘制轮廓、再绘制填充(保证填充覆盖轮廓内部,得到正确的视觉效果):
// 创建画刷 ID2D1SolidColorBrush* fillBrush = nullptr; ID2D1SolidColorBrush* outlineBrush = nullptr; m_RenderTarget->CreateSolidColorBrush(D2D1::ColorF(color.rBase(), color.gBase(), color.bBase(), color.aBase()), &fillBrush); m_RenderTarget->CreateSolidColorBrush(D2D1::ColorF(0.0f, 0.0f, 0.0f), &outlineBrush); // 黑色轮廓,可自定义颜色 // 开始绘制 m_RenderTarget->BeginDraw(); m_RenderTarget->SetStrokeWidth(2.0f); // 设置轮廓线宽,单位是DIP,可调整 for (const auto& entry : collector.glyphRuns) { const auto& glyphRun = std::get<0>(entry); const auto& origin = std::get<1>(entry); // 先绘制轮廓 m_RenderTarget->DrawGlyphRun( origin, &glyphRun, outlineBrush, DWRITE_MEASURING_MODE_NATURAL ); // 再绘制填充 m_RenderTarget->FillGlyphRun( origin, &glyphRun, fillBrush, DWRITE_MEASURING_MODE_NATURAL ); } HRESULT drawHr = m_RenderTarget->EndDraw(); if (FAILED(drawHr)) { /* 绘制错误处理 */ }
4. 资源清理
别忘了释放所有创建的COM对象,避免内存泄漏:
fillBrush->Release(); outlineBrush->Release(); textLayout->Release(); d2d_text_format->Release(); // 如果你之前创建的d2d_color不再使用,也需要释放
关键优势
- 性能更高:直接操作字形数据,跳过
DrawText的冗余封装,适合大量文本渲染场景。 - 效果可控:可以自由调整轮廓线宽、颜色,甚至实现多层轮廓、渐变填充等高级效果。
- 兼容原有流程:完全复用你之前的
IDWriteTextFormat,不需要指定字体文件路径,只需要字体系列名称即可。
内容的提问来源于stack exchange,提问作者user14173351
相关产品推荐
相关产品推荐

