Click here to Skip to main content
15,868,292 members
Articles / Multimedia / DirectX

Rendering Text with Direct2D & DirectWrite

Rate me:
Please Sign up or sign in to vote.
4.94/5 (39 votes)
3 Jan 2015CPOL8 min read 104.6K   2.8K   76   25
Direct2D, DirectWrite, Windows API, C++, std::shared_ptr and more

Updates

The code for Windows Development in C++, working with menus[^] contains significant new features and updates to the library.

Introduction

This is the first article in a series intended to illustrate an approach to safer C++ development made possible by the C++11 standard, while building our code directly on top of the Windows C and COM based APIs.

In the next article, Windows Development in C++, working with menus[^], we explore the Windows API for creating and handling menus, with an eye towards how C++11 enables a safer programming model.

The article is really much more about the programming style made possible by using std::shared_ptr<>, and other smart pointers, than it is about Direct2D and DirectWrite. The library includes a set of classes that wraps the functionality of Direct2D and DirectWrite, adding a few significant features:

  • Errors are converted into exceptions
  • Transparent management of COM interface lifetimes

The demo application implements the same functionality as one of the DirectWrite SDK examples, with a significant reduction in the size of the code.

Now, those of us that develop applications that display 3D content are used to having the power of the GPU at our disposal. While it’s certainly possible to use Direct3D to display 2D content, it’s not something most of us would use to render just a few lines of text, or anything else that can easily be implemented using GDI or GDI+.

Image 1

Starting with Windows Vista Service Pack 2 and Windows 7, we now have a new set of APIs that facilitate 2D rendering using the GPU called Direct2D. At the same time, Microsoft introduced another new API, DirectWrite, supporting text rendering, resolution-independent outline fonts, and full Unicode text and layout support.

While the examples included with the SDK for Direct2D and DirectWrite provide the basics we need to get started with the new APIs, they are somewhat cumbersome, and it’s my hope that you’ll find the approach I’m using somewhat easier to understand.

Currently the code is at a very early stage, meaning there are certainly some rough edges and unfinished parts, but from a design perspective, it’s starting to get interesting.

Wouldn’t you like your wWinMain to look like this:

C++
int APIENTRY wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
{
    auto application = std::make_shared<Application>();
    auto form = std::make_shared<MyForm>();
    auto result = application->Run(form);

    return result;
}

The code relies on the Boost C++ libraries, which can be downloaded from http://www.boost.org/[^], so you need to download and build it, before updating the provided projects with the include and library paths matching your installation.

Code Walkthrough

I’m sure you noticed the auto form = std::make_shared<MyForm>() statement above.

Now, std::make_shared<MyForm>() is a smart way of creating a std::share_ptr<MyForm> smart pointer to an object of the MyForm type; since it’s capable of allocating space both for the housekeeping information required for std::share_ptr<MyForm> and the MyForm object using a single allocation.

std::shared_ptr<>

The std::shared_ptr<> class template stores a pointer to a dynamically allocated object. std::shared_ptr<> guarantees that the object it points to will be deleted when the last std::shared_ptr<> pointing to it is destroyed or reset.

The implementation of std::shared_ptr<> uses reference counting, and cycles of std::shared_ptr<> instances will not be destroyed. If a function holds a std::shared_ptr<> to an object that directly or indirectly holds a std::shared_ptr<> back to the object, the objects use count will be 2, and destruction of the original std::shared_ptr<> will keep the object hanging around with a use count of 1. To avoid these kinds of circular references, you can use std::weak_ptr<> to reference objects back up the object hierarchy.

The MyForm class declaration looks like this:

C++
class MyForm : public Form
{
    graphics::Factory factory;
    graphics::WriteFactory writeFactory;
    graphics::WriteTextFormat textFormat;
    graphics::ControlRenderTarget renderTarget;
    graphics::SolidColorBrush blackBrush;
    float dpiScaleX;
    float dpiScaleY;
    String text;
public:
    typedef Form Base;

