Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / VC10.0

Outline Text With DirectWrite

4.83/5 (18 votes)
12 Apr 2016Ms-PL4 min read 56.3K   2K  
Draw text outline using DirectWrite

DirectWrite Demo

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.

C++
// 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));

In addition, we shall make use of a macro, IFR to call Direct2D and DirectWrite methods.

C++
#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.

C++
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.  

C++
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)));

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.

C++
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
        ));

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.

C++
    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;
}

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.

C++
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.

C++
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;
}

We will release our device dependent resources in reverse order of their creation in ReleaseDevDependentResources.

C++
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.

C++
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();

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

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)