Introduction

I believe that almost every .NET developer, assigned a task to display text, has experienced hard times with precise text measuring and drawing. The System.Drawing.GraphicsMeasureString and DrawString methods have several limitations, worst of all being not so accurate measuring and positioning of the desired text. Since .NET 2.0, Microsoft has introduced the TextRenderer class that provides more precise text manipulation, but also has its cons – may render only with solid colors, no transparency, etc. Neither of the above described approaches gives you the opportunity to have mixed font texts, extended paragraph layouts like justify, or additional text effects like stroke or shadow. All that said, I thought it would be a nice exercise to create a custom text rendering solution that both solves the standard problems and also adds some nice features. Having solid experience with GDI+ and Windows Forms (I was a GUI developer for over 4 years), I started this project about a month ago, and had contributed to it for an hour or two almost every day. There are many areas that one may or may not find useful, and many parts I consider tricky, so I do hope this article will be useful for many folks.

Who May be Interested in this Project

This project mainly targets the Windows Forms platform. Although, in theory, it may be used to create off-screen graphics for the ASP.NET engine, I doubt that web developers will rely on this solution, having the Web Browser (and all its HTML formatting capabilities) as their main visual surface. The included control provides basic text formatting features, and may be used as a light-weight substitution for the heavy IE ActiveX control.

Using the Code

There are two main aspects of this solution:

An abstract implementation – GTextView - that is completely detached from a certain platform (Windows Forms vs. ASP.NET). You may use this abstraction to display text on any Graphics surface. For example, you may extend a ListBox whose items will be rich-text enabled.

A Windows Forms control – GMarkupLabel - that composes the above class. What that control does is simply delegates paint and mouse events to the internal text view.

Please note that you may find some other attributes declared in the source, but those are not yet implemented.

Brief Model Overview

Behind the GMarkupLabel lies a framework of abstract hierarchies that form a light-weight DOM tree. There is a strict distinction between the Model and the View layers. The System.Xml.XmlDocument class is used to parse the provided raw text to an XML tree, and a model DOM is built on top of the already parsed text. The model structure is used by a GTextView instance to populate its child visuals (like GParagraph, GTextBlock) and layout atoms (like GTextLine, GWord). I will not cover every aspect of the framework, and will focus on some interesting and tricky parts instead.

GTextView Inside

Overview

The GTextView class is an abstract implementation that is completely detached from any specific platform (e.g., Windows Forms or ASP.NET), and provides the core logic for parsing a GTextDocument and organizing it into paragraphs, text blocks, lines, and words. Once parsed, the internal element structure looks like:

GTextView

GParagraph

GTextBlock

GTextLine

GWord

GWord

GWord

There are three different aspects of the View – how it is parsed, the way it is laid-out, and how it is visualized on the screen. The main approach is to split the entire text (excluding tags) into separate words, and to organize them in a composite structure. On top of the layout tree is the paragraph which consists of one or more text blocks (each block indicates a line break within the paragraph). A text block knows how many words it contains, and lays them out in lines, where each line contains one or more words, and calculates each word’s location. While paragraphs, text blocks, and words are parsed only once, lines are built dynamically, upon each layout request. So far, we have words that are laid out on the screen. Each word is associated with certain string, metric members and a style object which carries all the information needed by the view to display the word on the screen.

Parsing

Once we have the System.Xml.XmlDocument, we create our DOM tree from this document. Each XML element has its light-weight DOM equivalent, and each XML attribute is mapped to a property of that DOM element. Further on, we populate our text view with paragraphs, text blocks, and text lines. Following is a code snippet from the GTextDocumentParser:

We have two Stacks here – one is used to Push and Pop paragraphs (nested paragraphs are supported), and the other is used to track the GTextStyle of each word. Before the recursive call, we check whether a new paragraph and style needs to be pushed, and after the recursion is finished, we restore the state of the Stacks (if modified).

Measuring

