Introduction to DirectWrite

Introduction to DirectWrite

Microsoft has added two interesting new API's to Windows 7: 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. It makes it easy to render paragraphs of text that can contain different formatting, coloring, fonts etc. It supports horizontal and vertical alignments, even vertical centering of a paragraph with multiple lines which was not possible with the old text API, etc. This article will give an introduction to the new DirectWrite API.

DirectWrite Benefits

Let me summarize first a few of the benefits of the DirectWrite API:

  • More advanced anti-aliasing: horizontally this is done using cleartype, vertically this is done using standard anti-aliasing techniques.
  • Subpixel accurate rendering and positioning. This allows for much smoother animation of moving or rotating text. If you would use GDI or GDI+ to draw animated rotated text, you would notice severe artefacts and jumping of pixels. With DirectWrite, the rotation will be very smooth. See example below.
  • Handling of different formatting (bold, italic...), coloring, fonts and so on in a single paragraph.
  • Hit testing support for multi formatted text.
  • Full support for Unicode.
  • Full support for left-to-right or right-to-left languages.
  • Full DPI Aware.
  • Support for advanced typographic features embedded in certain fonts.
  • Support for font files conformant to the OpenType specification version 1.5. Basically, it supports font files with extension TTF, OTF or TTC.
  • Support for application local (= not installed) font files.
  • Independent of any rendering technology. DirectWrite is designed to work well with Direct2D, but you could also use DirectWrite in combination with GDI, GDI+, Direct3D or your own render engine.
  • When used in combination with Direct2D, you get hardware acceleration.

Currently, DirectWrite is being developed for Windows 7. Once Windows 7 ships, DirectWrite will also be made available for Windows Vista. At the moment there are no plans to support Windows XP.

As you can see, it looks like a powerful API. So let's get started with it.

Below you can find a screenshot of the demo application that comes with this article. Everything you see in that screenshot is pretty easy to accomplish with the Direct2D and DirectWrite combination.

Note: Pay special attention to the different ways of how the letter "y" is rendered in the "Fancy Typography Rendering". The way the "y" is rendered depends on where in the word the letter occurs.

Starting With DirectWrite

Before we can start using the DirectWrite and Direct2D features we need to get the latest Microsoft SDK for Windows 7 . See more about this SDK and Visual Studio 2010 Beta 1 in the last section of this article. Once the SDK is installed we can start using it. The first thing we need is to include the Direct2D and DirectWrite header files as follows:

#include <d2d1.h>
#include <dwrite.h>

You also need to link to the Direct2D and DirectWrite libraries:

#pragma comment(lib, "d2d1.lib")
#pragma comment(lib, "dwrite.lib") 

Both Direct2D and DirectWrite are implemented as COM objects and thus we will need to work with quite a few COM interface pointers. To make this easier I will use the _com_ptr_t smart COM interface pointer. To use this we need the following include:

#include <comdef.h>

This smart COM interface pointer is used by using the _COM_SMARTPTR_TYPEDEF macro, for example:

_COM_SMARTPTR_TYPEDEF(IMyInterface, __uuidof(IMyInterface));
IMyInterfacePtr pMyPointer;

The macro creates a type that has the same name as the interface with a suffix of Ptr.

In my code I will use the following helper macro to check the results of function calls.

#ifndef IFR
#define IFR(expr) do {hr = (expr); _ASSERT(SUCCEEDED(hr)); if (FAILED(hr)) return(hr);} while(0)
#endif

Direct2D and DirectWrite work with two kind of resources. There are device indepedent and device dependent resources. The device dependent resources might need to be recreated when for example display settings change.

Factories and Device Independent Resources

Both Direct2D and DirectWrite work with a factory that is used to create other resources. Those factories are device indepedent and stay valid during the execution of the program. The following code creates those two factories:

HRESULT hr = S_OK;
// Create a Direct2D factory.
IFR(D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &g_pD2DFactory));

// Create a DirectWrite factory.
IFR(DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED, __uuidof(IDWriteFactory),
	reinterpret_cast<IUnknown**>(&g_pDWriteFactory)));

Now that we have our factories we can start creating other objects. First lets create a text format object. A text format object is used to layout text and contains among others settings a font, font size and font style.

IFR(g_pDWriteFactory->CreateTextFormat(L"Arial", NULL, DWRITE_FONT_WEIGHT_REGULAR,
	DWRITE_FONT_STYLE_NORMAL, DWRITE_FONT_STRETCH_NORMAL,
	ConvertPointSizeToDIP(12.0f), L"en-us", &g_pTextFormat));

