Text Rendering

Build World-Ready Apps Using Complex Scripts In Windows Forms Controls

Miguel A. Lacouture

This article discusses:

  • The TextRenderer and Graphics classes
  • Using IDeviceContext
  • Printing with TextRenderer
  • Migrating from Graphics to TextRenderer
This article uses the following technologies:
Windows Forms, .NET Framework 2.0, GDI+

Code download available at:TextRendering.exe(154 KB)

Contents

The TextRenderer Class
The IDeviceContext Interface
Limitations of TextRenderer
Using TextRenderer
UseCompatibleTextRendering
Printing with TextRenderer
The TextFormatFlags Utility
Conclusion

Developing world-ready software is one of the requirements software companies must meet if they want to reach customers around the globe. Text-rendering libraries must excel at rendering text, as well as at supporting international locales (language packages). This is where the Windows ® Forms System.Drawing.Graphics class presents some limitations—it is based on GDI+ and currently has limited support for complex scripts.

The System.Windows.Forms.TextRenderer class was developed to provide the same level of support for complex scripts in Windows Forms controls that we expect from the Windows operating system. This TextRenderer is based on the GDI text-rendering API, which uses the Windows Unicode Script Processor (Uniscribe).

What are Complex Scripts?

Among the scripts that the Windows operating system supports, there are some that require special processing when editing and displaying text. This is because characters can have different shapes depending on the context in which they are written. Furthermore, characters from these scripts are not laid out in a simple linear progression—like the left to right progression used in English.

Arabic scripts, for instance, are right-justified and are read from right to left (RTL). In addition, the Arabic languages have character contextual shaping: a character may be represented differently depending on its position within a word or what characters precede or follow it. These scripts also require complex semantics for word and line breaking and text justification. Due to the complexity of text processing required by these scripts, they are called complex scripts.

Figure 1 shows a piece of Sinhalese text written with and without full support for complex scripts. Some of the characters in the first row, where the Graphics class is used, are not rendered properly. Compare these to the characters in the second row, which shows text rendered with full script support using TextRenderer. (For more information about complex scripts, see the " What are Complex Scripts? " sidebar.)

Figure 1 Text Rendering Differences

System.Drawing.Graphics System.Windows.Forms.TextRenderer
Character shapes are not rendered accurately. Characters are shaped properly with full script support.

The TextRenderer Class

The System.Windows.Forms.TextRenderer class, new to the Microsoft ® .NET Framework 2.0, contains only two overloaded static methods: MeasureText and DrawText. As their names imply, one method is used to measure the size of text, and the other to draw text, in a device context. Two new types were introduced with the TextRenderer class: the System.Windows.Forms.TextFormatFlags enumeration and the System.Drawing.IDeviceContext interface.

If you're familiar with the Win32 ® DrawTextEx function, you'll see a lot of commonalities between the values of the TextFormatFlags enumeration and the values defined for the dwDTFormat parameter of the native DrawTextEx function. This is not a coincidence—TextRenderer is actually a wrapper around DrawTextEx and most of the formatting options supported by DrawTextEx are also supported by TextRenderer. The TextFormatFlags enumeration defines the formatting options that govern how the text is to be formatted, measured, and drawn in a device context.

There are a few differences between the native DrawTextEx function and the TextRenderer methods. For instance, the native formatting options DT_CALCRECT and DT_TABSTOP are not present in TextFormatFlags. The DT_CALCRECT option, which is used for measuring text, is not needed because TextRenderer has MeasureText. The DT_TABSTOP option, which sets tab stops, is not currently supported by TextRenderer. To set the tab stop length in DrawTextEx, you use the DRAWTEXTPARAMS parameter; this also lets the user specify text margins. While TextRenderer has no counterpart for this particular parameter, it can expand tabs in the text if the TextFormatFlags.ExpandTabs flag is specified.

Even though TextRenderer does not expose a way to specify custom text margins as does the native DrawTextEx function, TextRenderer can automatically add text margins depending on some flags defined by TextFormatFlags for this purpose: TextFormatFlags.NoPadding, TextFormatFlags.GlyphOverhangPadding, and TextFormatFlags.LeftAndRightPadding.

Figure 2 illustrates these flags in use. The text is drawn using the flags and using a font created from the Times New Roman family. The text at the right is drawn with the italic style. The rectangle around the text is the text bounding rectangle, the size of which is obtained by measuring the text using these flags.

Figure 2 Text Drawn with Padding Flags

TextFormatFlags.NoPadding
TextFormatFlags.GlyphOverhangPadding
TextFormatFlags.LeftAndRightPadding

