Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Drawing Formatted Text in a Windows Forms Application using WPF FormattedText Class

0.00/5 (No votes)
22 Aug 2012 1  
How to draw muliline formatted text on a System.Drawing.Graphics object.

Introduction

Recently I came across the need to render some simple markup text (such as Lorem <b>ipsum</b> dolor sit amet, <u><i>consectetur adipisicing</u> elit, sed do eiusmod</i> tempor incididunt) into a column of a .NET DataGridView. So I decided to split the task into two: parsing the text for formatting information, and writing a custom DataGridViewCell with a custom Paint method. For the latter, I had to render the formatted text into the provided Graphics object and that's what this article is about. 

Background

Drawing multiline formatted text into a Graphics object turned out not to be as simple as it sounds like. There is nothing in the .NET platform to do this out of the box. I found some solutions using a RTF control but that was way too much overhead. Next I looked into coding it manually using font metrics, string measuring, etc., provided by the Font and Graphics classes, but quickly decided not to go that way - to do a good job using that probably would have taken weeks. Searching for a solution, I also came across the System.Windows.Media.FormattedText class, which was exactly what I needed, but it is WPF and cannot be drawn directly on a Graphics object. So I went back there and tried to figure out how I could make it work for me and here is what I came up with.

How it works  

The FormattedText class takes a string of characters and allows you to format arbitrary character ranges in a very simple way. Then you can provide MaxTextWidth and MaxTextHeight settings to define a layout rectangle for multiline rendering. If the text doesn't fit into the rectangle, the FormattedText object will display ellipsis as needed.

In this article, the following steps are taken to draw that output into a Graphics object:

On the WPF side:

  • Create and configure a FormattedText as needed
  • Draw the FormattedText on a DrawingVisual
  • Render the DrawingVisual into a RenderTargetBitmap

Interfacing between WPF and Windows Forms:

  • Create a System.Drawing.Bitmap
  • Copy the RenderTargetBitmap pixels into the bitmap's pixel buffer

On the Windows Forms side:

  •  Draw the bitmap on the Graphics object

Step by step

We begin with a given font and foreground color, usually taken from the form or control where we want to draw the formatted text. We also have a graphics and a layout rectangle to draw into.

Dim font As System.Drawing.Font
Dim color As System.Drawing.Color
Dim rectangle As System.Drawing.Rectangle
Dim graphics As System.Drawing.Graphics

Step 1: Create and configure a FormattedText 

The FormattedText constructor takes the following arguments:

Dim textToFormat As String
Dim culture As System.Globalization.CultureInfo
Dim flowDirection As System.Windows.FlowDirection
Dim typeface As System.Windows.Media.Typeface
Dim emSize As Double
Dim foreground As System.Windows.Media.Brush

As a text to format, we use Lorem ipsum:

textToFormat = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " + 
  "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, " + 
  "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. " + 
  "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore " + 
  "eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, " + 
  "sunt in culpa qui officia deserunt mollit anim id est laborum."

Then we have to derive the other arguments from our Windows Forms arguments.

For the culture, we just take the culture info from the current thread:

culture = System.Globalization.CultureInfo.CurrentCulture

Take the flow direction from the culture:

If culture.TextInfo.IsRightToLeft Then
    flowDirection = System.Windows.FlowDirection.RightToLeft
Else
    flowDirection = System.Windows.FlowDirection.LeftToRight
End If

Create a typeface based on the font:

typeface = New System.Windows.Media.Typeface(font.FontFamily.Name)

For the emSize, we have to convert: FormattedText expects emSize to be in device independent units, which is 1/96 inches. The font provides us a size in points, each point is 1/72 inches. So:

emSize = font.SizeInPoints * 96.0 / 72.0

The foreground brush can be easily created using the ARGB bytes from our color:

foreground = New System.Windows.Media.SolidColorBrush( _
                      System.Windows.Media.Color.FromArgb( _
                           color.A, color.R, color.G, color.B))

Now we have enough information to create a FormattedText object:

Dim formattedText = New System.Windows.Media.FormattedText( _
                    textToFormat, culture, flowDirection, typeface, emSize, foreground)

Now we can apply the font object's style information to our formatted text: the WPF equivalents to the FontStyle properties bold, italic, underline, and strikethrough.

Dim fontStyle As System.Windows.FontStyle
Dim fontWeight As System.Windows.FontWeight
Dim textDecorations = New System.Windows.TextDecorationCollection

If (font.Style And System.Drawing.FontStyle.Italic) <> 0 Then
    fontStyle = System.Windows.FontStyles.Italic