The above text format is created with the font "Arial" and a point size of 12. Note that we have to convert font point sizes into DIP units. A DIP is a Device Independent Pixel unit. 1 DIP = 1/96 inch. And 1 inch = 72 points, so conversion can be done with the following helper function:

FLOAT ConvertPointSizeToDIP(FLOAT points)
{
	return (points/72.0f)*96.0f;
}

The IDWriteTextFormat interface contains methods to setup flow direction, line spacing, paragraph alignment (= vertical alignment), reading direction, text alignment (= horizontal alignment), trimming, word wrapping and so on. For example, use the following to set the text alignment to align the text to the leading edge:

IFR(g_pTextFormat->SetTextAlignment(DWRITE_TEXT_ALIGNMENT_LEADING));

NOTE: Since DirectWrite supports both left-to-right and right-to-left languages, it does not use DWRITE_TEXT_ALIGNMENT_LEFT and DWRITE_TEXT_ALIGNMENT_RIGHT, but instead it uses DWRITE_TEXT_ALIGNMENT_LEADING, DWRITE_TEXT_ALIGNMENT_TRAILING and DWRITE_TEXT_ALIGNMENT_CENTER.

Device Dependent Resources

Now that we have our device independent resources we can start writing a function that will create all device dependent resources. An important device depedent resource is the render target. Since my example will render text to the screen, a HWND Render Target is created as follows:

IFR(g_pD2DFactory->CreateHwndRenderTarget(D2D1::RenderTargetProperties(),
	D2D1::HwndRenderTargetProperties(g_hWnd, size), &g_pRT));

Brushes are also device dependent. As an example, the following creates a solid black brush that will be used as the brush for rendering text unless a piece of text already has another brush attached to it.

IFR(g_pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black),
	&g_pSolidBrush));

Text Layout Object

Now we are ready to start creating objects for the actual text layouting, instances of IDWriteTextLayout. Three text layouts will be created. The first layout will render some English text with some simple formatting. The second layout will render some Arabic text to demonstrate the use of mixed left-to-right and right-to-left paragraphs. The third layout will use special typographic features to render some fancy text including a different text alignment. If you want different text alignment as demonstrated, you need different text layout objects because functions like SetTextAlignment do not accept character ranges and thus are applied to the entire text in the text layout. The same is valid for the text direction. If you want to mix left-to-right and right-to-left text you need a different text layout for each. Other things like font size, typographic features, formatting, coloring etc can be applied to ranges of a specific text layout. The following piece of code demonstrates how to create a text layout:

IFR(g_pDWriteFactory->CreateTextLayout(pStr, _tcslen(pStr), g_pTextFormat,
	sizeRT.width, sizeRT.height, &g_pTextLayout));

It accepts the text that needs to be rendered, a pointer to a text format object used as default formatting and the width and height of the layout rectangle. Once you have a text layout, you can apply formatting to parts of it. The following will put the range starting at character position 28 with a length of 4 characters in bold:

DWRITE_TEXT_RANGE range = {28, 4};
IFR(g_pTextLayout->SetFontWeight(DWRITE_FONT_WEIGHT_BOLD, range));

Some of the more frequently used functions on text layouts (IDWriteTextLayout) are:

  • SetFontFamilyName: sets the name of the font to use for the range. For example:
    IFR(g_pFancyTextLayout->SetFontFamilyName(L"Gabriola", range));
  • SetFontSize: sets the size of the font for the range. For example:
    IFR(g_pFancyTextLayout->SetFontSize(ConvertPointSizeToDIP(36.0f), range));
  • SetFontStyle: sets the style (normal, italic, oblique) of the font for the range. For example:
    IFR(g_pTextLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, range));
  • SetFontWeight: sets whether the font is bold and how much for the range. For example:
    IFR(g_pTextLayout->SetFontWeight(DWRITE_FONT_WEIGHT_BOLD, range));
  • SetStrikethrough: sets whether the font is striked through for the range. For example:
    IFR(g_pTextLayout->SetStrikethrough(TRUE, range));
  • SetTypography: sets typographic features of the font for the range. See the attached demo for an example on how to do this.
  • SetUnderline: sets whether the font is underlined for the range. For example:
    IFR(g_pTextLayout->SetUnderline(TRUE, range));

To change the color of text ranges, use the SetDrawingEffect function. The following example creates a blue brush and then applies it to a range of text.

_COM_SMARTPTR_TYPEDEF(ID2D1SolidColorBrush, __uuidof(ID2D1SolidColorBrush));
ID2D1SolidColorBrushPtr pBrush = NULL;
IFR(g_pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Blue),&pBrush));
DWRITE_TEXT_RANGE range = {115, 4};
IFR(g_pTextLayout->SetDrawingEffect(pBrush, range));
if (pBrush)
	pBrush.Release();

