1
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, whith 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 include 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:
Collapse | Copy
Code
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.
2
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 laststd::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 originalstd::shared_ptr<>
will
keep the object hanging around with a use count
of 1. To avoid this kind of circular references
you can use std::weak_ptr<>
to
reference objects back up the object hierarchy.
The MyForm
class
declaration looks like this:
Collapse | Copy
Code
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,
andgraphics::WriteFactory
is
a wrapper around the DirectWrite IDWriteFactory
interface.
Both are initialized in the constructor of MyForm
:
Collapse | Copy
Code
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.
Agraphics::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 scale the rending rectangle
for text output.
Collapse | Copy
Code
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.
Collapse | Copy
Code
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 relies on
a valid window handle.
Collapse | Copy
Code
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:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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 athttp://msdn.microsoft.com/en-us/library/dd742738(VS.85).aspx[^]
3
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
:
Collapse | Copy
Code
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:
Collapse | Copy
Code
Unknown()
: unknown(nullptr)
{}
Then we have a constructor that takes a pointer
to an IUnknown:
Collapse | Copy
Code
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:
Collapse | Copy
Code
Unknown(const Unknown& other)
: unknown(other.unknown)
{
if(unknown)
{
unknown->AddRef();
}
}
And then we have the move constructor:
Collapse | Copy
Code
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.
Collapse | Copy
Code
~Unknown()
{
IUnknown* tmp = unknown;
unknown = nullptr;
if(tmp)
{
tmp->Release();
}
}
In MyForm::DoOnSize
you
saw this test if(renderTarget)
which
uses this operator:
Collapse | Copy
Code
operator bool() const
{
return unknown != nullptr;
}
The copy assignment operator looks like this:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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 guards
against self-assignment that
would result in a premature call to Release
,
and the Reset
method
is implemented similarly:
Collapse | Copy
Code
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.
4
Remember the MyForm::DoOnDestroy
method?
Collapse | Copy
Code
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:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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:
Collapse | Copy
Code
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.
News:
1 UCanCode Advance E-XD++
CAD Drawing and Printing Solution