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+.
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:
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:
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
:
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.
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.
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.
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:
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:
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:
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:
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
:
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
:
Unknown()
: unknown(nullptr)
{}
Then we have a constructor that takes a pointer to an IUnknown
:
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:
Unknown(const Unknown& other)
: unknown(other.unknown)
{
if(unknown)
{
unknown->AddRef();
}
}
And then, we have the move
constructor:
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.
~Unknown()
{
IUnknown* tmp = unknown;
unknown = nullptr;
if(tmp)
{
tmp->Release();
}
}
In MyForm::DoOnSize
, you saw this test if(renderTarget)
which uses this operator:
operator bool() const
{
return unknown != nullptr;
}
The copy assignment operator looks like this:
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:
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:
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?
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:
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:
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:
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