Note: If you are going to use some colors a lot of time, it is better to create those brushes once in the CreateDevDependentResources() function.

The demo contains a paragraph with some Arabic text. This is accomplished by creating a text layout and setting the reading direction to right-to-left, as in:

IFR(g_pDWriteFactory->CreateTextLayout(pArabic, _tcslen(pArabic), g_pTextFormat,
	sizeRT.width, sizeRT.height, &g_pArabicTextLayout));
// Set reading direction to right-to-left
IFR(g_pArabicTextLayout->SetReadingDirection(DWRITE_READING_DIRECTION_RIGHT_TO_LEFT));

The Draw Function

Now that we have setup all device independent and device dependent resources, we can start working on the Draw function that will do the actual rendering. The first thing the Draw function does is calling the CreateDevDependentResources function to make sure that the device dependent resources are still valid. This will usually be a no-operation unless those resources are invalidated and need to be recreated. Drawing of one frame in Direct2D should always start with a call to BeginDraw and end with a call to EndDraw on the render target object as follows:

g_pRT->BeginDraw();
// ... The actual drawing code comes here ...
HRESULT hr = g_pRT->EndDraw();

The next thing we will do is to reset the transformation matrix to the identity matrix, meaning no translation nor rotation. We will also clear the entire background of the frame to white.

g_pRT->SetTransform(D2D1::IdentityMatrix());
g_pRT->Clear(D2D1::ColorF(D2D1::ColorF::White));

We are now ready to render the first piece of text. First we define the upper-left corner of where rendering will start in the frame, this is at position (g_siMargin, g_siMargin). The demo application contains some code to rotate the text based on a timer. So if rotation is enabled, we apply a rotation transformation matrix to the render target. Finally we call the DrawTextLayout function to do the actual rendering of the text.

D2D1_POINT_2F origin = D2D1::Point2F(static_cast<FLOAT>(g_siMargin),
	static_cast<FLOAT>(g_siMargin));
if (g_bRotate)
{
	g_pRT->SetTransform(D2D1::Matrix3x2F::Rotation(g_fAngle,
		D2D1::Point2F(0.0f,0.0f)));
}
g_pRT->DrawTextLayout(origin, g_pTextLayout, g_pSolidBrush);

The next paragraph is the one with the Arabic text. We want this to be positioned exactly below the previously rendered text, so we will first calculate the height of the previously rendered text using GetMetrics:

DWRITE_TEXT_METRICS textMetrics = {0};
IFR(g_pTextLayout->GetMetrics(&textMetrics));
FLOAT fArabicTextTop = textMetrics.top + textMetrics.height;

The actual rendering of the Arabic text is exactly the same as how the first piece of text was rendered, but instead of using g_pTextLayout you use g_pArabicTextLayout.

The last paragraph is the one with the "Fancy" text. Again, first we will calculate where to start rendering this paragraph as follows:

IFR(g_pArabicTextLayout->GetMetrics(&textMetrics));
FLOAT fFancyTypographTop = fArabicTextTop + textMetrics.height;

The "Fancy" text has some special typgraphic features enabled and will use a linear gradient brush instead of a solid brush. We start by setting up the linear gradient brush. For this, we need to know the width of the text. This can be calculated with GetMetrics as follows:

IFR(g_pFancyTextLayout->GetMetrics(&textMetrics));

The values in the textMetrics are used in the following piece of code to setup the properties of the linear gradient brush:

D2D1_LINEAR_GRADIENT_BRUSH_PROPERTIES props;
props.startPoint.x = textMetrics.left;
props.startPoint.y = 0.0f;
props.endPoint.x = textMetrics.left + textMetrics.width;
props.endPoint.y = 0.0f;

A second thing we need for a linear gradient brush is an array of stops. Stops are positions in the gradient for which you specify the color. The position is a value between 0.0f and 1.0f where 0.0f is completely left and 1.0f is complete right. The color of positions in between stops is interpolated. The following example creates a gradient stop collection with 3 stops. The gradient will go from red on the left to blue in the middle to red on the right.

D2D1_GRADIENT_STOP stops[3];
stops[0].color = D2D1::ColorF(D2D1::ColorF::Red);
stops[0].position = 0.0f;
stops[1].color = D2D1::ColorF(D2D1::ColorF::Blue);
stops[1].position = 0.5f;
stops[2].color = D2D1::ColorF(D2D1::ColorF::Red);
stops[2].position = 1.0f;