Else
    fontStyle = System.Windows.FontStyles.Normal
End If

If (font.Style And System.Drawing.FontStyle.Bold) <> 0 Then
    fontWeight = System.Windows.FontWeights.Bold
Else
    fontWeight = System.Windows.FontWeights.Normal
End If

If (font.Style And System.Drawing.FontStyle.Underline) <> 0 Then
    textDecorations.Add(System.Windows.TextDecorations.Underline)
End If

If (font.Style And System.Drawing.FontStyle.Strikeout) <> 0 Then
    textDecorations.Add(System.Windows.TextDecorations.Strikethrough)
End If

formattedText.SetFontStyle(fontStyle)
formattedText.SetFontWeight(fontWeight)
formattedText.SetTextDecorations(textDecorations)

Now we have a formatted text corresponding to our initial font and color. Let's apply some span formatting to the text: 50 characters italic starting at character index 5, 30 characters extrabold starting at character index 20, and 20 characters overline starting at character index 0.

formattedText.SetFontStyle(System.Windows.FontStyles.Italic, 5, 50)
formattedText.SetFontWeight(System.Windows.FontWeights.ExtraBold, 20, 30)
formattedText.SetTextDecorations(System.Windows.TextDecorations.OverLine, 0, 20)

Step 2: Draw the FormattedText on a Drawing Visual

Before we draw the formatted text we have to apply the layout rectangle to the formatted text using its MaxTextWidth and MaxTextHeight properties. Those properties expect values in device independent units, but our rectangle is in pixels. So we have to convert pixels into device independent units (1/96 inches) using the current Graphics object's DPI settings:

formattedText.MaxTextWidth = rectangle.Width / (graphics.DpiX / 96.0)
formattedText.MaxTextHeight = rectangle.Height / (graphics.DpiY / 96.0)

More details about DPI, points, device independent units, etc., can be found here: http://msdn.microsoft.com/en-us/library/windows/desktop/ff684173%28v=vs.85%29.aspx.

With the MaxTextWidth and MaxTextHeight properties set, the FormattedText object will now layout itself inside the given rectangle, performing word wrap and applying ellipses as needed. So now we can draw the formatted text on a WPF DrawingVisual.

Dim drawingVisual = New System.Windows.Media.DrawingVisual
Using drawingContext = drawingVisual.RenderOpen()
  drawingContext.DrawText(formattedText, New System.Windows.Point(0, 0))
End Using

Step 3: Render the DrawingVisual into a RenderTargetBitmap 

At this point we have to obtain the metrics for our formatted text - Width and Height - and convert them into pixels to create a RenderTargetBitmap.

First we have to measure the formatted text, using width and height properties. Again, those properties are in device independent units and have to be converted into pixel. Then we can create the RenderTargetBitmap.

Dim pixelWidth = System.Convert.ToInt32( _
        System.Math.Ceiling(formattedText.Width * (graphics.DpiX / 96.0)))
Dim pixelHeight = System.Convert.ToInt32( _
        System.Math.Ceiling(formattedText.Height * (graphics.DpiY / 96.0)))

Dim rtb = New System.Windows.Media.Imaging.RenderTargetBitmap( _
              pixelWidth, pixelHeight, graphics.DpiX, graphics.DpiY, _
              System.Windows.Media.PixelFormats.Pbgra32)

Step 4: Create a System.Drawing.Bitmap

The Windows Forms equivalent PixelFormat for Pbgra32 is Format32bppPArgb; the width and height of the bitmap are the same as the source RenderTargetBitmap:

Dim bitmap = New System.Drawing.Bitmap(rtb.PixelWidth, rtb.PixelHeight, _
                            System.Drawing.Imaging.PixelFormat.Format32bppPArgb)

Step 5: Copy the RenderTargetBitmap pixels into the bitmap's pixel buffer 

We get access to the Bitmap object's pixel buffer using the LockBits function, then we can use the RenderTargetBitmap.CopyPixels method:

Dim pdata = bitmap.LockBits(New System.Drawing.Rectangle( _
                    0, 0, bitmap.Width, bitmap.Height), _
                    System.Drawing.Imaging.ImageLockMode.WriteOnly, bitmap.PixelFormat)
rtb.CopyPixels(System.Windows.Int32Rect.Empty, _
                    pdata.Scan0, pdata.Stride * pdata.Height, pdata.Stride)
bitmap.UnlockBits(pdata)

Step 6: Draw the Bitmap on the Graphics object 