    MyForm();
protected:
    virtual void DoOnShown();
    virtual void DoOnDestroy(Message& message);
    virtual void DoOnDisplayChange(Message& message);
    virtual void DoOnPaint(Message& message);
    virtual void DoOnSize(Message& message);
private:
    void UpdateScale( );
};

MyForm is derived from Form, a class that represents a top level window, which is what we need for our example. The graphics::Factory class is a wrapper around the Direct2D ID2D1Factory interface, and graphics::WriteFactory is a wrapper around the DirectWrite IDWriteFactory interface. Both are initialized in the constructor of MyForm:

C++
MyForm::MyForm()
    : Base(),
      factory(D2D1_FACTORY_TYPE_SINGLE_THREADED),
      writeFactory(DWRITE_FACTORY_TYPE_SHARED),
      dpiScaleX(0),dpiScaleY(0),
      text(L"Windows Development in C++, rendering text with Direct2D & DirectWrite")
{
    SetWindowText(text);
    textFormat = writeFactory.CreateTextFormat(L"Plantagenet Cherokee",72);
    textFormat.SetTextAlignment(DWRITE_TEXT_ALIGNMENT_CENTER);
    textFormat.SetParagraphAlignment(DWRITE_PARAGRAPH_ALIGNMENT_CENTER);

    UpdateScale( );
}

Since our application is single threaded and we have full control of how the objects interact, and what state they are in, we create a single threaded ID2D1Factory and a shared IDWriteFactory.

Inside the constructor, we use the writeFactory to create a graphics::WriteTextFormat object. A graphics::WriteTextFormat object describes the format for text and is used when an entire string is to be rendered using the same font size, style, weight, alignment, etc.

We also want our little application to be able to render correctly on high DPI devices, and UpdateScale calculates factors, based on the resolution of the desktop, that are later used to scale the rending rectangle for text output.

C++
void MyForm::UpdateScale( )
{
    factory.GetDesktopDpi(dpiScaleX,dpiScaleY);
    dpiScaleX /= 96.0f;
    dpiScaleY /= 96.0f;
}

At this point, we have a fully initialized MyForm object, which we pass to the Run method of the Application object.

C++
auto result = application->Run(form);

Now we have a running Windows desktop application, and it’s time to look at the 5 virtual methods declared in the MyForm class. These methods override methods declared in the Form class, or in the Control class, the ancestor of the Form class.

The DoOnShown method is only called the first time a form is displayed – and any later minimizing, maximizing, restoring, hiding, showing, or invalidating and repainting will not cause this method to be called again. So it’s a good opportunity to initialize objects that rely on a valid window handle.

C++
void MyForm::DoOnShown()
{
    Base::DoOnShown();

    renderTarget = factory.CreateControlRenderTarget(shared_from_this());
    blackBrush = renderTarget.CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black));
}

renderTarget is a ControlRenderTarget object, which is a wrapper around the Direct2D ID2D1HwndRenderTarget interface, and we use this object to render the text on our DoOnPaint method:

C++
void MyForm::DoOnPaint(Message& message)
{
    Base::DoOnPaint(message);
    ValidateRect();
    RECT rc = GetClientRect();

    renderTarget.BeginDraw();

    renderTarget.SetTransform(D2D1::IdentityMatrix());
    renderTarget.Clear(D2D1::ColorF(D2D1::ColorF::White));
    
    D2D1_RECT_F layoutRect = D2D1::RectF(rc.top * dpiScaleY,rc.left * dpiScaleX,
        (rc.right - rc.left) * dpiScaleX,(rc.bottom - rc.top) * dpiScaleY);
    
    renderTarget.DrawText(text.c_str(),text.length(),textFormat,layoutRect,blackBrush);

    renderTarget.EndDraw();
}

As you see, we call the BeginDraw method of the renderTarget object before issuing drawing commands, and after we’ve finished the drawing, we call the EndDraw method, indicating that drawing is finished.

Direct2D ID2D1HwndRenderTarget objects are double buffered, and drawing commands issued do not appear immediately, as they are performed on an offscreen surface. EndDraw causes the offscreen buffer to be presented onscreen.

Note that we call ValidateRect to tell windows that the entire client area is now valid.

