Friday, January 11, 2008

Making Multiline MeasureString work with different font sizes

Today I got an email asking for help using my suggested Multiline MeasureString implementation when the control has a different font size.

Well, in the provided sample code I'm using the default font size on all the controls. To get a simpler code, I'm using always the form graphics as parameter for MeasureString:

textboxHeight = CFMeasureString.MeasureString(CreateGraphics(), newText,textboxRect, true).Height;

CreateGraphics() as shown gets the Graphics object for the form. MeasureString will calculate the height based on the selected font on that Graphics.

Let's work now with the adaptative UI sample. What if we change the font size in one of the controls?

image image

Let's see what happen if we change it to 15:


The application will not work properly anymore, because the font of the label is not the same font of the provided Graphics:


Apparently, the solution could be to get the graphics from the control instead of still using the form graphics. We can create an overload for CreateGraphics like this:

private Graphics CreateGraphics(Control control)
return Graphics.FromHdc(control.Handle);

But it doesn't work because the Graphics won't have any font selected. .Net CF releases fonts after using them when i.e. you call DrawString (thanks god!)... that's the reason for having the same measure based on the default font size in spite of what graphics we are using.

The real solution

Obviously, the problem has a solution. We can create a new overload for MeasureString, which receives the control as one of its parameters and: 1)creates the right graphics object 2)select the control's font in the graphics and 3)releases it after the string has been measured.

To get all this working, we'll add two imports into our CFMeasureString class: GetDC and SelectObject.

Please include the following code as part of the CFMeasureString class:

static extern IntPtr GetDC(IntPtr hWnd);

static extern IntPtr SelectObject(IntPtr hDC, IntPtr hObject);

/// <summary>
Measure a multiline string for a Control
/// </summary>
/// <param name="control">
/// <param name="text">
string to measure</param>
/// <param name="rect">
Original rect. The width will be taken as fixed.</param>
/// <returns>
A Size object with the measure of the string according with the params</returns>
static public Size MeasureString(Control control, string text, Rectangle rect)
Size result = Size.Empty;
IntPtr controlFont = control.Font.ToHfont();
IntPtr hDC = GetDC(control.Handle);
using (Graphics gr = Graphics.FromHdc(hDC))
IntPtr originalObject = SelectObject(hDC, controlFont);
result = MeasureString(gr, text, rect, control is TextBox);
SelectObject(hDC, originalObject); //Release resources
return result;

In our sample application, we can replace the MeasureString calls in Form1.cs with the following:

labelHeight = CFMeasureString.MeasureString(label1, label1.Text, labelRect).Height;
textboxHeight = CFMeasureString.MeasureString(textBox1, newText, textboxRect).Height;
instructionsLabel.Height = CFMeasureString.MeasureString(instructionsLabel,instructionsLabel.Text, instructionsLabel.ClientRectangle).Height;

Now, even if we additionally change the textbox font size to 20, the application still working!


Thanks Sven for the feedback, I hope it helps!


Anonymous said...

this is some great stuff.
Excellent work.

Anonymous said...

Great Work, Thanks a lot!

Jim Lavin said...

Wanted to thank you a lot! This little post solved a problem I had been wrestling with for a couple of days!

Because of it, I now have a custom list control that has items that vary in size based on the post text.

You don't know how much time you saved me! I wish I would have found it a lot sooner.


Bahadır ARSLAN said...

Wonderful article, excellent work.

Anonymous said...

Cheers mate, nice one!

Jumping Matt Flash said...

I've created the following overload which accepts a font object instead of a control which was necessary in my situation:

static public Size MeasureString(Graphics gr, Font font, string text, Rectangle rect)
Size result = Size.Empty;
IntPtr controlFont = font.ToHfont();
IntPtr hDC = gr.GetHdc();
using (Graphics g = Graphics.FromHdc(hDC))
IntPtr originalObject = SelectObject(hDC, controlFont);
result = MeasureString(g, text, rect, false);
SelectObject(hDC, originalObject); //Release resources
return result;

Anonymous said...

Thanks a lot for the solution.

Tim Dawson said...

This code contains a leak. You should be calling DeleteObject passing the hFont you're creating, after you have selected it back out of the DC.

DanceOften said...

Thanx for the great article.
I'm trying to use it for calculating the TextBox.SelecionStart known a Drawing.Point into the text box client area. I must iterate all substrings starting from len 1 to Lenght and see if the size is outer than the point.
Is there a better way using a simple api to do this ?

Thanx for any response or suggestion.