With the above array of gradient stops, we can now create our Direct2D stop collection object ID2D1GradientStopCollection:

_COM_SMARTPTR_TYPEDEF(ID2D1GradientStopCollection, __uuidof(ID2D1GradientStopCollection));
ID2D1GradientStopCollectionPtr pStopCollection = NULL;
IFR(g_pRT->CreateGradientStopCollection(stops, 3, D2D1_GAMMA_2_2,
	D2D1_EXTEND_MODE_CLAMP, &pStopCollection));

The following step is to create the actual linear gradient brush object ID2D1LinearGradientBrush, assign it to the range of text and release resources:

_COM_SMARTPTR_TYPEDEF(ID2D1LinearGradientBrush, __uuidof(ID2D1LinearGradientBrush));
ID2D1LinearGradientBrushPtr pGradientBrush = NULL;
IFR(g_pRT->CreateLinearGradientBrush(&props, NULL, pStopCollection, &pGradientBrush));
DWRITE_TEXT_RANGE range = {0, 27};
IFR(g_pFancyTextLayout->SetDrawingEffect(pGradientBrush, range));
if (pGradientBrush)
	pGradientBrush.Release();
if (pStopCollection)
	pStopCollection.Release();

After this, rendering happens as usual:

origin.y = fFancyTypographTop;
if (g_bRotate)
{
	g_pRT->SetTransform(D2D1::Matrix3x2F::Rotation(g_fAngle,
		D2D1::Point2F(0.0f,0.0f)));
}
// Draw our text.
g_pRT->DrawTextLayout(origin, g_pFancyTextLayout, g_pSolidBrush);

The last thing in the Draw function is the following:

hr = g_pRT->EndDraw();
if (FAILED(hr))
	ReleaseDevDependentResources();

If the EndDraw call returned an error code, we release device dependent resources to force them to be recreated on the next call to Draw.

Handling WM_PAINT, WM_DISPLAYCHANGE and WM_SIZE

In the window procedure, we handle WM_PAINT and WM_DISPLAYCHANGE messages to call our Draw function:

case WM_PAINT:
case WM_DISPLAYCHANGE:
	hdc = BeginPaint(hWnd, &ps);
	Draw();
	EndPaint(hWnd, &ps);
	break;

We also need to handle WM_SIZE messages to update the size of our render target and our text layouts.

case WM_SIZE:
{
	if (g_pRT)
		g_pRT->Resize(D2D1::SizeU(LOWORD(lParam), HIWORD(lParam)));
	// For sizing the text layouts, ask the Render Target what his
	// size is instead of using the lParam. Asking the Render Target
	// will automatically bring system DPI into account.
	D2D1_SIZE_F sizeRT = g_pRT->GetSize();
	// Take margin into account.
	sizeRT.height -= 2*g_siMargin;
	sizeRT.width -= 2*g_siMargin;
	if (g_pTextLayout)
	{
		g_pTextLayout->SetMaxWidth(sizeRT.width);
		g_pTextLayout->SetMaxHeight(sizeRT.height);
	}
	if (g_pFancyTextLayout)
	{
		g_pFancyTextLayout->SetMaxWidth(sizeRT.width);
		g_pFancyTextLayout->SetMaxHeight(sizeRT.height);
	}
	if (g_pArabicTextLayout)
	{
		g_pArabicTextLayout->SetMaxWidth(sizeRT.width);
		g_pArabicTextLayout->SetMaxHeight(sizeRT.height);
	}
	break;
}

Hit Testing

DirectWrite also comes with support for hit testing which can be used to create links inside rendered text, to implement caret positioning and to implement highlight selection. The attached example contains sample code for embedding a link inside rendered text. To implement this link, we handle the WM_MOUSEMOVE message to perform hit testing on the text layout using the HitTestPoint function:

BOOL bIsTrailingHit = FALSE;
BOOL bIsInside = FALSE;
DWRITE_HIT_TEST_METRICS hit = {0};
HRESULT hr = g_pTextLayout->HitTestPoint(static_cast<FLOAT>(LOWORD(lParam)-g_siMargin),
	static_cast<FLOAT>(HIWORD(lParam)-g_siMargin), &bIsTrailingHit, &bIsInside, &hit);