By calling renderTarget.SetTransform(D2D1::IdentityMatrix()); we ensure that no transformation – such as rotation, skewing or scaling – takes place, and Clear draws our beautiful white background. Next, layoutRect is calculated using the scaling factors previously calculated by UpdateScale before calling DrawText to render the text using the textFormat created in the contructor and the black brush created in the DoOnShown method.

As mentioned, the renderTarget uses an offscreen surface, and the size of that surface is set in the DoOnSize method:

C++
void MyForm::DoOnSize(Message& message)
{
    Base::DoOnSize(message);
    if(renderTarget)
    {
        D2D1_SIZE_U size;

        size.width = LOWORD(message.lParam);
        size.height = HIWORD(message.lParam);
        renderTarget.Resize(size);
    }
}

While the DoOnDisplayChange method:

C++
void MyForm::DoOnDisplayChange(Message& message)
{
    UpdateScale( );
    InvalidateRect();
}

allows the application to handle changes to the display configuration. Lastly, the DoOnDestroy method is used to clean up the rendering target when the window closes:

C++
void MyForm::DoOnDestroy(Message& message)
{
    Base::DoOnDestroy(message);
    blackBrush.Reset();
    renderTarget.Reset();
}

Except for a few include statements; we’ve now gone through the complete source code for an application that provides functionality similar to the DirectWrite Simple Hello World Sample, that can be found at this link[^].

Unknown

I’m sure you noticed that there are no calls to Release, but that does not mean that the program does not release the interfaces in an appropriate manner.

Since we are working with DirectX based APIs, it’s useful to have a class that wraps a pointer to the IUnknown interface, and surprisingly I called this wrapper Unknown:

C++
class Unknown
    {
    protected:
        IUnknown* unknown;
    public:
        Unknown();
        explicit Unknown(IUnknown* unknown);
        Unknown(const Unknown& other);
        Unknown(Unknown&& other);
        ~Unknown();
        operator bool() const;
        Unknown& operator = (const Unknown& other);
        Unknown& operator = (Unknown&& other);
        Unknown& Reset(IUnknown* other = nullptr);
    };

It’s pretty much a minimal implementation of a smart pointer to COM based objects, and it’s used as a base class for the various interface wrappers in the harlinn::windows::graphics namespace, so it’s worth looking at the implementation details.

The default constructor does pretty much what one would expect, as it just sets unknown to nullptr:

C++
Unknown()
    : unknown(nullptr)
{}

Then we have a constructor that takes a pointer to an IUnknown:

C++
explicit Unknown(IUnknown* unknown)
    : unknown(unknown)
{}

It’s declared explicit because I don’t want the compiler to automagically generate instances of the class. Please note that the implementation does not call AddRef on the interface.

Next we have the copy constructor, which do call AddRef – otherwise the whole thing would be rather pointless:

C++
Unknown(const Unknown& other)
    : unknown(other.unknown)
{
    if(unknown)
    {
        unknown->AddRef();
    }
}

And then, we have the move constructor:

C++
Unknown(Unknown&& other)
    : unknown(0)
{
    if(other.unknown)
    {
        unknown = other.unknown;
        other.unknown = nullptr;
    }
}

Which copies the pointer managed by the argument, and sets the unknown field of the argument to nullptr, preventing a call to Release from the argument when that object goes out of scope.

C++
~Unknown()
{
    IUnknown* tmp = unknown;
    unknown = nullptr;
    if(tmp)
    {
        tmp->Release();
    }
}

In MyForm::DoOnSize, you saw this test if(renderTarget) which uses this operator:

C++
operator bool() const
{
    return unknown != nullptr;
}

The copy assignment operator looks like this:

C++
Unknown& operator = (const Unknown& other)
{
    if(unknown != other.unknown)
    {
        if(unknown)
        {
            IUnknown* tmp = unknown;
            unknown = nullptr;
            tmp->Release();
        }
        unknown = other.unknown;
        if(unknown)
        {
            unknown->AddRef();
        }
    }
    return *this;
}

while the move assignment operator is implemented like this:

C++
Unknown& operator = (Unknown&& other)
{
    if (this != &other)
    {
        IUnknown* tmp = unknown;
        unknown = nullptr;
        if(tmp)
        {
            tmp->Release();
        }
        unknown = other.unknown;
        other.unknown = nullptr;
    }
    return *this;
}

It’s worth noting that both the copy assignment operator and the move assignment operator guard against self-assignment that would result in a premature call to Release, and the Reset method is implemented similarly:

C++
Unknown& Reset(IUnknown* other = nullptr)
{
    if(unknown != other)
    {
        if(unknown)
        {
            IUnknown* tmp = unknown;
            unknown = nullptr;
            tmp->Release();
        }
        unknown = other;
    }
    return *this;
}

Also note that the Reset method does not call AddRef on the passed interface.

Application

Remember the MyForm::DoOnDestroy method?

C++
void MyForm::DoOnDestroy(Message& message)
{
    Base::DoOnDestroy(message);
    blackBrush.Reset();
    renderTarget.Reset();
}

Perhaps you wondered why we made a call to the DoOnDestroy method of the base class. The Control class implements the DoOnDestroy method like this:

C++
HWIN_EXPORT void Control::DoOnDestroy(Message& message)
{
    OnDestroy(message);
}

Where OnDestroy is not another method, but a signal from the boost::signals2[^] library, declared like this:

C++
signal<void (Message& message)> OnDestroy;

Signals provides functionality that are in many ways similar to .NET events, something that the Application::Run method puts to good use by connecting a lambda expression to the OnDestroy signal:

C++
HWIN_EXPORT int Application::Run
    (std::shared_ptr<Form> mainform, std::shared_ptr<MessageLoop> messageLoop)
{
    if(mainform)
    {
        mainform->OnDestroy.connect( [=](Message& message)
            {
                ::PostQuitMessage(-1);
            });
        mainform->Show();

        int result = messageLoop->Run();
        return result;
    }
    return 0;
}

The lambda expression calls PostQuitMessage, causing the message loop to terminate when the application causes the DoOnDestroy method, usually in response to a WM_DESTROY message, to be called for the argument form only, so the lifetime of the message loop is tied to the lifetime of the window.

Concluding Remarks

You may have noticed that this article isn’t as much about Direct2D and DirectWrite as it is about simplifying Windows C++ development. The demo application has just above a 100 lines of code, and we don’t have to worry about resource leakage, and compared to the original DirectWrite SDK sample application, it should be pretty easy to understand – at least I hope it is.

I gave Unknown a pretty detailed treatment because there seems to some misconceptions about how to implement move constructors and move assignment operators, and I would advise anybody that is really interested in the topic to read Dave Abrahams “RValue References: Moving Forward” series, you’ll find the first article here: Want Speed? Pass by Value[^]

History

  • 30th September, 2012 - Initial post
  • 30th November, 2012 - Library update
  • 20th August, 2014 - More than a few updates and bug-fixes
  • 3rd January, 2015 - A few new classes, some updates and a number of bug-fixes

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect Sea Surveillance AS
Norway Norway
Chief Architect - Sea Surveillance AS.

Specializing in integrated operations and high performance computing solutions.

I’ve been fooling around with computers since the early eighties, I’ve even done work on CP/M and MP/M.

Wrote my first “real” program on a BBC micro model B based on a series in a magazine at that time. It was fun and I got hooked on this thing called programming ...

A few Highlights:

  • High performance application server development
  • Model Driven Architecture and Code generators
  • Real-Time Distributed Solutions
  • C, C++, C#, Java, TSQL, PL/SQL, Delphi, ActionScript, Perl, Rexx
  • Microsoft SQL Server, Oracle RDBMS, IBM DB2, PostGreSQL
  • AMQP, Apache qpid, RabbitMQ, Microsoft Message Queuing, IBM WebSphereMQ, Oracle TuxidoMQ
  • Oracle WebLogic, IBM WebSphere
  • Corba, COM, DCE, WCF
  • AspenTech InfoPlus.21(IP21), OsiSoft PI


