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 MeasureCharacterRanges
).
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 MeasureString
or 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 MeasureString
and 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 Changed
event.
ControlState
� holds a pool of ControlProperty
objects.
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 ControlProperty
and 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
. 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 Lab
el 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 FontMetric
-MasterCharacter
-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 TextAlignment
.
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 ExtTextOut
and 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 EditBox
.
Project Stats
An explosion of tests due to the kerning situation.
Downloads
Links
- Petzold - Chapter devoted to Text and Fonts on page 359.
- Microsoft - On GDI+ string rendering...