In order to provide precise layout, we need a way to tell how many pixels a word is going to occupy. As mentioned in the introduction, a simple Graphics.MeasureString call will not do the job – it will always add few or more additional pixels on the returned value. What I am using as an approach here is to first render the word on an off-screen bitmap, then perform some per-pixel operations on this bitmap to determine the render padding from edges, and then calculate the so called "Black Box" (the smallest rectangle that completely encloses the displayed text):

Whitespaces are special case. Instead of measuring each whitespace’s size, I simply keep a value named WhitespaceWidth in each GFontDeviceMetric. This ensures that we measure the width of a whitespace only once, during initialization of each font’s device metrics.

The algorithm which calculates the padding is pretty simple – we loop through each pixel of the bitmap, starting from each edge, and look for a pixel that is different from the background color. The following snippet demonstrates how we calculate left edge's padding:

Now is the time to say my thanks to the author of this article. The GBitmap uses some approaches and tricks from q123456789'sFastBitmap. For example, manipulating a bitmap’s data directly by unsafe code speeds-up pixel information retrieval drastically. Using .NET’s Bitmap.GetPixel is slow as hell, and would simply not work in this case, where each word is painted off-screen and quite many GetPixel calls are made.

Layout

So far, we have words whose exact size is measured. Now, we need to calculate the location of each word in the view. Here come paragraphs, text blocks, and text lines. Let's have a look at the GTextBlock class and how it organizes its words in lines (a snippet from the BuildLines method):

Once we have built all the lines, we need to calculate each word's location. This calculation depends on several things: padding, alignment, and mixed font baseline. The interesting part here is how we determine the font baseline. Given the fact that we may want to display words in different fonts on the same text line, we should align them in such a way that they all lie on a logical line which separates each word's Ascent and Descent. What I am using as an approach here is to find the word with the highest Ascent and then align each word with this value.

An interesting thing to point to is the fact that I am using the GDI TEXTMETRIC and its tmAscent value. At first, I played with the EmHeight value returned by the FontFamily.GetEmHeight method. The problem was that this value is a single-precision floating-point number, and sometimes (most probably due to an internal rounding by the rendering engine), words were not precisely aligned – a one-pixel error occurred. On the other hand, GDI and TEXTMETRIC returns an integer number, with a rounding already applied, which turned to be much more precise.

Painting

Painting is the easiest part. Once we have laid-out all the words, we need to simply enumerate and paint them. I will open a bracket here and explain a bit more about the paint logic used in the underlying framework. Instead of working directly with GDI+ objects such as Pens and Brushes (all associated with a Handle, cannot be edited at design-time), I have their abstract counter-parts such as GBrush and GPen. In parallel, I have an abstract hierarchy of GDeviceContext interfaces. Currently, the only concrete implementation is the GGdiPlusDeviceContext which is associated with a System.Drawing.Graphics instance. The idea behind is to have as detached from a specific platform paint logic as possible. It is the Device implementation that knows how to map a GDrawingAttribute to a native device paint object – e.g., an abstract GSolidBrush is mapped to a concrete System.Drawing.SolidBrush. The GDI+ Device keeps a cache of native drawing primitives in a Hashtable, where each key is the abstract drawing attribute and the value is its native counterpart. This speeds up the rendering logic significantly.

Decorations – Stroke and Shadow

What one may find as nice and useful features are the Stroke and Shadow support per word. These two decorations are defined by the <stroke> and <shadow> tags, respectively. As you may have already guessed, I am using a System.Drawing.Drawing2D.GraphicsPath to implement the stroke logic. As creating a path by a string is generally an expensive operation, each word (if stroked) keeps a cached bitmap with its visual representation. This bitmap is once created when the word is measured. The same is true for the shadow - it uses the Blur mechanism of q123456789'sFastBitmap. Blurring is quite an expensive per-pixel operation, and caching the result on a bitmap is almost a must.