Formatting text on a common baseline.

Text formatting using GDI+ is, let’s face it, a pain. Labels are useless, the RichTextBox is so bemired in the old world of Win32 that it’s next to useless and GDI+’s drawing capabilities vis-à-vis text is next to zero. This implies that to get any sort of decent text formatting from GDI+ one must use the good old pedestrian method and do the lot oneself.

One of the most requested features is to be able to display superscript or subscript text in relation to a line in construction. Another frequent request is to simply be able to mix fonts on the same line. Both of these are relatively difficult in GDI+ simply because text positioning takes place in reference to the top-left corner of the text rather than the bottom.

To better understand the process we must first understand how text is drawn and what information is available to us to make the text output consistent. One of the first mistakes a newbie GDI+ programmer will make is to blast out a string of text using DrawString and some carefully selected fonts and then be so deliriously happy with the result that they don’t see the pitfalls. For example…

    private int StringLength(Graphics g, string s, Font f)

    {

      StringFormat sf=(StringFormat)StringFormat.GenericTypographic.Clone();

      sf.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces;

 

      return (int)g.MeasureString(s,f,1024,sf).Width;

    }

 

    private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e)

    {

      const string part1 = "Here is some ";

      const string part2 = "bold ";

      const string part3 = "and italic ";

      const string part4 = "text.";

 

      int currentOffset=0;

 

      Font f=new Font("Verdana",12,GraphicsUnit.Point);

 

      e.Graphics.DrawString(part1,f,Brushes.Black,currentOffset,10, StringFormat.GenericTypographic);

 

      currentOffset+=this.StringLength(e.Graphics,part1,f);

 

      f=new Font("Verdana",12,FontStyle.Bold,GraphicsUnit.Point);

 

      e.Graphics.DrawString(part2,f,Brushes.Black,currentOffset,10, StringFormat.GenericTypographic);

 

      currentOffset+=this.StringLength(e.Graphics,part2,f);

 

      f=new Font("Verdana",12,FontStyle.Italic,GraphicsUnit.Point);

 

      e.Graphics.DrawString(part3,f,Brushes.Black,currentOffset,10, StringFormat.GenericTypographic);

 

      currentOffset+=this.StringLength(e.Graphics,part3,f);

 

      f=new Font("Verdana",12,GraphicsUnit.Point);

 

      e.Graphics.DrawString(part4,f,Brushes.Black,currentOffset,10, StringFormat.GenericTypographic);

    }

    Private Function StringLength(ByVal g As Graphics, ByVal s As String, ByVal f As Font) As Integer

      Dim sf As StringFormat = CType(StringFormat.GenericTypographic.Clone(), StringFormat)

      sf.FormatFlags = sf.FormatFlags Or StringFormatFlags.MeasureTrailingSpaces

 

      Return CInt(g.MeasureString(s, f, 1024, sf).Width)

    End Function 'StringLength

 

 

    Private Sub Form1_Paint(ByVal sender As Object, ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint

      Const part1 As String = "Here is some "

      Const part2 As String = "bold "

      Const part3 As String = "and italic "

      Const part4 As String = "text."

 

      Dim currentOffset As Integer = 0

 

      Dim f As New Font("Verdana", 12, GraphicsUnit.Point)

 

      e.Graphics.DrawString(part1, f, Brushes.Black, currentOffset, 10, StringFormat.GenericTypographic))

 

      currentOffset += Me.StringLength(e.Graphics, part1, f)

 

      f = New Font("Verdana", 12, FontStyle.Bold, GraphicsUnit.Point)

 

      e.Graphics.DrawString(part2, f, Brushes.Black, currentOffset, 10, StringFormat.GenericTypographic))

 

      currentOffset += Me.StringLength(e.Graphics, part2, f)

 

      f = New Font("Verdana", 12, FontStyle.Italic, GraphicsUnit.Point)

 

      e.Graphics.DrawString(part3, f, Brushes.Black, currentOffset, 10, StringFormat.GenericTypographic))

 

      currentOffset += Me.StringLength(e.Graphics, part3, f)

 

      f = New Font("Verdana", 12, GraphicsUnit.Point)

 

      e.Graphics.DrawString(part4, f, Brushes.Black, currentOffset, 10, StringFormat.GenericTypographic))

 

    End Sub 'Form1_Paint

This chunk of code gives the following result. Not bad at first glance.

 Problems arise however when we substitute a font, such as Times New Roman for the ever lovely Verdana and output it to the same vertical position. Here’s the result…

You can see that the result is pretty nasty. The bottom of the text is at different heights depending on the font. The reason for this is that the text has been placed by DrawString using the top-left extent of the string as a reference point and the two fonts have two completely different ascent measurements.

If you’re wondering what the font ascent is scoot over to the Beginners Guide right away and read the part 1 article on text.

Clearly then, to make the text look any where near pretty, as pretty as is humanly possible with such a horrible font as Times New Roman, we need to be looking at the font’s baseline as a positioning reference, not the top of it’s EM square.

To do this, we can use some of the sparse data provided to us by the FontFamily to ascertain the true offset between the font’s upper extent and its declared baseline and use that to draw our font. The calculation is simple and goes something like:

BaselineOffset=SizeInPoints / EM-height * Cell-ascent

Another calculation required is to convert from points to pixels; this enables the positioning of the text at a specific vertical pixel position. The calculation is also fairly straightforward and goes like this:

Pixels = DPIY / 72 * points

DPIY is the vertical dots-per-inch as reported by the Graphics object.

Once we have this information and these conversions it becomes possible to draw text on a given baseline using the method below.

      private void DrawOnBaseline(string s, Graphics g, Font f, Brush b, Point pos)

    {

      float baselineOffset=f.SizeInPoints/f.FontFamily.GetEmHeight(f.Style)*f.FontFamily.GetCellAscent(f.Style);

      float baselineOffsetPixels = g.DpiY/72f*baselineOffset;

      

      g.DrawString(s,f,b,new Point(pos.X,pos.Y-(int)(baselineOffsetPixels+0.5f)),StringFormat.GenericTypographic);

    }

        Private Sub DrawOnBaseline(ByVal s As String, ByVal g As Graphics, ByVal f As Font, ByVal b As Brush, ByVal pos As Point)

            Dim baselineOffset As Single = f.SizeInPoints / f.FontFamily.GetEmHeight(f.Style) * f.FontFamily.GetCellAscent(f.Style)

            Dim baselineOffsetPixels As Single = g.DpiY / 72.0F * baselineOffset

 

            g.DrawString(s, f, b, New PointF(pos.X, pos.Y - CInt(baselineOffsetPixels + 0.5F)), StringFormat.GenericTypographic)

        End Sub 'DrawOnBaseline

The DrawOnBaseline method can be used to mix fonts of different sizes on a line, even up fonts of the same declared height but different faces or even to create superscript or subscript text.

Visit Windows Forms Tips and Tricks

Return to the GDI+ FAQ

Copyright © Ramuseco Limited 2004-2006 All Rights Reserved.