views:

775

answers:

5

I am trying to calculate the pixel width of Excel columns, as described in this post, using the official formula from the OpenXML specification. However, in order to apply this formula, I need to know the Maximum Digit Width of the Normal font, which is the pixel width of the widest numeric digit. The OpenXML specification gives this example as a clarification:

Using the Calibri font as an example, the maximum digit width of 11 point font size is 7 pixels (at 96 dpi).

I have checked that this is correct by visually examining a Calibri 11-point digit and it is indeed 7 pixels wide. So, I am trying to create a method that will return the maximum digit width for any font / size.

I have followed the recommendations made in this question, but it doesn't produce the results I am expecting.

Here's my test code:

var font = new Font("Calibri", 11.0f, FontStyle.Regular);

for (var i = 0; i < 10; i++)
{
    Debug.WriteLine(TextRenderer.MeasureText(i.ToString(), font));
}

This reports that all digits have a width of 15.

Any suggestions please?

Thanks, Tim

+2  A: 

Hello Tim, I am not 100% sure it is what you exactly need.

But you may want to have a look at this Microsoft page:

BlueTrin
+4  A: 

TextRenderer is not always accurate the way you would expect:

From MSDN: http://msdn.microsoft.com/en-us/library/8wafk2kt.aspx

For example, the default behavior of the TextRenderer is to add padding to the bounding rectangle of the drawn text to accommodate overhanging glyphs

Do you have a Graphics object or a Control object? If you are using these you can get accurate restults the way you would expect.

Take a look at http://msdn.microsoft.com/en-us/library/6xe5hazb%28VS.80%29.aspx

Bob
Careful with MeasureString - it also pads: "The MeasureString method is designed for use with individual strings and includes a small amount of extra space before and after the string to allow for overhanging glyphs." Also, be aware that measuring an individual glyph may not give accurate results out of context. The sum of widths of characters of a string is not always the width of the string, due to kerning.
plinth
@plinth: Yes, I tried my code using MeasureString and it reports that the 7-pixel digits have a width of 9.408365 pixels.
Tim Coulter
+2  A: 

Measuring text widths can be a nighmare.. I dont know why they made it so confusing..

Heres the most accurate way Ive got to measure the length of text :

    protected int _MeasureDisplayStringWidth ( Graphics graphics, string text, Font font )
 {
  if ( text == "" )
   return 0;

  StringFormat format = new StringFormat ( StringFormat.GenericDefault );
  RectangleF rect = new RectangleF ( 0, 0, 1000, 1000 );
  CharacterRange[] ranges = { new CharacterRange ( 0, text.Length ) };
  Region[] regions = new Region[1];

  format.SetMeasurableCharacterRanges ( ranges );
  format.FormatFlags = StringFormatFlags.MeasureTrailingSpaces;

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

  return (int)( rect.Right );
 }
Mongus Pong
@Fungus: Thanks, your code got closest to the correct answer, reporting the 7-pixel digits as being 8 pixels wide. Unfortunately, an error of 1 pixel will scale up to a much larger error in my application. Also, it seems that the arguments to the Font constructor affect the result. Is there a correct way to construct a Font object to represent 11pt Calibri?
Tim Coulter
Hmmm... it is possible that this will return 1 pixel wider as it is measuring the space the characters are taking up, which would then take into account the border around the character.
Mongus Pong
+1  A: 

First let me say that the code I'm about to show looks like a horrible hack and I'm not presenting it as a solution. Rather, I'm hoping it reveals a little more about the behavior of MeasureText that might lead you to your final solution.

I made a slight alteration to your code. I'm measuring the length of two underscore characters. Then, in the body of the for loop I'm measuring the desired character with underscores on either side and subtracting the length of the two underscore characters alone. For the test scenario I am getting a result of 7. The main purpose of this was to determine whether MeasureText is accounting for padding on a character-by-character basis or padding at the beginning/ending of the string. It seems that it is the latter. Perhaps this, combined with some of the other input you've received, will lead you to a more elegant solution.

var font = new Font("Calibri", 11.0f, FontStyle.Regular);
int underscoreWidth = TextRenderer.MeasureText("__", font).Width; 
for (var i = 0; i < 10; i++)
   {
      int charWidth = TextRenderer.MeasureText(String.Concat("_", i.ToString(), "_"), font).Width - underscoreWidth;
      Debug.WriteLine(charWidth);
   }
lJohnson
Thanks for that solution. As you say, it is a little unorthodox but it actually produces the correct result, which is more than I have achieved in the last four hours :). A secondary bonus for my application is that it does so without the need for a Graphics object. Thanks again.
Tim Coulter
+2  A: 

MeasureText adds padding, the amount is fairly unpredictable although it roughly scales linearly with the font size. The trick is to subtract two measurements to get rid of the padding. This code generated very happy numbers:

        float size = 5;
        for (int ix = 0; ix < 10; ++ix) {
            var font = new Font("Calibri", size, FontStyle.Regular);
            string txt = new string('0', 100);
            SizeF sz1 = TextRenderer.MeasureText("00", font) - TextRenderer.MeasureText("0", font);
            Console.WriteLine("{0} {1:N3}", size, sz1.Width);
            size += 2;
        }

Output:

5 4.000
7 5.000
9 6.000
11 7.000
13 9.000
15 10.000
17 12.000
19 13.000
21 14.000
23 16.000

That gets you the 7 pixels you're looking for. Beware that you'll have to accommodate TrueType hinting, it can adjust the shape of the digit so that its main stems fall on pixel boundaries. Add at least one pixel.

Hans Passant
Very nice, will be useful !
BlueTrin