Introduction
Windows 7 has two interesting new API: Direct2D and DirectWrite. Direct2D replaces GDI and GDI+. It can render more accurate results and has support for hardware acceleration on your graphics hardware. DirectWrite is a new API to render text. We will look at how to use DirectWrite to draw text outlines in this article. This article is the result of the assessing DirectWrite for inclusion in the coming version 2 of TextDesigner. The tentative plan for version is to include WPF in addition to C++ GDI+ and C# GDI+. As you can see on the TextDesigner home page that text outline can produce many fantastic text effects.
Source Code
In the normal DirectWrite text rendering, we would need to use a text layout object. However, the text outline in DirectWrite does not require the use of text layout object and its text layout object does not manage text outlines. The downside is we do not have the flexibility of a text layout object to assist us to render the text specifically the way we wanted.
In our use of Direct2D and DirectWrite, we use the _COM_SMARTPTR_TYPEDEF
macro to define the COM smart pointers of _com_ptr_t
type 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));
In addition, we shall make use of a macro, IFR
to call Direct2D and DirectWrite methods.
#ifndef IFR
#define IFR(expr) do {hr = (expr); _ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return(hr);} while(0)
#endif
We shall also make use of a helper method, ConvertPointSizeToDIP
to convert point size to Device Independent Pixel (DIP). Device Independent Pixel is defined as 1/96 of an inch and a point is 1/72 of an inch.
FLOAT CTestDirectWriteDlg::ConvertPointSizeToDIP(FLOAT points)
{
return (points/72.0f)*96.0f;
}
First, we need to create the device independent resources (like Direct2D factory, DirectWrite factory and font face object) first in the our own CreateDevInDependentResources
method. Direct2D factory is used to create the window render target and path geometry object while DirectWrite factory is responsible for loading the font file and creating the font face.
HRESULT CTestDirectWriteDlg::CreateDevInDependentResources()
{
HRESULT hr = S_OK;
IFR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory));
IFR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
reinterpret_cast<IUnknown**>(&m_pDWriteFactory)));
Next, we create our font face object from a collection of fonts in the same font family. The reader may ask why a font family consists of several fonts; The reason is different font may store regular or bold or italic glyphs of the same family. In our case, we only load 1 font file (Ruzicka TypeK.ttf
) using CreateFontFileReference
method. Then we proceed to create the font face with 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, fontFileArray,
0,
DWRITE_FONT_SIMULATIONS_NONE,
&pFontFace
));
After loading the font from file, we can create the glyphs used to render the outlines of the text. What is a glyph? A glyph is a series of lines and curves which form the shape of letter. The curves are usually specified as bezier curves. In the source code, we get the code points of every letters in the string (szOutline
) in order to get the glyph indices. It is best to illustrate the reason we do that with an example: every glyph is stored at the specific index inside the glyph table in the font file. For example, the letter A
has a ASCII value of 65 which is its code point but its glyph may not be stored at index, 65! That's why we need to get its glyph index before we can retrieve the glyph to render A
!
After we retrieved all the glyphs information onto the m_pPathGeometry
, we have no longer use for the code points and glyph indices, so we would delete them before returning from the function.
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);
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;
}
We would release our device independent resources in ReleaseDevInDependentResources
method which should be call at the exit of the application. Our device independent resources consists of geometry sink, path geometry, DirectWrite factory and Direct2D factory. They should be released in the reverse order of their creation.
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();
}
After creating our device independent resources, let's create device dependent resources (like render target and brushes) in our custom CreateDevDependentResources
function. Render target is being used to call the drawing methods. The reason they are device dependent resources because they are created using the GPU resources. We only need 2 brushes; 1 gradient brush for the text body and 1 solid color brush for the text outline.
HRESULT CTestDirectWriteDlg::CreateDevDependentResources()
{
HRESULT hr = S_OK;
if (m_pRT)
return hr;
if (!IsWindowVisible())
return E_FAIL;
RECT rc;
GetClientRect(&rc);
D2D1_SIZE_U size = D2D1::SizeU((rc.right-rc.left), (rc.bottom-rc.top));
IFR(m_pD2DFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
D2D1::HwndRenderTargetProperties(GetSafeHwnd(), size), &m_pRT));
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;
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;
}
We will release our device dependent resources in reverse order of their creation in ReleaseDevDependentResources
.
void CTestDirectWriteDlg::ReleaseDevDependentResources()
{
if (m_pSolidBrushOutline)
m_pSolidBrushOutline.Release();
if(m_pLinearGradientBrush)
m_pLinearGradientBrush.Release();
if (m_pRT)
m_pRT.Release();
}
Now, we can render our path geometry in the OnPaint
handler. We put all our drawing code in between BeginDraw
and EndDraw
. The reason that we need to set the transform to translate the text downwards, is because without the downward translation, the text would appear at too far top which is cut off by the windows title bar.
if(FAILED(CreateDevDependentResources()))
return;
if (m_pRT->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED)
return;
m_pRT->BeginDraw();
m_pRT->SetTransform(D2D1::IdentityMatrix());
m_pRT->Clear(D2D1::ColorF(D2D1::ColorF::White));
m_pRT->SetTransform(D2D1::Matrix3x2F::Translation(10.0f,60.0f));
m_pRT->DrawGeometry(m_pPathGeometry, m_pSolidBrushOutline, 3.0f);
m_pRT->FillGeometry(m_pPathGeometry, m_pLinearGradientBrush);
HRESULT hr = m_pRT->EndDraw();
if(FAILED(hr))
ReleaseDevDependentResources();
Conclusion
One of the requirements for TextDesigner version 2 is the API for all different platforms (C++ GDI+, C# GDI+, C# WPF) should stay similar with minor difference. The good news is it is possible to achieve this requirement. However, DirectWrite portion of TextDesigner will not be a drop-in replacement of C++ GDI+ because their objects (brushes, fontface and etc) are incompatible to be reused in GDI+.
History
- 2012-05-01 : Initial Release