However, Figure 2 shows a problem. Take a look at the italicized text that is rendered with the NoPadding flag. Notice where the glyphs fall outside the bounding box in the lower-left and upper-right areas. If the bounding box is clipped, those parts will not be drawn. Some characters have this problem when drawn at the beginning or end of a text block with a particular font and style. The GlyphOverhangPadding flag, which is the default padding value in TextFormatFlags, was provided to address this problem. When GlyphOverhangPadding is specified, TextRenderer computes some left and right margins based on the font size and style.

When drawing controls—buttons, for example—some extra text margins can sometimes make the text in the control look better. This is particularly true when the button's size is calculated on the fly. The LeftAndRightPadding flag was provided for this purpose.

The TextFormatFlags enumeration also defines PreserveGraphicsClipping and PreserveGraphicsTranslateTransform flags. These flags help to improve interoperability with the Graphics class and are needed only when some coordinate translation and custom clipping information have been applied to the Graphics object. When transitioning from GDI+ to GDI, this information is lost. If these flags are set, TextRenderer reapplies the information to the GDI device context. One more property that is automatically preserved from the Graphics object is Graphics.TextRenderingHint, which helps choose the quality of the font to be used.

The TextFormatFlags enumeration is to TextRenderer what the System.Drawing.StringFormat class is to Graphics—both define the way text should be laid out. There isn't an exact mapping between the flags in the TextFormatFlags enumeration and the ones in the StringFormat class, but a pretty good approximation can be achieved. Figure 3 outlines the relationships between the values of these formatting types.

Figure 3 Mapping Values from StringFormat and TextFormatFlags

StringFormat Property Value TextFormatFlags Flag Value
Horizontal Alignment
Alignment.Near Left
Alignment.Center HorizontalCenter
Alignment.Far Right
Vertical Alignment
LineAlignment.Near Top
LineAlignment.Center VerticalCenter
LineAlignment.Far Bottom
Ellipsis
Trimming.EllipsisCharacter EndEllipsis
Trimming.EllipsisWord WordEllipsis
Trimming.EllipsisPath PathEllipsis
Trimming.Character N/A
Trimming.Word N/A
Trimming.None <default behavior>
Hotkey Prefix
HotkeyPrefix.None NoPrefix
HotkeyPrefix.Show <default behavior>
HotkeyPrefix.Hide HidePrefix
N/A PrefixOnly
Text Padding
FormatFlags.FitBlackBox * NoPadding
N/A LeftAndRightPadding
Text Wrapping
FormatFlags.NoWrap SingleLine
<default behavior> WordBreak
FormatFlags.LineLimit WordBreak | TextBoxControl
N/A NoFullWidthCharacterBreak
Other Flags
FormatFlags.DirectionRightToLeft RightToLeft
FormatFlags.NoClip NoClipping
FormatFlags.DisplayFormatControl N/A
FormatFlags.NoFontFallBack N/A
FormatFlags.MeasureTrailingSpaces N/A
FormatFlags.DirectionVertical N/A
N/A PreserveGraphicsClipping
N/A PreserveGraphicsTranslateTransform
N/A ExternalLeading
N/A Internal
N/A ModifyString
SetTabStops method ExpandTabs
* Actually means NoFitBlackBox

The TextFormatFlags utility I discuss later in this article can create a StringFormat object based on the values of TextFormatFlags, and vice versa and then copy it to the clipboard as C# code.

The IDeviceContext Interface

IDeviceContext is used to define a contract for getting and releasing a native handle to a device context (HDC).

The IDeviceContext interface was provided for two reasons. The first is for a way to interact seamlessly with the Graphics class. The type of most of the parameters passed to the TextRenderer methods come from the System.Drawing namespace. Thus, the Graphics stateless model was preserved in TextRenderer for consistency. The Graphics class implements this interface and can be used directly with TextRenderer methods.

The second reason for the IDeviceContext interface is to allow for custom initialization of a device context before calling into TextRenderer. There are several features in Graphics that are not supported by TextRenderer, like drawing inclined text. IDeviceContext actually lets you implement support for the missing features, assuming feature parity exists in both of the underlying technologies (GDI and GDI+), which is not always the case. This interface is also used by the System.Windows.Forms.VisualStyles API. This is a new API, introduced with the .NET Framework 2.0, used for rendering controls and other UI elements with visual styles.

Figure 4 shows the code that was used to render the text in Figure 2 . This code shows the steps needed to properly render text in a device context that a Graphics object represents. Note that the value Size.Empty is used as the proposed size when measuring the text. This is one way to tell TextRenderer that there isn't a bounding restriction and that the box size should be that which best fits the text. Another way to express this is by passing an unbounded size with width and height set to the maximum possible.

Figure 4 Rendering Text with TextRenderer

Size PaintText(Graphics g, string txt, Font font, 
    Point pt, TextFormatFlags flags) {
    Size size = TextRenderer.MeasureText(g, txt, 
        font, Size.Empty, flags);
    Rectangle box = new Rectangle(pt, size);
    g.DrawRectangle(Pens.Black, box);
    TextRenderer.DrawText(g, txt, font, box, 
        SystemColors.ControlText, flags);
    return size;
}

