Share via



October 2013

Volume 28 Number 10

DirectX Factor - Text Formatting and Scrolling with DirectWrite

By Charles Petzold

Charles PetzoldThe display of text in computer graphics has always been rather awkward. It would be great' to display text with as much ease as other two-­dimensional graphics, such as geometries and bitmaps, yet text comes with hundreds of years of baggage and some very definite needs—the crucial requirement of readability, for example.

In recognition of the special nature of text within graphics, DirectX splits the job of working with text into two major subsystems, Direct2D and DirectWrite. The ID2D1RenderTarget interface declares the methods for displaying text, along with other 2D graphics, while interfaces beginning with IDWrite help prepare the text for display.

At the border between graphics and text are some interesting techniques, such as obtaining outlines of text characters, or implementing the IDWriteTextRenderer interface for intercepting and manipulating text on its way to the display. But a necessary preliminary is a solid understanding of basic text formatting—in other words, the display of text that’s intended to be read rather than admired with aesthetic rapture.

The Simplest Text Output

The most fundamental text display interface is IDWriteTextFormat, which combines a font family (Times New Roman or Arial, for example) along with a style (italic or oblique), weight (bold or light), stretch (narrow or expanded), and a font size. You’ll probably define a reference-counted pointer for the IDWriteTextFormat object as a private member in a header file:

Microsoft::WRL::ComPtr<IDWriteTextFormat> m_textFormat;

The object is created with a method defined by IDWriteFactory:

dwriteFactory->CreateTextFormat(
  L"Century Schoolbook", nullptr,
  DWRITE_FONT_WEIGHT_NORMAL,
  DWRITE_FONT_STYLE_ITALIC,
  DWRITE_FONT_STRETCH_NORMAL,
  24.0f, L"en-US", &m_textFormat);

In the general case, your application will probably create several IDWriteTextFormat objects for different font families, sizes and styles. These are device-independent resources, so you can create them at any time after you call DWriteCreateFactory to obtain an IDWriteFactory object, and keep them for the duration of your application.

If you spell the family name incorrectly—or if a font with that name isn’t on your system—you’ll get a default font. The second argument indicates the font collection in which to search for such a font. Specifying nullptr indicates the system font collection. You can also have private font collections.

The size is in device-independent units based on a resolution of 96 units per inch, so a size of 24 is equivalent to an 18-point font. The language indicator refers to the language of the font family name, and can be left as an empty string.

Once you’ve created an IDWriteTextFormat object, all the information you’ve specified is immutable. If you need to change the font family, style or size, you’ll need to re-create the object.

What more do you need to render text beyond the IDWriteText­Format object? Obviously, the text itself, but also the location on the screen where you want it to be displayed, and the color. These items are specified when the text is rendered: The destination of the text is indicated not with a point but with a rectangle of type D2D1_RECT_F. The color of the text is specified with a brush, which can be any type of brush, such as a gradient brush or image brush.

Here’s a typical DrawText call:

deviceContext->DrawText(
  L"This is text to be displayed",
  28,    // Characters
  m_textFormat.Get(),
  layoutRect,
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE,
  DWRITE_MEASURING_MODE_NATURAL);

By default, the text is broken into lines and wrapped based on the width of the rectangle (or the height of the rectangle for languages that read from top to bottom). If the text is too long to display within the rectangle, it will continue beyond the bottom. The penultimate argument can indicate optional flags to clip text falling outside the rectangle, or to not align characters on pixel boundaries (which is useful if you’ll be performing animations on the text) or, in Windows 8.1, to enable multicolor font characters.

The downloadable code for this column includes a Windows 8.1 program that uses IDWriteTextFormat and DrawText to display Chapter 7 of Lewis Carroll’s “Alice’s Adventures in Wonderland.” (I obtained the text from the Project Gutenberg Web site, but modified it somewhat to make it more consistent with the typography of the original edition.) The program is called PlainTextAlice, and I created it using the DirectX App (XAML) template in Visual Studio Express 2013 Preview for Windows 8.1. This project template generates a XAML file containing a SwapChainPanel and all the necessary overhead for displaying DirectX graphics on it.

The file with the text is part of the project content. Each paragraph is a single line, and each is separated by a blank line. The DirectXPage class loads the text in a Loaded event handler and transfers it to the PlainTextAliceMain class (created as part of the project), which transfers it to the PlainTextAliceRenderer class—the class I contributed to the project.