We are almost done, the last step is to draw the Bitmap object on the Graphics object, at the origin of the layout rectangle:  

graphics.DrawImage(bitmap, rectangle.Location)

Performance

The goal of this is to draw formatted text directly on forms and controls, so performance is important for the speed and responsiveness of the Windows Forms application's GUI.

I used the profiler in the SharpDevelop IDE with following results: 

The more expensive operations are:

  • Drawing the text on the DrawingVisual
  • Rendering the DrawingVisual into the RenderTargetBitmap
  • The Bitmap constructor 
  • Drawing the Bitmap on the Graphics object
  • Measuring the FormattedText (Width and Height properties) before drawing

Less expensive operations are:

  • Copying the pixels from the RenderTargetBitmap to the Bitmap 
  • Setting text formatting parameters
  • Measuring the FormattedText (Width and Height properties) after drawing

The performance difference in the text measuring exists because FormattedText uses cached metrics. Those metrics are cached whenever the text is drawn or measured, and invalidated whenever text/span properties are changed. If you access measuring properties and there are no valid cached metrics, the FormattedText object will internally draw the text in order to obtain the metrics.

Taking those results into account, I wrote a class of reusable code I called WindowsFormsFormattedText, trying to maximize performance by carefully caching DrawingVisuals, RenderTargetBitmaps, and Bitmaps.

For instance, to avoid the Bitmap constructor, it uses only one single Bitmap shared at class level - a new Bitmap is only created if the cached Bitmap is smaller than the RenderTargetBitmap. If it is equal or bigger, then it is reused. This requires synchronization on the code that uses the cached bitmap, but that's much cheaper than creating Bitmaps all the time or caching Bitmaps at object level.

For the caching to work efficiently, I also needed to know if there are cached metrics in the FormattedText object or not. Since that information is private to FormattedText, Reflection was used to obtain it. 

For implementation details, you can refer to the WindowsFormsFormattedText class attached to this article. 

A performance comparison between Graphics.DrawString and WindowsFormsFormattedText.Draw using the same profiler shows that Graphics.DrawString is two to four times faster than WindowsFormsFormattedText.Draw in the worst case (no caching of DrawingVisual and RenderTargetBitmap can be done), but WindowsFormsFormattedText is still fast enough to be used in Windows Forms Applications with excellent GUI performance.

Using the code    

The attached Zip contains two classes:

  • WindowsFormsFormattedText does all the work and
  • FormattedTextLabel is a control derived from Label and can be used on forms etc.

Note for C# users: the code should be easy to convert, just pass it through the code conversion tool from Developer Fusion, freely available on the web and in the SharpDevelop IDE.

Add the classes to your project and reference two assemblies: FoundationCore and WindowsBase. Those assemblies are included in the framework since version 3.0, so no external dependencies are needed.

The WindowsFormsFormattedText class maintains internally a FormattedText object and exposes all of its members regarding text formatting.Those members are documented here: http://msdn.microsoft.com/en-us/library/system.windows.media.formattedtext.aspx

Aditionally, it adds methods and properties with arguments from the Windows Forms world to automate the needed conversions. For example, in addition to the TextWidth property there is a TextPixelWidth(dpiX) property. For the built-in SetTypeface methods there are equivalent SetFont methods and so on.  

The SetStyle(System.Drawing.FontStyle) method sets all of the Windows Forms font styles properties (bold, italic, underline, and strikethrough) at once. So if you do, for example, SetStyle(FontStyle.Bold Or FontStyle.Italic, 20, 30), it will remove underline and strikethrough for the given character range, which might not be what you have expected. To set individual style properties without affecting other style properties, you have to use the built-in methods SetFontWeight, SetFontStyle and SetTextDecorations.

Note: The SetStyle(System.Drawing.FontStyle) method was a SetFontStyle overload in the first uploaded version, but I renamed it because it was missleading: in FormattedText that method applies only to normal, italic or oblique.

It also exposes a Draw(Graphics, ...) method which automates the drawing of the formatted text on a Graphics object.  

FormattedTextLabel exposes a FormattedText property providing access to an internal WindowsFormsFormattedText object allowing to format the text to be displayed on the label. It is derived from Label for convenience, a better implementation probably would be derived directly from Control. But the label was not my goal, I wrote it just for testing.

Flicker: Since the drawing on the Graphics object is done with a Bitmap, flicker may occur under some circumstances. If you experience flicker, consider to double buffer the flickering controls. 