More information about what I do for a living can be found at: harlinn.com or LinkedIn

You can contact me at espen@harlinn.no

Comments and Discussions

 
QuestionLoved it, Great modernized version of C++ Win App!! Pin
paulmarley5-Jan-15 15:02
paulmarley5-Jan-15 15:02 
AnswerRe: Loved it, Great modernized version of C++ Win App!! Pin
Espen Harlinn5-Jan-15 15:30
professionalEspen Harlinn5-Jan-15 15:30 
GeneralMy vote of 5 Pin
Michael Haephrati31-Oct-12 20:46
professionalMichael Haephrati31-Oct-12 20:46 
GeneralRe: My vote of 5 Pin
Espen Harlinn1-Nov-12 1:46
professionalEspen Harlinn1-Nov-12 1:46 
GeneralRe: My vote of 5 Pin
Michael Haephrati1-Nov-12 1:50
professionalMichael Haephrati1-Nov-12 1:50 
QuestionMy vote of 5 Pin
AlirezaDehqani29-Oct-12 4:38
AlirezaDehqani29-Oct-12 4:38 
AnswerRe: My vote of 5 Pin
Espen Harlinn29-Oct-12 4:39
professionalEspen Harlinn29-Oct-12 4:39 
GeneralMy vote of 5 Pin
Ștefan-Mihai MOGA15-Oct-12 22:01
professionalȘtefan-Mihai MOGA15-Oct-12 22:01 
GeneralRe: My vote of 5 Pin
Espen Harlinn15-Oct-12 22:41
professionalEspen Harlinn15-Oct-12 22:41 
GeneralMy vote of 5 Pin
CPallini6-Oct-12 11:00
mveCPallini6-Oct-12 11:00 
GeneralRe: My vote of 5 Pin
Espen Harlinn6-Oct-12 11:30
professionalEspen Harlinn6-Oct-12 11:30 
GeneralMy vote of 5 Pin
Wendelius6-Oct-12 8:30
mentorWendelius6-Oct-12 8:30 
GeneralRe: My vote of 5 Pin
Espen Harlinn6-Oct-12 11:29
professionalEspen Harlinn6-Oct-12 11:29 
QuestionWhy not use the smart pointers? Pin
Shao Voon Wong4-Oct-12 22:28
mvaShao Voon Wong4-Oct-12 22:28 
AnswerRe: Why not use the smart pointers? Pin
Espen Harlinn4-Oct-12 22:52
professionalEspen Harlinn4-Oct-12 22:52 
Questionstd::shared_ptr<> vs std::unique_ptr<> Pin
Yiannis Spyridakis1-Oct-12 20:14
Yiannis Spyridakis1-Oct-12 20:14 
AnswerRe: std::shared_ptr vs std::unique_ptr Pin
Espen Harlinn2-Oct-12 1:00
professionalEspen Harlinn2-Oct-12 1:00 
GeneralMy vote of 5 Pin
Kenneth Haugland1-Oct-12 1:43
mvaKenneth Haugland1-Oct-12 1:43 
GeneralRe: My vote of 5 Pin
Espen Harlinn1-Oct-12 1:49
professionalEspen Harlinn1-Oct-12 1:49 
GeneralMy vote of 5 Pin
Slacker0071-Oct-12 0:13
professionalSlacker0071-Oct-12 0:13 
GeneralRe: My vote of 5 Pin
Espen Harlinn1-Oct-12 0:14
professionalEspen Harlinn1-Oct-12 0:14 
GeneralMy vote of 5 Pin
JF201530-Sep-12 18:12
JF201530-Sep-12 18:12 
GeneralRe: My vote of 5 Pin
Espen Harlinn30-Sep-12 22:02
professionalEspen Harlinn30-Sep-12 22:02 
GeneralMy vote of 5 Pin
Nemanja Trifunovic30-Sep-12 15:10
Nemanja Trifunovic30-Sep-12 15:10 
GeneralRe: My vote of 5 Pin
Espen Harlinn30-Sep-12 22:03
professionalEspen Harlinn30-Sep-12 22:03 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.