Because the graphics displayed by this program are fairly static, I disabled the rendering loop in DirectXPage by not attaching a handler for the CompositionTarget::Rendering event. Instead, PlainTextAliceMain determines when the graphics should be redrawn, which is only when the text is loaded or when the application window changes size or orientation. At these times, PlainTextAliceMain calls the Render method in PlainTextAlice­Renderer and the Present method in DeviceResources.

The C++ part of the PlainTextAliceRenderer class is shown in Figure 1. For clarity, I’ve removed the HRESULT checks.

Figure 1 The PlainTextAliceRenderer.cpp File

#include "pch.h"
#include "PlainTextAliceRenderer.h"
using namespace PlainTextAlice;
using namespace D2D1;
using namespace Platform;
PlainTextAliceRenderer::PlainTextAliceRenderer(
  const std::shared_ptr<DeviceResources>& deviceResources) :
  m_text(L""),
  m_deviceResources(deviceResources)
{
  m_deviceResources->GetDWriteFactory()->
    CreateTextFormat(L"Century Schoolbook",
                     nullptr,
                     DWRITE_FONT_WEIGHT_NORMAL,
                     DWRITE_FONT_STYLE_NORMAL,
                     DWRITE_FONT_STRETCH_NORMAL,
                     24.0f,
                     L"en-US",
                     &m_textFormat);
  CreateDeviceDependentResources();
}
void PlainTextAliceRenderer::CreateDeviceDependentResources()
{
  m_deviceResources->GetD2DDeviceContext()->
    CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Black),
                          &m_blackBrush);
  m_deviceResources->GetD2DFactory()->
    CreateDrawingStateBlock(&m_stateBlock);
}
void PlainTextAliceRenderer::CreateWindowSizeDependentResources()
{
  Windows::Foundation::Size windowBounds =
    m_deviceResources->GetOutputBounds();
  m_layoutRect = RectF(50, 0, windowBounds.Width - 50,
      windowBounds.Height);
}
void PlainTextAliceRenderer::ReleaseDeviceDependentResources()
{
  m_blackBrush.Reset();
  m_stateBlock.Reset();
}
void PlainTextAliceRenderer::SetAliceText(std::wstring text)
{
  m_text = text;
}
void PlainTextAliceRenderer::Render()
{
  ID2D1DeviceContext* context = 
    m_deviceResources->GetD2DDeviceContext();
  context->SaveDrawingState(m_stateBlock.Get());
  context->BeginDraw();
  context->Clear(ColorF(ColorF::White));
  context->SetTransform(m_deviceResources->GetOrientationTransform2D());
  context->DrawText(m_text.c_str(),
                    m_text.length(),
                    m_textFormat.Get(),
                    m_layoutRect,
                    m_blackBrush.Get(),
                    D2D1_DRAW_TEXT_OPTIONS_NONE,
                    DWRITE_MEASURING_MODE_NATURAL);
  HRESULT hr = context->EndDraw();
  context->RestoreDrawingState(m_stateBlock.Get());
}

Notice that the m_layoutRect member is calculated based on the application’s size on the screen but with a 50-pixel margin at the left and right. The result is shown in Figure 2.

The PlainTextAlice Program
Figure 2 The PlainTextAlice Program

First off, I’ll find some good things to say about this program: Clearly, with a minimum overhead, the text is properly wrapped into nicely separated paragraphs.

The inadequacies of the PlainText­Alice project are also obvious: The spacing between the paragraphs exists only because the original text file has blank lines I inserted just for that purpose. If you wanted a somewhat smaller or wider paragraph spacing, that wouldn’t be possible. Moreover, there’s no way to indicate italicized or boldfaced words within the text.

But the biggest drawback is that you can see only the beginning of the chapter. Scrolling logic could be implemented, but how can you tell how far to scroll? The really big problem with IDWriteText­Format and DrawText is that the height of the formatted rendered text is simply not available.

In conclusion, using DrawText makes sense only when the text has uniform formatting, and you know the rectangle you’re specifying is sufficient to fit the text or you don’t care if there’s a little runover. For more sophisticated purposes a better approach is required.

Before moving on, take note that the information specified in CreateTextFormat method is immutable in the IDWriteTextFormat object, but the interface declares several methods that let you change how the text is displayed: For example, SetParagraphAlignment alters the vertical placement of the text within the rectangle specified in DrawText, while SetTextAlignment lets you specify whether the lines of the paragraph are left, right, centered or justified within the rectangle. The arguments to these methods use words such as near, far, leading, and trailing to be generalized for text that reads from top to bottom or from right to left. You can also control line spacing, text wrapping and tab stops.

The Text Layout Approach