if (SUCCEEDED(hr) && bIsInside)
{
	// NOTE: link position is hardcoded for demonstration purposes.
	BOOL bOverLink = (hit.textPosition >= 129 && hit.textPosition < 129+11);
	if (bOverLink != g_bOverLink)
	{
		g_bOverLink = bOverLink;
		// Force a recreate of the textlayouts.
		// NOTE: In a real world application, you should just update the formatting
		// of the parts you need instead of recreating the whole layout. This is
		// now just for demonstration purposes.
		CreateTextLayouts();
		InvalidateRect(hWnd, NULL, TRUE);
		MessageBeep(-1);
	}
}

The CreateTextLayouts function will then apply the proper formatting to the link. When the mouse is hovering the link, it will be rendered in blue and underlined.

DPI Awareness

The DirectWrite API is DPI aware, therefor I recommend you to make your DirectWrite application DPI aware by using a manifest. This can be done by making a file with the following contents:

<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3" >
	<asmv3:application>
		<asmv3:windowsSettings xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
			<dpiAware>true</dpiAware>
		</asmv3:windowsSettings>
	</asmv3:application>
</assembly>

Save the file as for example DPIAware.manifest. Next, in Visual Studio, go to the properties of your project, then click on "Manifest Tool", go to "Input and Output" and add the DPIAware.manifest file to the "Additional Manifest Files" setting. When you now recompile your application, you might get the following warning:

DPIAware.manifest : manifest authoring warning 81010002:
	Unrecognized Element "application" in namespace "urn:schemas-microsoft-com:asm.v3".

You can safely ignore this warning. It is just that the manifest tool doesn't yet recognize it but it will work correctly.

Demo Application

The attached example demonstrates all of the above, including the fancy typographic text, Arabic right-to-left text, linear gradient filled text, active hyperlink using hit testing and rotation. To activate rotation go to the Options menu and click on Rotate. All the text will be rotated around the upper-left corner. Pay special attention to the quality of the rendering during rotation. Everything is rendered using sub-pixel perfect positioning and anti-aliasing resulting in no jumps whatsoever during the rotation. If the same would be implemented in plain GDI which can only position text on pixel boundaries, you will see a lot of jumping of the individual characters making it basically useless for these kind of animations. On the other hand, the results obtained using DirectWrite are very smooth.

A few notes on the attached demo application. The source code contains hard coded character positions and text lengths used for formatting and hit testing. Obviously this should not be hard coded in a production quality application. It is done just for demonstration purposes. The demo also uses a few global variables. Again, this is something you should avoid in production quality code.

Requirements

The above explanation and the demo application require Windows 7 RC and the latest Windows 7 RC SDK. There were some breaking changes in the Direct2D and DirectWrite APIs between Windows 7 Beta and Windows 7 RC, so an older version of the SDK will not work properly.

If you want to use this latest SDK with Visual Studio 2010 Beta 1, please read the following information: Using the Windows 7 RC SDK in Visual C++ 2010 Beta 1.

Windows 7 is still in development, so is Direct2D and DirectWrite. Keep this in mind as all the documentation of those APIs is still marked as preliminary and subject to changes.



About the Author

Marc Gregoire

Marc graduated from the Catholic University Leuven, Belgium, with a degree in "Burgerlijk ingenieur in de computer wetenschappen" (equivalent to Master of Science in Engineering in Computer Science) in 2003. In 2004 he got the cum laude degree of Master In Artificial Intelligence at the same university. In 2005 he started working for a big software consultancy company. His main expertise is C/C++ and specifically Microsoft VC++ and the MFC framework. Next to C/C++, he also likes C# and uses PHP for creating webpages. Besides his main interest for Windows development, he also has experience in developing C++ programs running 24x7 on Linux platforms and in developing critical 2G,3G software running on Solaris for big telecom operators.

Downloads

Comments

  • This article is good. Thanks!

    Posted by CBasicNet on 08/17/2010 12:37am

    The demo app crashes. Even if I rebuild from source code, it still crashes on my Windows 7 Home Edition. Maybe you would want to update your source code? Other than that, the sample code is excellent and well-designed, I copied those code to my MFC test app and played with DirectWrite. Thanks!! :)

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Live Event Date: April 22, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Database professionals — whether developers or DBAs — can often save valuable time by learning to get the most from their new or existing productivity tools. Whether you're responsible for managing database projects, performing database health checks and reporting, analyzing code, or measuring software engineering metrics, it's likely you're not taking advantage of some of the lesser-known features of Toad from Dell. Attend this live …

  • Today's competitive marketplace requires the organization to frequently release and deploy applications at the pace of user demands, with reduced cost, risk, and increased quality. This book defines the basics of application release and deployment, and provides best practices for implementation with resources for a deeper dive. Inside you will find: The business and technical drivers behind automated application release and deployment. Evaluation guides for application release and deployment solutions. …

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds