Text gets ugly
Even with GDI+ and its “device-less” approach to text, we are in a world of hurt when it comes to painting text accurately. Apparently this is because as UI builders, we find ourselves in a subset of the text rendering world: that of GUI Text. GUI Text is a wonderful term which (to the best of our knowledge) was first introduced in this CodeProject article by Pierre Arnaud. The article describes a technique which is essentially text measurement by side-effect. The idea is that you paint black text on a white bitmap, and then scan the bitmap pixel by pixel until you find a non-white pixel. In this way you can measure the exact size of the text in screen pixels. By definition this approach always comes up with the correct result – how could it not? as we are examining exactly what GDI+ has done, as opposed to what it says it will do (i.e. see
MeasureString, or even
GUI Text defined
But back to that wonderful term: GUI Text. Let’s define GUI Text as that text defined, positioned and painted strictly to enhance the usability of an application. It’s not part of a document. It’s not for printing. It’s just there to help the user out and to make other controls obvious in their function.
So let’s define, in terms of “features” what we are looking for in GUI Text:
- Positioning to the pixel (with exact height and width), just like all other Graphics primitives.
- Effective anti-aliasing at all point sizes (preferably something exactly like the Smooth – Sharp – Crisp – Strong flavors in Adobe PhotoShop – which work quite well even at small font sizes).
- Animation, where text can move around like in Macromedia Flash.
Well, don’t hold your breath for items 2 and 3. It seems likely that Microsoft will give us more "Golden API" which attempt these in Avalon. Really it’s a moot point though, because just getting item 1 is a real doozy in the .NET Framework. Here’s why:
The MeasureString situation
DrawString puts extra space before and after the text it renders.
MeasureString faithfully returns this extra space.
GenericTypographic cannot be used to render text – at least not crisp GUI text.
GenericTypographic is often suggested as the way to measure text accurately, and while it’s a little better (it removes the space after the text), it really doesn’t matter because of 3.
MeasureCharacterRanges does the same thing, but only after you’ve done a lot more up-front work to make the call to it.
Some empirical evidence on the “golden font” (which, if you are living in Windows XP, is Tahoma 8 regular):
Half of these tests were done with the default font resolution (96 dpi), the other half with large fonts (now simply called 120 dpi in Windows XP.) The blue rectangle represents the size returned by
MeasureCharacterRanges. The numbers in red indicate the amount of white space appearing between the edge of the blue rectangle and the edge of the test string (the test string “Wello jelly” was chosen for its very wide first character and for the presence of ascenders and descenders.) From a GUI Text perspective, every combination failed.
There are just enough settings in
StringFormat, just enough overloads on
DrawString and even a cute little
TextRenderingHint property on the
Graphics object to keep you working on this problem for days. No matter what you try though, that blue rectangle will never touch the text on every edge. Instead of fighting the technology, we decided to concede defeat. True screen pixel measurement does not appear to be available in GDI+, which leads us to conclude that measuring text by side-effect is the only solution available, save the use of some P/Invoke work which we would like to avoid.
The new Label control
So what are we shooting for here? Well, the new
Label control should support single or multi-line text, left, center, right or fully justified. Margins should be available on all four sides of the text. AutoSize should exist and should default to
true for both width and height. A pluggable border, which changes the inner rectangle available for text wrapping would also be nice.
Which leads us to some high-level tests:
- Left margin 10
- Right Margin 10
- Top Margin 10
- Bottom Margin 10
- Tahoma 8 Bold
- Tahoma 8 Italic
- Tahoma 8 Bold and Italic
- Change Font to Tahoma 10
- Change Text
- Change Location
- AutoSize off, Reduce Width to Truncate
- AutoSize off, Reduce Height to Truncate
- AutoSize off, Right Justify, Increase Width
- AutoSize off, Center, Increase Width
- AutoSize off, Full Justify, Increase Width
Coding up these tests is quite trivial, as is coding the new declarative classes to support the functionality. Our new label is modeled like this:
ControlProperty – represents any property used in a control, declares a
ControlState – holds a pool of
LabelController – listens for changes in any given
ControlProperty and routes the change to the corresponding
LabelBot for handling.
LabelBot – wraps text, calculates bounds and refreshes the label in response to a change in a single property.
VisibleBot – shows or hides the label when the
Visible property changes value.
TextModel – contains an object-based description of the wrapped text.
Line – a single line of text.
Word – a single word of text.
Character – a single character of text.
Rectangle – a
Bounds descendant which defines a rectangular area.
Point – implements the
Location property of
Rectangle, contains notification events.
Size – implements the
Size property of
Rectangle, also contains notification events.
There are many
LabelBot descendant classes not included in the diagram.
VisibleBot is shown here as a concrete example of properties and bots. In modeling controls using MVC, we have noticed that the application logic seems to centralize in the controller, resulting in a complex class. By isolating each individual change which can be made to a control and implementing a bot descendant to handle just that change, we are able to decentralize all of the logic required to maintain the integrity of the control. The
LabelController need only hook the
Changed event of each property. When the event fires, a bot is instantiated and called. Bots are modal, that is, only one bot can be active at a time. This is necessary because bots often trigger further changes in the control: a non-modal implementation would result in a stack fault.
Most of the work bots do on a
Label involves wrapping text (or re-wrapping text.) Coding a text wrapping engine isn’t too difficult, unless you need to know the position of each character in the string. Our research indicates that GDI+ treats each word as a work of art. That is, the character spacing can vary from word to word – even when two words contain the same letter combinations. For example, “rav” and “bravo”, when drawn by GDI+ have different spacing between the “a” and the “v”. When time comes to push a caret around (and that time will come soon), we are going to have a serious problem if this is the case. A compromise solution then is to draw each character, using the recommended spacing for the adjacent character as given by GDI+. This approach gives us total control over character spacing, at the expense of exact GDI+ kerning for text extents (whole words.) We think this is a trade-off worth making, since this is GUI Text (not WYSIWYG text) and the resulting
TextModel gives us full control over kerning anyway.
The text engine
Here’s how the text engine is implemented:
The result of the text wrapping process is the
TextModel is an object-based description of the text, down to the last character (as well as the spacing between each character.) This level of detail is not strictly needed for the La
bel control (we could get by with just Line objects), but kerning is needed for the
EditBox, so we went ahead and implemented the full solution. The
KerningPair classes on the right are a flyweight rendition containing measurements for a given font face (name, size, style.) The real work of assembling the
TextModel is done in the
TextWrapper class, which breaks the text down into lines and words using the
TextRuler for all measurements. The
LabelController uses the
TextJustifier to further process the
TextModel, pushing the words in each line around based on the
In testing the differences between our character placement method and GDI+, we went rather bezerk. We tested five different strings, with several different font faces. As a result, there are about 1,500 cases just to test the font painting. The upside is that it looks like our technique is sound: it doesn’t vary too much from GDI+ kerning. (If you download the source, you'll notice that these tests are not present - they simply made downloads too large.)
The new label control, in Tahoma 8 Regular, with full justification, a single pixel border (blue) and a two pixel margin (light blue.) Notice how the ascenders on the first line and the descenders on the last line touch the edge of the margin.
We are now strategically positioned to pursue the
EditBox control – that will be the topic of the next article.
R & D
Our arrival at this implementation was not without some serious detours. It was very important to us to stick with GDI+ and not break down to
GetABCCharWidths. Not just because we are trying to keep everything in native .NET, but also because of concerns about Unicode support. Beyond the P/Invoke approach, we considered these options:
- A game developer approach, where individual characters were blasted from a
Bitmap onto the screen. We found a CodeProject article on this as well as a couple of nice options on the internet at large:
In the end we couldn’t justify creating a
Bitmap for every font face used in the UI, it just seemed like too much overhead.
- A reverse “white-out” implementation. Here we extended the “measurement by side-effect scheme” by painting entire words on a
Bitmap, then erasing each letter of the word (working from last to first) using a specially created mask
Bitmap for each character. This technique gave us the exact character-spacing used in any given word – but it broke down when letters were kerned such that they overlapped. Workarounds were available for that problem, but the creation of the character masks slowed things down noticeably, so we abandoned the approach.
- A look into the FreeType open source project, especially the documentation for that product helped a lot. Reading this material should give you a clear idea that rendering text is no small thing, and that the chances of calculating “a priori” GDI+ rendering results is absolutely out of the question. It was the FreeType documentation which finally made us decide on a measurement by side-effect variant as the solution.
Needless to say the kerning algorithm used by GDI+ is very difficult to work around, particularly the behavior where words change kerning as they are formed. Our final approach resolves this problem by locking in the first kerning used for a letter combination. This should keep the text from “jumping around” as it is entered in the
An explosion of tests due to the kerning situation.
- Petzold - Chapter devoted to Text and Fonts on page 359.
- Microsoft - On GDI+ string rendering...