The next step up from IDWriteTextFormat is a big one, and it’s ideal for pretty much all standard text output needs, including hit-testing. It involves an object of type IDWriteTextLayout, which not only derives from IDWriteTextFormat, but incorporates an IDWriteTextFormat object when it’s being instantiated.

Here’s a reference-counted pointer to an IDWriteTextLayout object, probably declared in a header file:

Microsoft::WRL::ComPtr<IDWriteTextLayout> m_textLayout;

Create the object like so:

dwriteFactory->CreateTextLayout(
  pText, wcslen(pText),
  m_textFormat.Get(),
  maxWidth, maxHeight,
  &m_textLayout);

Unlike the IDWriteTextFormat interface, IDWriteTextLayout incorporates the text itself and the desired height and width of the rectangle to format the text. The DrawTextLayout method that displays an IDWriteTextLayout object needs only a 2D point to indicate where the top-left corner of the formatted text is to appear:

deviceContext->DrawTextLayout(
  point,
  m_textLayout.Get(),
  m_blackBrush.Get(),
  D2D1_DRAW_TEXT_OPTIONS_NONE);

Because the IDWriteTextLayout object has all the information it needs to calculate line breaks before the text is rendered, it also knows how large the rendered text will be. IDWriteTextLayout has several methods—GetMetrics, GetOverhangMetrics, GetLineMetrics and GetClusterMetrics—that provide a wealth of information to help you work with this text effectively. For example, GetMetrics provides the total width and height of the formatted text, as well as the number of lines and other information.

Although the CreateTextLayout method includes maximum width and height arguments, these can be set to other values at a later time. (The text itself is immutable, however.) If the display area changes (for example, you turn your tablet from landscape to portrait mode), you don’t need to re-create the IDWriteTextLayout object. Just call the SetMaxWidth and SetMaxHeight methods declared by the interface. Indeed, when you first create the IDWriteTextLayout object, you can set the maximum width and height arguments to zero.

The ParagraphFormattedAlice project uses IDWriteTextLayout and DrawTextLayout, and the results are shown in Figure 3. You still can’t scroll to see the rest of the text, but notice that some lines are centered, and many have first line indents. The titles use a larger font size than the rest and some words are italicized.

The ParagraphFormattedAlice Program
Figure 3 The ParagraphFormattedAlice Program

The text file is a little different in this project than the first project: Each paragraph is still a separate line, but no blank lines separate these paragraphs. Rather than using a single IDWriteTextFormat object for the entire text, each paragraph in ParagraphFormatted­Alice is a separate IDWriteTextLayout object rendered with a separate DrawTextLayout call. The space between the paragraphs can therefore be set to any desired amount.

To work with the text, I defined a structure named Paragraph:

struct Paragraph
{
  std::wstring Text;
  ComPtr<IDWriteTextLayout> TextLayout;
  float TextHeight;
  float SpaceAfter;
};

A helper class called AliceParagraphGenerator generates a collection of Paragraph objects based on the lines of text.

IDWriteTextLayout has a bunch of methods to set formatting on individual words or other blocks of text. For example, here’s how the five characters beginning at offset 23 in the text are italicized:

DWRITE_TEXT_RANGE range = { 23, 5 };
textLayout->SetFontStyle(DWRITE_FONT_STYLE_ITALIC, range);

Similar methods are available for the font family, collection, size, weight, stretch, underline and strikethrough. In real-life applications, text formatting is usually defined by markup (such as HTML), but to keep things simple, the AliceParagraphGenerator class works with a plain-text file and contains hardcoded locations for the italicized words.

AliceParagraphGenerator also has a SetWidth method to set a new display width, as shown in Figure 4. (For clarity, the HRESULT checks have been removed from the code in this figure.) The display width changes when the window size changes, or when a tablet changes orientation. SetWidth loops through all the Paragraph objects, calls SetMaxWidth on the TextLayout, and then obtains a new formatted height of the paragraph that it saves in TextHeight. Earlier, the SpaceAfter field was simply set to 12 pixels for most of the paragraphs, 36 pixels for the headings, and 0 for a couple lines of verse. This makes it easy to obtain the height of each paragraph and the total height of all the text in the chapter.

Figure 4 The SetWidth Method in AliceParagraphGenerator