Here is some sample code. To use it, create a form and drop two buttons on it, one called PerformanceTestButton and the other LabelTestButton.

  1. The code used for the performance test:
  2. Private endPerformanceTest As Boolean
    Private performanceTestForm As Form
    
    Private Sub PerformanceTestButton_Click(ByVal sender As System.Object, _
            ByVal e As System.EventArgs) Handles PerformanceTestButton.Click
        performanceTestForm = New Form
        performanceTestForm.Text = "Performance test - close form to finish."
        performanceTestForm.CreateControl()
        performanceTestForm.Font = New Font(performanceTestForm.Font.FontFamily, 12.0!)
        Dim t = New System.Threading.Thread(AddressOf PerformanceTest)
        t.Start()
        performanceTestForm.ShowDialog()
        endPerformanceTest = True
    End Sub
    
    Sub PerformanceTest()
    
        Dim rnd = New System.Random
    
        Dim textSample = "Lorem ipsum dolor sit amet, consectetur adipisicing elit," & _ 
            " sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " & _ 
            "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris " & _ 
            "nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in " & _ 
            "reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " & _ 
            "Excepteur sint occaecat cupidatat non proident, sunt in culpa qui " & _ 
            "officia deserunt mollit anim id est laborum."
        Dim fText = New WindowsFormsFormattedText(textSample, performanceTestForm.Font)
    
        endPerformanceTest = False
    
        Do
            Dim x = rnd.Next() Mod 10
            Dim y = rnd.Next() Mod 10
            Dim w = 10 + (rnd.Next() Mod (performanceTestForm.Width - 20))
            Dim h = 10 + (rnd.Next() Mod (performanceTestForm.Height - 20))
    
            Using g = performanceTestForm.CreateGraphics()
                fText.MaxTextPixelWidth(g.DpiX) = w
                fText.MaxTextPixelHeight(g.DpiY) = h
    
                Try
                    fText.Draw(g, x, y)
                Catch ex As Exception
                    System.Diagnostics.Debug.Print(ex.Message)
                End Try
    
                Try
                    g.DrawString(textSample, performanceTestForm.Font, Brushes.Black, _
                          New RectangleF(x, y, w, h), System.Drawing.StringFormat.GenericDefault)
                Catch ex As Exception
                    System.Diagnostics.Debug.Print(ex.Message)
                End Try
            End Using
    
        Loop Until endPerformanceTest
    End Sub
  3. The code used to test the FormattedTextLabel:
  4. Private Sub LabelTestButton_Click(ByVal sender As System.Object, _
                ByVal e As System.EventArgs) Handles LabelTestButton.Click
        Dim f = New Form()
        f.Text = "FormattedTextLabel Test"
        f.Width = 600
        f.Height = 400
    
        Dim l = New FormattedTextLabel
        l.Text = "Lorem ipsum dolor sit amet, consectetur adipisicing elit, " & _ 
             "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. " & _ 
             "Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi " & _ 
             "ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit " & _ 
             "in voluptate velit esse cillum dolore eu fugiat nulla pariatur. " & _ 
             "Excepteur sint occaecat cupidatat non proident, sunt in culpa " & _ 
             "qui officia deserunt mollit anim id est laborum."
        l.FormattedText.SetFontStyle(FontStyle.Bold Or FontStyle.Italic, 0, 30)
        l.FormattedText.SetFontSizeInPoints(18.0, 200, 60)
        l.FormattedText.SetForegroundColor(Color.Aquamarine, 100, 120)
    
        l.BackColor = Color.White
        l.BorderStyle = BorderStyle.FixedSingle
        l.SetBounds(50, 50, 500, 300)
        l.Anchor = AnchorStyles.Bottom Or AnchorStyles.Left Or AnchorStyles.Right Or AnchorStyles.Top
        f.Controls.Add(l)
    
        f.ShowDialog()
    
    End Sub

This is the result:

I hope this code is useful for your project. In my next article I am going to write about the development of a DataGridViewFormattedTextCell and a DataGridViewFormattedTextColumn based on the WindowsFormsFormattedText and a helper class I called SimpleMarkupText which parses text with simple markup like <b>, <u>, <font color...> etc. into a WindowsFormsFormattedText object.

History

  • Article submitted: August 2012.
  • Updated text and downloads: August 22
    • Renamed SetStyle(System.Drawing.FontStyle) to SetStyle(System.Drawing.FontStyle) 
    • Added Draw(graphics, ..., clipBounds) overloads: taking into account the clipBounds (or clipRectangle) rectangles usually provides in OnPaint overrides or Paint events, performance can be improved when the drawing surface is partially covered by other windows or out of screen.

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