Limitations of TextRenderer

The TextRenderer class was designed for rendering text in Windows Forms controls (display device context type). As such, the TextRenderer class, unlike the Graphics class, is not intended for device-independent drawing. This does not mean it cannot be used in other device context types, but some work will need to be done beforehand. For instance, when using TextRenderer for printing, all parameter values must be properly scaled based on the printer resolution. (I discuss this in more detail later in this article.) This is probably the main reason why TextRenderer appears under the System.Windows.Forms namespace rather than under the System.Drawing namespace.

TextRenderer does not support rendering text at an angle. A simple workaround is to draw the text into a bitmap and then rotate the bitmap as needed. Note that when text is rendered into a bitmap, transparency is lost. Though this solution won't work in all cases, I recommend that you fill the bitmap background color where you're placing the text with the same color used in the background of the bitmap image where the text will be rendered.

As mentioned previously, TextRenderer does not allow you to specify custom tab lengths. A solution for this is to format the string with the appropriate tab lengths and location using the rich string formatting options in the .NET Framework before passing it to TextRenderer.

Using TextRenderer

Should You Make the Switch?

Not sure whether you should continue using Graphics or switch to using TextRenderer? The right decision depends on your situation. Here are a few things you need to consider:

Can your application take advantage of TextRenderer? If after reading the previous sections you determine that your application will not benefit from TextRenderer, then stick with the Graphics class you've already implemented and tested.

Can you can afford to make the changes? There are a couple of things you would need to do. First, you would call Application.SetUseCompatibleTextRenderingDefault before creating any form. You would then replace calls to Graphics.MeasureString and Graphics.DrawString with TextRenderer.MeasureText and TextRenderer.DrawText, respectively. The biggest challenge when replacing the Graphics calls is determining what set of TextFormatFlags to use based on the StringFormat options. To make this process easier, the TextFormatFlags utility can give you a hand, as explained later.

Can you afford to test for changes that break? The way GDI and GDI+ calculate text metrics, like character spacing and word breaking, is not the same and in some cases the differences are noticeable. There are several factors that affect the way text is rendered, including the font and all of its variations (such as style, size, family, and so on), the device resolution, and system text options (such as text appearance effects). Also, there isn't a full mapping between the StringFormat options and the TextFormatFlags options. Clipped text or layout changes are among the most common cases of breaking changes to watch for when switching text-rendering engines.

TextRenderer has multiple benefits for drawing text in controls—it lets you obtain complex script support for free, and it allows your controls to display text using the same rendering engine, providing a consistent text layout across the UI. In the .NET Framework 2.0, all controls render text using GDI by default, and there are actually some controls that cannot render text using GDI+, like TextBox and ListView.