float AliceParagraphGenerator::SetWidth(float width)
{
  if (width <= 0)
    return 0;
  float totalHeight = 0;
  for (Paragraph& paragraph : m_paragraphs)
  {
    HRESULT hr = paragraph.TextLayout->SetMaxWidth(width);
    hr = paragraph.TextLayout->SetMaxHeight(FloatMax());
    DWRITE_TEXT_METRICS textMetrics;
    hr = paragraph.TextLayout->GetMetrics(&textMetrics);
    paragraph.TextHeight = textMetrics.height;
    totalHeight += paragraph.TextHeight + paragraph.SpaceAfter;
  }
  return totalHeight;
}

The Render method in ParagraphFormattedAliceRenderer also loops through all the Paragraph objects and calls DrawTextLayout with a different origin based on the accumulated height of the text.

The First-Line Indent Problem

As you can see in Figure 3, the ParagraphFormattedAlice program indents the first line of the paragraphs. Perhaps the easiest way to make such an indent is by inserting some blank spaces at the beginning of the text string. The Unicode standard defines codes for an em space (which has a width equal to the point size), en space (half that), quarter em, and smaller spaces, so you can combine these for the desired spacing. The advantage is that the indent is proportional to the point size of the font.

However, that approach won’t work for a negative first-line indent—sometimes called a hanging indent—which is a first line that begins to the left of the rest of the paragraph. Moreover, first-line indents are commonly specified in metrics (such as a half-inch) that are independent of the point size.

For these reasons, I decided instead to implement first-line indents using the SetInlineObject method of the IDWriteTextLayout interface. This method is intended to allow you to put any graphical object inline with the text so it becomes almost like a separate word whose size is taken into account for wrapping the line.

The SetInlineObject method is commonly used for inserting small bitmaps into the text. To use it for that or any other purpose, you need to write a class that implements the IDWriteInlineObject interface, which, besides the three standard IUnknown methods, declares the GetMetrics, GetOverhangMetrics, GetBreakConditions, and Draw methods. Basically, the class you supply is called while the text is being measured or rendered. For my FirstLineIndent class, I defined a constructor with an argument indicating the desired indent in pixels, and that value is basically returned by the GetMetrics call to indicate the size of the embedded object. The class’s implementation of Draw does nothing. Negative values work fine for hanging indents.

You call the SetInlineObject of IDWriteFontLayout just like SetFontStyle and other methods based on text ranges, but it was only when I began using SetInlineObject that I discovered the range couldn’t have a length of zero. In other words, you can’t simply insert an inline object. The inline object has to replace at least one character of text. For that reason, while defining the Paragraph objects, the code inserts a no-width space character (‘\x200B’) at the beginning of each line, which has no visual appearance but can be replaced when SetInlineObject is called.

DIY Scrolling

The ParagraphFormattedAlice program doesn’t scroll, but it has all the essential information to implement scrolling, specifically, the total height of the rendered text. The ScrollableAlice project demonstrates one approach to scrolling: The program still outputs to a SwapChainPanel the size of the program’s window, but it offsets the rendering based on the user’s mouse or touch input. I think of this approach as “do it yourself” scrolling.

I created the ScrollableAlice project in Visual Studio 2013 Preview using the same DirectX App (XAML) template I used for the earlier projects, but I was able to take advantage of another interesting aspect of this template. The template contains code in DirectXPage.cpp that creates a secondary thread of execution to handle Pointer events from the SwapChainPanel. This technique avoids bogging down the UI thread with this input.

Of course, introducing a secondary thread complicates things as well. I wanted some inertia in my scrolling, which meant I wanted Manipulation events rather than Pointer events. This required that I use DirectXPage to create a GestureRecognizer object (also in that secondary thread), which generates Manipulation events from Pointer events.

The previous ParagraphFormattedAlice program redraws the program’s window when the text is loaded and when the window changes size. ScrollableAlice does that also, and that redrawing continues to occur in the UI thread. ScrollableAlice also redraws the window when ManipulationUpdated events are fired, but this occurs in the secondary thread created for the pointer input.

What happens if you give the text a good flick with your finger so it continues scrolling with inertia, and while the text is still scrolling, you resize the window? There’s a good possibility that overlapping DirectX calls will be made from two different threads at the same time, and that’s a problem.

What’s required is some thread synchronization, and a good, easy solution involves the critical_section class in the Concurrency namespace. In a header file, the following is declared:

Concurrency::critical_section m_criticalSection;

The critical_section class contains an embedded class named scoped_lock. The following statement creates an object named lock of type scoped_lock by calling the constructor with the critical_section object:

critical_section::scoped_lock lock(m_criticalSection);

This constructor assumes ownership of the m_criticalSection object, or blocks execution if the m_criticalSection object is owned by another thread. What’s nice about this scoped_lock class is that the destructor releases ownership of the m_criticalSection when the lock object goes out of scope, so it’s incredibly easy to just sprinkle a bunch of these around in various methods that might be called concurrently.

I determined it would be easiest to implement this critical section in DirectXPage, which contains some crucial calls to the DeviceResources class (such as UpdateForWindowSizeChanged) that require other threads be blocked for the duration. Although it’s not a good idea to block the UI thread (which happens when pointer events are fired), these blocks are extremely short.

By the time the scrolling information is delivered to the Scrollable­AliceRenderer class, it’s in the form of a floating-point value stored as a variable named m_scrollOffset, clamped between 0 and a maximum value equal to the difference between the height of the full chapter of text and the height of the window. The Render method uses that value to determine how to begin and end the display of paragraphs, as shown in Figure 5.

Figure 5 Implementing Scrolling in ScrollableAlice

std::vector<Paragraph> paragraphs =
  m_aliceParagraphGenerator.GetParagraphs();
float outputHeight = m_deviceResources->GetOutputBounds().Height;
D2D1_POINT_2F origin = Point2F(50, -m_scrollOffset);
for (Paragraph paragraph : paragraphs)
{
  if (origin.y + paragraph.TextHeight + paragraph.SpaceAfter > 0)
    context->DrawTextLayout(origin, paragraph.TextLayout.Get(),
    m_blackBrush.Get());
  origin.y += paragraph.TextHeight + paragraph.SpaceAfter;
  if (origin.y > outputHeight)
    break;
}

Scrolling with a Bounce

Although the ScrollableAlice program implements scrolling that has touch inertia, it doesn’t have the characteristic Windows 8 bounce when you try to scroll past the top or bottom. That (by now) familiar bounce is incorporated into ScrollViewer, and while it might be fun to try to duplicate the ScrollViewer bounce in your own code, that’s not the job for today.

Because scrollable text might be long, it’s best not to try to render it all on one surface or bitmap. DirectX has restrictions on how large these surfaces can be. The ScrollableAlice program gets around these restrictions by limiting its display to a SwapChainPanel the size of the program’s window, and that works just fine. But for ScrollViewer to work, the content must have a size in layout reflecting the full height of the formatted text.

Fortunately, Windows 8 supports an element that does exactly what’s needed. The VirtualSurfaceImageSource class derives from SurfaceImageSource, which in turn derives from ImageSource so it can serve as a bitmap source for an Image element in a ScrollViewer. VirtualSurfaceImageSource can be any desired size (and can be resized without being re-created), and gets around DirectX size limitations by virtualizing the surface area and implementing on-demand drawing. (However, SurfaceImageSource and VirtualSurfaceImageSource are not optimal for high-performance frame-based animations.)

VirtualSurfaceImageSource is a Windows Runtime ref class. To use it in conjunction with DirectX, it must be cast to an object of type IVirtualSurfaceImageSourceNative, which reveals the methods used to implement on-demand drawing. These methods report what rectangular areas need to be updated, and allow the program to be notified of new update rectangles by supplying a class that implements IVirtualSurfaceUpdatesCallbackNative.

The project demonstrating this technique is BounceScrollableAlice, and because it didn’t require a SwapChainPanel, I created it in Visual Studio 2013 Preview based on the Blank App (XAML) template. For the required class that implements IVirtualSurface­UpdatesCallbackNative I created a class named VirtualSurface­ImageSourceRenderer, and it also provides much of the DirectX overhead. The AliceVsisRenderer class derives from VirtualSurface­ImageSourceRenderer to provide the Alice-specific drawing.

The update rectangles available from IVirtualSurfaceImageSourceNative are relative to the full size of the VirtualSurfaceImageSource, but drawing coordinates are relative to the update rectangles. This means the DrawTextLayout calls in BounceScrollableAlice are virtually the same as those shown in Figure 5, except the initial origin is set to the negative of the top of the update rectangle rather than the scroll offset, and the outputHeight value is the difference between the bottom and top of the update rectangle.

By introducing ScrollViewer into the mix, the text display feels truly Windows 8-like, and you can even use a pinch gesture to make it larger or smaller, emphasizing even more that this Windows 8 melding of DirectX and XAML provides something close to the best of both worlds.


Charles Petzold is a longtime contributor to MSDN Magazine and the author of “Programming Windows, 6th edition” (Microsoft Press, 2012), a book about writing applications for Windows 8. His Web site is charlespetzold.com.

Thanks to the following technical experts for reviewing this article: Jim Galasyn (Microsoft) and Justin Panian (Microsoft)