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

Bypass Graphics.MeasureString limitations

Rate me:
Please Sign up or sign in to vote.
4.73/5 (45 votes)
22 Apr 2003LGPL31 min read 427.4K   89   69
This sample code computes the width of the string, as drawn by Graphics.DrawString

Sample Image - measurestring.gif

Introduction

Graphics.MeasureString can be used to compute the height and width of a text string. Often, however, the dimensions returned do not match the size of what gets drawn on screen when calling Graphics.DrawString. The red box above shows the dimensions returned by Graphics.MeasureString, which is about an em too wide...

The differences between what is computed and what is really drawn on the screen are related to how GDI+ computes its widths when using hinting and antialiasing. Here are the  gory details. A known work-around is to make GDI+ display its string antialiased, in which case the measured width matches the displayed result. If you want to draw standard strings (to match the GUI appearance, for instance), you are left out.

First, naive solution

The code I present here can be inserted into any class which needs to compute the real width of a string (shown by the yellow background above). The trick I use to compute the real string width is to ask GDI+ to draw the string into a bitmap and then find the position of the last character by reading back the pixels. A few optimisations ensure that this gets done as fast as possible (small bitmap, few pixels).

C#
static public int MeasureDisplayStringWidth(Graphics graphics, string text,
                                            Font font)
{
    const int width = 32;

    System.Drawing.Bitmap   bitmap = new System.Drawing.Bitmap (width, 1, <BR>                                                                graphics);
    System.Drawing.SizeF    size   = graphics.MeasureString (text, font);
    System.Drawing.Graphics anagra = System.Drawing.Graphics.FromImage(bitmap);

    int measured_width = (int) size.Width;

    if (anagra != null)
    {
        anagra.Clear (Color.White);
        anagra.DrawString (text+"|", font, Brushes.Black,
                           width - measured_width, -font.Height / 2);

        for (int i = width-1; i >= 0; i--)
        {
            measured_width--;
            if (bitmap.GetPixel (i, 0).R != 255)    // found a non-white pixel ?
                break;
        }
    }

    return measured_width;
}

That's all, folks. Right-to-left scripts won't probably work with this piece of code.

Another solution...

It is also possible to get the accurate string geometry by using MeasureCharacterRanges, which returns a region matching exactly the bounding box of the specified string. This is faster and more elegant than the first solution I posted on CodeProject.

C#
static public int MeasureDisplayStringWidth(Graphics graphics, string text,
                                            Font font)
{
    System.Drawing.StringFormat format  = new System.Drawing.StringFormat ();
    System.Drawing.RectangleF   rect    = new System.Drawing.RectangleF(0, 0,
                                                                  1000, 1000);
    System.Drawing.CharacterRange[] ranges  = <BR>                                       { new System.Drawing.CharacterRange(0, 
                                                               text.Length) };
    System.Drawing.Region[]         regions = new System.Drawing.Region[1];

    format.SetMeasurableCharacterRanges (ranges);

    regions = graphics.MeasureCharacterRanges (text, font, rect, format);
    rect    = regions[0].GetBounds (graphics);

    return (int)(rect.Right + 1.0f);
}

Post Script

Both functions only work with non-empty strings. The second solution will strip the trailing spaces; the first solution will take them in account. Choose the one which best fits your needs

License

This article, along with any associated source code and files, is licensed under The GNU Lesser General Public License (LGPLv3)


Written By
Web Developer
Switzerland Switzerland

Pierre Arnaud got a Ph.D. in computer science at the Swiss Federal Institute of Technology; he currently works both as an independent contractor on hardware and software projects at OPaC bright ideas and as a senior software designer at EPSITEC.


Pierre was a key contributor to the Smaky computer, a real time, multitasking system based on the Motorola 680x0 processor family.


Now, Pierre works on his spare time for the Creative Docs .NET project: it is a vector based drawing and page layout software based on .NET and AGG.


Comments and Discussions

 
Generalnon integer font size Pin
compumaster12-Mar-03 11:42
compumaster12-Mar-03 11:42 
GeneralRe: non integer font size Pin
Pierre Arnaud13-Apr-03 20:47
Pierre Arnaud13-Apr-03 20:47 
GeneralA better way to measure strings. Pin
20-Jun-02 5:24
suss20-Jun-02 5:24 
GeneralRe: A better way to measure strings. Pin
dog_spawn23-Apr-03 10:25
dog_spawn23-Apr-03 10:25 
GeneralRe: A better way to measure strings. Pin
netmar7-Jul-04 22:19
netmar7-Jul-04 22:19 
Generalquestion Pin
15-Apr-02 2:30
suss15-Apr-02 2:30 
GeneralRe: question Pin
15-Apr-02 22:36
suss15-Apr-02 22:36 
GeneralYet Another Solution Pin
Alastair Stell29-Jul-02 23:25
Alastair Stell29-Jul-02 23:25 
Dear Pierre,

