Click here to Skip to main content
15,883,812 members
Articles / Programming Languages / C#
Article

Article Four: Building a UI Platform in C# - Painting Text to the Pixel

,
Rate me:
Please Sign up or sign in to vote.
4.58/5 (25 votes)
24 Mar 200510 min read 181.6K   2K   82   38
GDI+: Getting control of MeasureString, DrawString and MeasureCharacterRanges.

Article Selector

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:

  1. Positioning to the pixel (with exact height and width), just like all other Graphics primitives.
  2. 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).
  3. 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

  1. DrawString puts extra space before and after the text it renders.
  2. MeasureString faithfully returns this extra space.
  3. GenericTypographic cannot be used to render text – at least not crisp GUI text.
  4. 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.
  5. 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):

Image 2

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:

  1. Left margin 10
  2. Right Margin 10
  3. Top Margin 10
  4. Bottom Margin 10
  5. Tahoma 8 Bold
  6. Tahoma 8 Italic
  7. Tahoma 8 Bold and Italic
  8. Change Font to Tahoma 10
  9. Change Text
  10. Change Location
  11. AutoSize off, Reduce Width to Truncate
  12. AutoSize off, Reduce Height to Truncate
  13. AutoSize off, Right Justify, Increase Width
  14. AutoSize off, Center, Increase Width
  15. 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:

Image 3

  • 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:

Image 4

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 Label 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.)

Image 5

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:

  1. 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.

  2. 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.
  3. 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

Image 6

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...

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
CEO Sagerion, LLC
United States United States
I read About Face by Alan Cooper in 1995 and immediately recognized it as a founding document for the future of software. I also recognized we had a long, long way to go - and yes, even with the advent of iOS, we are still not there yet.

At my company, Sagerion (say-jair-ee-on), we can take a look at your planned or existing software and suggest ways of making it better - lots better. We can develop down-to-the-pixel blueprints showing exactly what our suggestions mean. We can help manage on-going development to make sure the top-notch user-experience we've suggested really does get built. Now, honestly, how often have you ever seen all those things happen?

You may or may not already have great development going on - but what does that matter if you don't have great design driving it?

Feel free to contact me at tom@sagerion.com, I would love to hear about your next ground-breaking project.

Written By
Founder Sagerion LLC
United States United States
www.filoshare.com
-It is a fresh and free distributed source control system.

Comments and Discussions

 
QuestionArticle Five??? Pin
lokis3-Jun-07 18:55
lokis3-Jun-07 18:55 
General.NET 2 Pin
Christopher Wells20-Jan-07 2:37
Christopher Wells20-Jan-07 2:37 
GeneralRe: .NET 2 Pin
Tom Ollar22-Jan-07 3:19
Tom Ollar22-Jan-07 3:19 
GeneralRe: .NET 2 Pin
drstuckforhelp17-Nov-07 7:43
drstuckforhelp17-Nov-07 7:43 
QuestionGood series and a question? Pin
Rajesh Pillai22-Nov-05 3:57
Rajesh Pillai22-Nov-05 3:57 
AnswerRe: Good series and a question? Pin
Tom Ollar22-Nov-05 5:05
Tom Ollar22-Nov-05 5:05 
GeneralNice work Pin
LittleTiger14-Nov-05 22:00
LittleTiger14-Nov-05 22:00 
QuestionIs article Five out yet? Pin
Hai M. Nguyen11-Nov-05 1:57
professionalHai M. Nguyen11-Nov-05 1:57 
GeneralTMI, but having fun. Pin
Ashaman25-May-05 2:30
Ashaman25-May-05 2:30 
GeneralRe: TMI, but having fun. Pin
Jim Bennett25-May-05 6:40
Jim Bennett25-May-05 6:40 
QuestionFor this particular application, why are the pixel boundaries needed? Pin
Frank Hileman30-Mar-05 14:14
Frank Hileman30-Mar-05 14:14 
AnswerRe: For this particular application, why are the pixel boundaries needed? Pin
Tom Ollar31-Mar-05 6:33
Tom Ollar31-Mar-05 6:33 
GeneralRe: For this particular application, why are the pixel boundaries needed? Pin
Frank Hileman31-Mar-05 12:23
Frank Hileman31-Mar-05 12:23 
GeneralRe: For this particular application, why are the pixel boundaries needed? Pin
Tom Ollar31-Mar-05 15:05
Tom Ollar31-Mar-05 15:05 
GeneralRe: For this particular application, why are the pixel boundaries needed? Pin
Frank Hileman1-Apr-05 6:47
Frank Hileman1-Apr-05 6:47 
GeneralDType is worth a look Pin
noirs225-Mar-05 3:53
noirs225-Mar-05 3:53 
GeneralRe: DType is worth a look Pin
Jim Bennett25-Mar-05 6:39
Jim Bennett25-Mar-05 6:39 
GeneralRe: DType is worth a look Pin
noirs225-Mar-05 6:41
noirs225-Mar-05 6:41 
GeneralRe: DType is worth a look Pin
Jim Bennett25-Mar-05 6:48
Jim Bennett25-Mar-05 6:48 
GeneralRe: DType is worth a look Pin
noirs225-Mar-05 6:53
noirs225-Mar-05 6:53 
GeneralCropping text and ellipses Pin
jmw24-Mar-05 11:58
jmw24-Mar-05 11:58 
GeneralRe: Cropping text and ellipses Pin
Jim Bennett24-Mar-05 13:08
Jim Bennett24-Mar-05 13:08 
GeneralFlatlands Pin
jmw24-Mar-05 11:45
jmw24-Mar-05 11:45 
GeneralRe: Flatlands Pin
Jim Bennett24-Mar-05 11:52
Jim Bennett24-Mar-05 11:52 
GeneralRe: Flatlands Pin
jmw24-Mar-05 11:55
jmw24-Mar-05 11:55 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.