使用 DirectWrite 绘制文本轮廓






4.83/5 (18投票s)
使用 DirectWrite 绘制文本轮廓
引言
Windows 7 提供了两个有趣的 API:Direct2D 和 DirectWrite。Direct2D 取代了 GDI 和 GDI+。它可以渲染更精确的结果,并支持图形硬件的硬件加速。DirectWrite 是一个渲染文本的新 API。在本文中,我们将研究如何使用 DirectWrite 绘制文本描边。本文是对 DirectWrite 进行评估,以决定是否将其包含在即将推出的 TextDesigner 版本 2 中的结果。版本 2 的初步计划是在 C++ GDI+ 和 C# GDI+ 的基础上包含 WPF。正如您在 TextDesigner 主页上看到的,文本描边可以产生许多出色的文本效果。
源代码
在正常的 DirectWrite 文本渲染中,我们需要使用一个文本布局对象。然而,DirectWrite 中的文本描边不需要使用文本布局对象,并且文本布局对象也不管理文本描边。缺点是我们无法像使用文本布局对象那样灵活地专门按照我们想要的方式来渲染文本。
在使用 Direct2D 和 DirectWrite 时,我们使用 _COM_SMARTPTR_TYPEDEF
宏来为 Direct2D 和 DirectWrite 接口定义 _com_ptr_t
类型的 COM 智能指针。
// Define smartpointers types for Direct2D and DirectWrite interfaces.
_COM_SMARTPTR_TYPEDEF(ID2D1Factory, __uuidof(ID2D1Factory));
_COM_SMARTPTR_TYPEDEF(ID2D1HwndRenderTarget, __uuidof(ID2D1HwndRenderTarget));
_COM_SMARTPTR_TYPEDEF(ID2D1SolidColorBrush, __uuidof(ID2D1SolidColorBrush));
_COM_SMARTPTR_TYPEDEF(ID2D1GradientStopCollection, __uuidof(ID2D1GradientStopCollection));
_COM_SMARTPTR_TYPEDEF(ID2D1LinearGradientBrush, __uuidof(ID2D1LinearGradientBrush));
_COM_SMARTPTR_TYPEDEF(IDWriteFactory, __uuidof(IDWriteFactory));
_COM_SMARTPTR_TYPEDEF(IDWriteFontFace, __uuidof(IDWriteFontFace));
_COM_SMARTPTR_TYPEDEF(IDWriteFontFile, __uuidof(IDWriteFontFile));
_COM_SMARTPTR_TYPEDEF(ID2D1SimplifiedGeometrySink, __uuidof(ID2D1SimplifiedGeometrySink));
_COM_SMARTPTR_TYPEDEF(ID2D1PathGeometry, __uuidof(ID2D1PathGeometry));
_COM_SMARTPTR_TYPEDEF(ID2D1LinearGradientBrush, __uuidof(ID2D1LinearGradientBrush));
_COM_SMARTPTR_TYPEDEF(ID2D1GradientStopCollection, __uuidof(ID2D1GradientStopCollection));
此外,我们还将使用一个宏 IFR
来调用 Direct2D 和 DirectWrite 方法。
#ifndef IFR
#define IFR(expr) do {hr = (expr); _ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return(hr);} while(0)
#endif
我们还将使用一个帮助方法 ConvertPointSizeToDIP
来将点大小转换为设备无关像素 (DIP)。设备无关像素定义为一英寸的 1/96,而一个点是 1/72 英寸。
FLOAT CTestDirectWriteDlg::ConvertPointSizeToDIP(FLOAT points)
{
return (points/72.0f)*96.0f;
}
首先,我们需要在我们自己的 CreateDevInDependentResources
方法中创建设备无关资源(如 Direct2D 工厂、DirectWrite 工厂和字体面对象)。Direct2D 工厂用于创建窗口渲染目标和路径几何图形对象,而 DirectWrite 工厂负责加载字体文件并创建字体面。
HRESULT CTestDirectWriteDlg::CreateDevInDependentResources()
{
HRESULT hr = S_OK;
// Create a Direct2D factory.
IFR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory));
// Create a DirectWrite factory.
IFR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
reinterpret_cast<IUnknown**>(&m_pDWriteFactory)));
接下来,我们从同一字体系列中的字体集合中创建字体面对象。读者可能会问为什么一个字体系列包含几种字体;原因是不同的字体可能存储同一字体的常规、粗体或斜体字形。在我们的例子中,我们只加载一个字体文件(Ruzicka TypeK.ttf
)使用 CreateFontFileReference
方法。然后,我们继续使用 CreateFontFace
创建字体面。
IDWriteFontFacePtr pFontFace = NULL;
IDWriteFontFilePtr pFontFiles = NULL;
if (SUCCEEDED(hr))
{
CString strPath;
TCHAR* pstrExePath = strPath.GetBuffer (MAX_PATH);
::GetModuleFileName (0, pstrExePath, MAX_PATH);
strPath.ReleaseBuffer ();
strPath = strPath.Left(strPath.ReverseFind(L'\\')+1);
strPath += L"Ruzicka TypeK.ttf";
hr = m_pDWriteFactory->CreateFontFileReference(
strPath,
NULL,
&pFontFiles);
}
IDWriteFontFile* fontFileArray[] = {pFontFiles};
if(pFontFiles==NULL)
{
MessageBox(L"No font file is found at executable folder", L"Error");
return E_FAIL;
}
IFR(m_pDWriteFactory->CreateFontFace(
DWRITE_FONT_FACE_TYPE_TRUETYPE,
1, // file count
fontFileArray,
0,
DWRITE_FONT_SIMULATIONS_NONE,
&pFontFace
));
从文件加载字体后,我们可以创建用于渲染文本描边的字形。什么是字形?字形是由线条和曲线组成的一个字母的形状。曲线通常指定为贝塞尔曲线。在源代码中,我们获取字符串 (szOutline
) 中每个字母的代码点,以便获取字形索引。最好用一个例子来说明我们这样做的原因:每个字形都存储在字体文件字形表中的特定索引处。例如,字母 A
的 ASCII 值为 65,这是它的代码点,但它的字形可能不存储在索引 65 处!这就是为什么我们需要在检索字形以渲染 A
之前获取其字形索引!
将所有字形信息检索到 m_pPathGeometry
后,我们就再也不需要代码点和字形索引了,所以在返回函数之前我们会删除它们。
UINT* pCodePoints = new UINT[szOutline.GetLength()];
UINT16* pGlyphIndices = new UINT16[szOutline.GetLength()];
ZeroMemory(pCodePoints, sizeof(UINT) * szOutline.GetLength());
ZeroMemory(pGlyphIndices, sizeof(UINT16) * szOutline.GetLength());
for(int i=0; i<szOutline.GetLength(); ++i)
{
pCodePoints[i] = szOutline.GetAt(i);
}
pFontFace->GetGlyphIndicesW(pCodePoints, szOutline.GetLength(), pGlyphIndices);
//Create the path geometry
IFR(m_pD2DFactory->CreatePathGeometry(&m_pPathGeometry));
IFR(m_pPathGeometry->Open((ID2D1GeometrySink**)&m_pGeometrySink));
IFR(pFontFace->GetGlyphRunOutline(
ConvertPointSizeToDIP(48.0f),
pGlyphIndices,
NULL,
NULL,
szOutline.GetLength(),
FALSE,
FALSE,
m_pGeometrySink));
IFR(m_pGeometrySink->Close());
if(pCodePoints)
{
delete [] pCodePoints;
pCodePoints = NULL;
}
if(pGlyphIndices)
{
delete [] pGlyphIndices;
pGlyphIndices = NULL;
}
return hr;
}
我们将在 ReleaseDevInDependentResources
方法中释放我们的设备无关资源,该方法应在应用程序退出时调用。我们的设备无关资源包括几何图形接收器、路径几何图形、DirectWrite 工厂和 Direct2D 工厂。它们应该按照创建的相反顺序释放。
void CTestDirectWriteDlg::ReleaseDevInDependentResources()
{
if (m_pGeometrySink)
m_pGeometrySink.Release();
if (m_pPathGeometry)
m_pPathGeometry.Release();
if (m_pDWriteFactory)
m_pDWriteFactory.Release();
if (m_pD2DFactory)
m_pD2DFactory.Release();
}
创建我们的设备无关资源后,让我们在自定义 CreateDevDependentResources
函数中创建设备相关资源(如渲染目标和画笔)。渲染目标用于调用绘图方法。它们是设备相关资源,因为它们是使用 GPU 资源创建的。我们只需要 2 个画笔;1 个渐变画笔用于文本主体,1 个纯色画笔用于文本描边。
HRESULT CTestDirectWriteDlg::CreateDevDependentResources()
{
HRESULT hr = S_OK;
// Only create device dependent resource if not already created.
if (m_pRT)
return hr;
if (!IsWindowVisible())
return E_FAIL;
// Determine size of the window to render to.
RECT rc;
GetClientRect(&rc);
D2D1_SIZE_U size = D2D1::SizeU((rc.right-rc.left), (rc.bottom-rc.top));
// Create a device dependent Direct2D render target.
IFR(m_pD2DFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(GetSafeHwnd(), size), &m_pRT));
// Create an array of gradient stops to put in the gradient stop
// collection that will be used in the gradient brush.
ID2D1GradientStopCollectionPtr pGradientStops = NULL;
D2D1_GRADIENT_STOP gradientStops[3];
gradientStops[0].color = D2D1::ColorF(D2D1::ColorF::Blue, 1);
gradientStops[0].position = 0.0f;
gradientStops[1].color = D2D1::ColorF(D2D1::ColorF::Purple, 1);
gradientStops[1].position = 0.5f;
gradientStops[2].color = D2D1::ColorF(D2D1::ColorF::Red, 1);
gradientStops[2].position = 1.0f;
// Create the ID2D1GradientStopCollection from a previously
// declared array of D2D1_GRADIENT_STOP structs.
IFR(m_pRT->CreateGradientStopCollection(
gradientStops,
3,
D2D1_GAMMA_2_2,
D2D1_EXTEND_MODE_CLAMP,
&pGradientStops
));
IFR(m_pRT->CreateLinearGradientBrush(
D2D1::LinearGradientBrushProperties(
D2D1::Point2F(0.0, -30.0),
D2D1::Point2F(0.0, 0.0)),
pGradientStops,
&m_pLinearGradientBrush
));
IFR(m_pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Plum),
&m_pSolidBrushOutline
));
return hr;
}
我们将在 ReleaseDevDependentResources
中按照创建的相反顺序释放我们的设备相关资源。
void CTestDirectWriteDlg::ReleaseDevDependentResources()
{
if (m_pSolidBrushOutline)
m_pSolidBrushOutline.Release();
if(m_pLinearGradientBrush)
m_pLinearGradientBrush.Release();
if (m_pRT)
m_pRT.Release();
}
现在,我们可以在 OnPaint
处理程序中渲染我们的路径几何图形。我们将所有绘图代码放在 BeginDraw
和 EndDraw
之间。需要设置变换以将文本向下平移的原因是,如果没有向下平移,文本将显得太靠上,被窗口标题栏遮挡。
if(FAILED(CreateDevDependentResources()))
return;
if (m_pRT->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED)
return;
// Start the drawing cycle
m_pRT->BeginDraw();
// First make sure the transformation is set to the identity transformation.
m_pRT->SetTransform(D2D1::IdentityMatrix());
// Clear the background of the window with a white color.
m_pRT->Clear(D2D1::ColorF(D2D1::ColorF::White));
// shift the text down
m_pRT->SetTransform(D2D1::Matrix3x2F::Translation(10.0f,60.0f));
// Draw text outline
m_pRT->DrawGeometry(m_pPathGeometry, m_pSolidBrushOutline, 3.0f);
// Draw text body
m_pRT->FillGeometry(m_pPathGeometry, m_pLinearGradientBrush);
HRESULT hr = m_pRT->EndDraw();
if(FAILED(hr))
ReleaseDevDependentResources();
结论
TextDesigner 版本 2 的要求之一是所有不同平台 (C++ GDI+, C# GDI+, C# WPF) 的 API 应该保持相似,只有细微差别。好消息是,可以实现这个要求。但是,TextDesigner 的 DirectWrite 部分将无法直接替换 C++ GDI+,因为它们的(画笔、字体面等)对象不兼容,无法在 GDI+ 中重用。
历史
- 2012-05-01:初次发布