Thank you for suggesting the MeasureCharacterRanges() method as a technique to provide an accurate report of a string's length and height. I am very grateful to you.

I originally observed that I still found small errors from this method, however on closer examination it appears that:

1) I needed to set FormatFlags to MeasureTrailingSpaces; and
2) this function returns a rectangle of floating point numbers. when working in pixels, the Rectangle.Round() method I was using in my calculations introduced an accumulating error.

MeasurableCharacterRanges() is therefore the preferred solution to the MeasureString problem, however I have included the relevant code for anyone who wants to wrap GDI calls to achieve the same ends (using DrawText()):

Here are the C# GDI declarations (I wrapped these into a class called gdiapi)

[DllImport("gdi32.dll")]
public static extern int CreateFont(
int nHeight,
int nWidth,
int nEscapement,
int nOrientation,
int nWeight,
uint bItalic,
uint bUnderline,
uint cStrikeOut,
uint nCharSet,
uint nOutPrecision,
uint nClipPrecision,
uint nQuality,
uint nPitchAndFamily,
String lpszFacename );

The rectangle structure for DrawText():

[StructLayout(LayoutKind.Sequential)]
public struct CRECT
{
public int left;
public int top;
public int right;
public int bottom;
}

[DllImport("user32.dll")]
public static extern int DrawText(
int hDc,
String lpStr,
int nCount,
ref CRECT lpRect,
int wFormat);

Here is the declaration for SelectObject():

[DllImport("gdi32.dll")]
public static extern int SelectObject(int hDc, int hObject);

And so I could draw a line around my text:

[DllImport("gdi32.dll")]
public static extern int LineTo(int hDc, int x, int y);

For my example, I created a simple form with a PictureBox control called 'pcbOne'. I then created a Bitmap object and attached it to the Image property of 'pcbOne' as shown below:

Bitmap aBmp = new Bitmap(600, 400, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
pcbOne.Image = aBmp;

(you probably want to fill the bitmap with a background color at this point)

To use:

// We need the Device Context first
Graphics eg = Graphics.FromImage(pcbOne.Image);
int iHdc = (int)eg.GetHdc();

// must create a font because the hdc from GetHdc() has no font object attached!

// for no apparent reason, I now select maroon for the text color
SetTextColor(iHdc, 343434);

// you can get font height by creating a Font object and calling Font.ToLogFont() to
// populate a LOGFONT object (code ommited because it's two am and I'm tired). I just
// happen to know that -21 corresponds to a font size of 15.75F
int hfFont = gdiapi.CreateFont(-21,0,0,0,0,0,0,0,0,0,0,0,0, "Times New Roman");

// must select the font into the Device Context
int iOldObj = gdiapi.SelectObject(iHdc, hfFont);

// initialize a rectangle object (there is an api to do this too!)
gdiapi.CRECT aRect;
aRect.left = 0;
aRect.top = 0;
aRect.right = 0;
aRect.bottom = 0;

// et voila! now we can size strings and draw them (note that 1024 is DT_CALCRECT)
String aStr = "Calculate the Test size of equation 12 * 4.56 + 12e";

// first measure the rectangle needed to display the text
int i = gdiapi.DrawText(iHdc, aStr, aStr.Length, ref aRect, 1024);

// rectangle now contains the appropriate width and height (ie. right and bottom)
// so we can now draw our text
i = gdiapi.DrawText(iHdc, aStr, aStr.Length, ref aRect, 0);

// and a nice box to show the fit is good
gdiapi.LineTo(iHdc, 0, 0);
gdiapi.LineTo(iHdc, 0, aRect.bottom);
gdiapi.LineTo(iHdc, aRect.right, aRect.bottom);
gdiapi.LineTo(iHdc, aRect.right, 0);
gdiapi.LineTo(iHdc, 0, 0);

// what you should now have is a maroon line of text with a rectangle drawn around it
// in my actual code I only create the font object once (in the constructor for a
// gdiapi object) but I do (un)SelectObject() the font after each use (code not here)

// release resources
eg.ReleaseHdc((System.IntPtr)iHdc);
eg.Dispose();
pcbOne.Refresh();

I apologise if I've accidentally included a typo here or there. I am left wondering if Microsoft deliberately sabotaged the MeasureString API. I've found several other APIs which don't quite work 'as advertised' and they all seem to be APIs that SHOULD work first time around. I'd love to know when the Evil Empire plans on fixing these problems!

I know the best solution is to try and stay within GDI+, but at least this code also serves to illustrate the implimentation of calls to GDI and USR DLLs, as well as providing a more-or-less perfect-in-all situations attempt to measure strings.
Smile | :)
Regards, and thanks again, Alastair Stell


Only change is constant

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.