As with complex scripts, there are other cases where Graphics presents some limitations that TextRenderer can fix. For instance, you cannot use GDI+ when creating end-user-defined characters (EUDC) at run time because it locks the font, preventing the addition of new characters. An important point to keep in mind is to avoid measuring text using one class and then drawing it using the another. This could lead to issues like text clipping since the text layout algorithms are different in the two classes. Always use Graphics.MeasureString with Graphics.DrawString and TextRenderer.MeasureText with TextRender-er.DrawText. (If you're wondering whether moving your apps to TextRenderer will be worth the effort, see the sidebar " Should You Make the Switch? " for some advice.)

UseCompatibleTextRendering

All Windows Forms controls in the .NET Framework 2.0 provide the same level of support for complex scripts as does Windows. In previous releases, a few Windows Forms controls could only render text using the Graphics class with the GDI+ inherited complex scripts limitation. These controls now render text using the TextRenderer class by default. For compatibility with previous releases, these controls have a new property: UseCompatibleTextRendering. When this property is set to true, text is rendered as in previous releases using the Graphics class. The controls that have this new property are Button, RadioButton, CheckBox, CheckedListBox, GroupBox, Label, LinkLabel, and PropertyGrid.

For compatibility, the UseCompatibleTextRendering property is set to false by default so applications built with previous releases of the .NET Framework will continue to look the same even when run on the .NET Framework 2.0.

New applications can change the default value of this property by calling the static Application.SetUseCompatibleTextRenderingDefault method before running the first form in a process. The Visual Studio ® designer for Windows Forms generates a call to this method in the Main method.

As a side effect of this feature, controls in Windows Forms apps now look more consistent with the rest of the Windows UI because they all share the same text-rendering engine (GDI).

Printing with TextRenderer

When working with System.Drawing.Printing, you are usually interested in handling the PrintPage event of the PrintDocument object. You can use the Graphics object of the PrintPageEventArgs in the same way you use the Graphics object obtained from the PaintEventArgs when handling a control's paint event. In many cases, you want to use a common method to draw—for example, drawing into a Form, the printer, and a print-preview dialog box. This ensures consistency between what the user sees on screen and on the printed page.

This technique, however, does not work with TextRenderer. The reason is that the Graphics object in the PrintPageEventArgs object is set up with the resolution of the printer and also works with a different coordinate unit—it is usually set for the coordinate unit to be hundredths of an inch. How does it work for the Graphics object? That is where GDI+ displays its beauty. GDI+ is a device-independent graphics library and knows how to scale units properly in this case. TextRenderer assumes all units are at 100 percent. In other words, no scaling is needed.

To make TextRenderer work with the printing API, unit scaling needs to be performed beforehand. Figure 5 shows how to scale a Point and a Font to be used with TextRenderer when the Graphics object comes from the printing library. Units are converted from hundredths of an inch to pixels using the resolution of the device, which is obtained from the Graphics object DpiX and DpiY properties.

Figure 5 Scaling Font and Point

private Point ScalePoint( Graphics g, ref Point p ) {
   return new Point( 
      (int) Math.Ceiling( p.X / 100f * g.DpiX ), 
      (int) Math.Ceiling( p.Y / 100f * g.DpiY ) );
}

private Font ScaleFont( Graphics g, Font font ) {
   return new Font( 
      font.FontFamily, font.SizeInPoints / 72f * 
      g.DpiY, font.Style, GraphicsUnit.Pixel, 
      font.GdiCharSet, font.GdiVerticalFont );
}

In Figure 6 , the printing flag is set to true in the printDoc_Print-Page event handler method for the PrintPage event, and false in the control_Paint event handler method for the Paint event. The PaintWorker method checks this flag and, if printing, calls the scaling methods in Figure 5 .

Figure 6 Paint Method for Display or Printer

private void PaintWorker( Graphics g ) {
    Point pt = new Point(10, 10);
    Font f = this.Font;

    if( this.printing ) {
        pt = ScalePoint(g, ref pt);
        f = ScaleFont(g, f);
    }

    TextRenderer.DrawText(g, Text, f, pt, SystemColors.ControlText);

    if( f != this.Font) f.Dispose();
}

private void control_Paint( object sender, PaintEventArgs e ) {
    this.printing = false;
    PaintWorker( e.Graphics );
}

void printDoc_PrintPage( object sender, PrintPageEventArgs e ) {
    this.printing = true;
    PaintWorker( e.Graphics );
}

Even though this approach solves the problem encountered when using TextRenderer for printing and print-previewing, it does not guarantee that you will obtain the same WYSIWYG results you get with the Graphics class. The main reason is that the GDI character-spacing and word-breaking algorithm is not linear—the text rendered using the scaled values may end up with a different layout. Other reasons include the loss of precision when scaling the values and the possibility of obtaining a scaled font that is different from the intended scaling. This can occur if the intended scaled font does not exist and the font-mapping algorithm selects what it thinks is the closest match.

The TextFormatFlags Utility

Figure 7 shows the TextFormatFlags utility UI. Note that in this image, there is a small window on top of the main form. This window, called the drawing surface, is used to compare text rendered with both the TextRenderer and the Graphics classes. The text format can be chosen from the options form—TextFormatFlags options are chosen from the left panel and StringFormat options from the right.

Figure 7 TextFormatFlags Utility

Figure 7 ** TextFormatFlags Utility **

One of the most interesting features of this tool is that it can obtain the text format options from one API based on the options from the other API and copy this information to the clipboard as C# code, like this:

TextFormatFlags textFlags = TextFormatFlags.NoPrefix | 
    TextFormatFlags.WordBreak | TextFormatFlags.ExpandTabs;

StringFormat strFormat = new StringFormat();
strFormat.FormatFlags = 
    StringFormatFlags.MeasureTrailingSpaces;

Although text format options do not map 100 percent from one API to the other API, this tool does a very good job of getting you started with your transition between APIs. This tool was very useful during the development of the TextRenderer class, as it was used to better understand the behavior of the underlying technologies. You can download it from the MSDN®Magazine Web site.

Conclusion

Applications that need to be ready for globalization must provide at least the same level of support for rendering text using different scripts as the underlying operating system. With the introduction of the System.Windows.Forms.TextRenderer class in the .NET Framework 2.0, it has become easier to give your Windows Forms applications the same level of support for complex scripts as the platform the application is targeted to. Now all Windows Forms controls can render text using the same underlying text-rendering engine when the UseCompatibleTextRendering property is set to false, making the user interface more consistent in appearance and readability.

Miguel A. Lacouture is a Software Design Engineer on the Windows Forms team and has been at Microsoft for seven years. You can reach him at mlacouture@msn.com .