10

why do i have to increase MeasureString() result width by 21% size.Width = size.Width * 1.21f; to evade Word Wrap in DrawString()?

I need a solution to get the exact result.

Same font, same stringformat, same text used in both functions.


From answer by OP:

  SizeF size = graphics.MeasureString(element.Currency, Currencyfont, new PointF(0, 0), strFormatLeft);
  size.Width = size.Width * 1.21f;
  int freespace = rect.Width - (int)size.Width;
  if (freespace < ImageSize) { if (freespace > 0) ImageSize = freespace; else ImageSize = 0; }
  int FlagY = y + (CurrencySize - ImageSize) / 2;
  int FlagX = (freespace - ImageSize) / 2;
  graphics.DrawImage(GetResourseImage(@"Flags." + element.Flag.ToUpper() + ".png"), 
         new Rectangle(FlagX, FlagY, ImageSize, ImageSize));
  graphics.DrawString(element.Currency, Currencyfont, Brushes.Black, 
       new Rectangle(FlagX + ImageSize, rect.Y, (int)(size.Width), CurrencySize), strFormatLeft);

My code.

H H
  • 263,252
  • 30
  • 330
  • 514
bikt
  • 173
  • 1
  • 2
  • 14

6 Answers6

8

MeasureString() method had some issues, especially when drawing non-ASCII characters. Please try TextRenderer.MeasureText() instead.

Paweł Dyda
  • 18,366
  • 7
  • 57
  • 79
  • 1
    @ChocapicSz: To be honest, I haven't found single reliable string bounds measuring method, regardless of programming environment... The funny thing is, the same text, the same font face but different font size may yield completely different results... I've once compared English to translated string ratio with funny results: for Arial 10 the translated text was wider, but for Arial 18 it was narrower than English text... C'est la vie. – Paweł Dyda Aug 13 '15 at 18:18
3

Graphics.MeasureString, TextRenderer.MeasureText and Graphics.MeasureCharacterRanges all return a size that includes blank pixels around the glyph to accomodate ascenders and descenders.

In other words, they return the height of "a" as the same as the height of "d" (ascender) or "y" (descender). If you need the true size of the glyph, the only way is to draw the string and count the pixels:

Public Shared Function MeasureStringSize(ByVal graphics As Graphics, ByVal text As String, ByVal font As Font) As SizeF

    ' Get initial estimate with MeasureText
    Dim flags As TextFormatFlags = TextFormatFlags.Left + TextFormatFlags.NoClipping
    Dim proposedSize As Size = New Size(Integer.MaxValue, Integer.MaxValue)
    Dim size As Size = TextRenderer.MeasureText(graphics, text, font, proposedSize, flags)

    ' Create a bitmap
    Dim image As New Bitmap(size.Width, size.Height)
    image.SetResolution(graphics.DpiX, graphics.DpiY)

    Dim strFormat As New StringFormat
    strFormat.Alignment = StringAlignment.Near
    strFormat.LineAlignment = StringAlignment.Near

    ' Draw the actual text
    Dim g As Graphics = graphics.FromImage(image)
    g.SmoothingMode = SmoothingMode.HighQuality
    g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAliasGridFit
    g.Clear(Color.White)
    g.DrawString(text, font, Brushes.Black, New PointF(0, 0), strFormat)

    ' Find the true boundaries of the glyph
    Dim xs As Integer = 0
    Dim xf As Integer = size.Width - 1
    Dim ys As Integer = 0
    Dim yf As Integer = size.Height - 1

    ' Find left margin
    Do While xs < xf
        For y As Integer = ys To yf
            If image.GetPixel(xs, y).ToArgb <> Color.White.ToArgb Then
                Exit Do
            End If
        Next
        xs += 1
    Loop
    ' Find right margin
    Do While xf > xs
        For y As Integer = ys To yf
            If image.GetPixel(xf, y).ToArgb <> Color.White.ToArgb Then
                Exit Do
            End If
        Next
        xf -= 1
    Loop
    ' Find top margin
    Do While ys < yf
        For x As Integer = xs To xf
            If image.GetPixel(x, ys).ToArgb <> Color.White.ToArgb Then
                Exit Do
            End If
        Next
        ys += 1
    Loop
    ' Find bottom margin
    Do While yf > ys
        For x As Integer = xs To xf
            If image.GetPixel(x, yf).ToArgb <> Color.White.ToArgb Then
                Exit Do
            End If
        Next
        yf -= 1
    Loop

    Return New SizeF(xf - xs + 1, yf - ys + 1)

End Function
smirkingman
  • 6,167
  • 4
  • 34
  • 47
  • Sounds inefficient, but it's true: couldn't get it working with neither of functions, they all included padding. Will use bitmaps, too. Thanks! – peenut Oct 08 '12 at 09:36
2

If it helps anyone, I transformed answer from smirkingman to C#, fixing memory bugs (using - Dispose) and outer loop breaks (no TODOs). I also used scaling on graphics (and fonts), so I added that, too (didn't work otherwise). And it returns RectangleF, because I wanted to position the text precisely (with Graphics.DrawText).

The not-perfect but good enough for my purpose source code:

static class StringMeasurer
{
    private static SizeF GetScaleTransform(Matrix m)
    {
        /*
         3x3 matrix, affine transformation (skew - used by rotation)
         [ X scale,     Y skew,      0 ]
         [ X skew,      Y scale,     0 ]
         [ X translate, Y translate, 1 ]

         indices (0, ...): X scale, Y skew, Y skew, X scale, X translate, Y translate
         */
        return new SizeF(m.Elements[0], m.Elements[3]);
    }

    public static RectangleF MeasureString(Graphics graphics, Font f, string s)
    {
        //copy only scale, not rotate or transform
        var scale = GetScaleTransform(graphics.Transform);

        // Get initial estimate with MeasureText
        //TextFormatFlags flags = TextFormatFlags.Left | TextFormatFlags.NoClipping;
        //Size proposedSize = new Size(int.MaxValue, int.MaxValue);
        //Size size = TextRenderer.MeasureText(graphics, s, f, proposedSize, flags);
        SizeF sizef = graphics.MeasureString(s, f);
        sizef.Width *= scale.Width;
        sizef.Height *= scale.Height;
        Size size = sizef.ToSize();

        int xLeft = 0;
        int xRight = size.Width - 1;
        int yTop = 0;
        int yBottom = size.Height - 1;

        // Create a bitmap
        using (Bitmap image = new Bitmap(size.Width, size.Height))
        {
            image.SetResolution(graphics.DpiX, graphics.DpiY);

            StringFormat strFormat = new StringFormat();
            strFormat.Alignment = StringAlignment.Near;
            strFormat.LineAlignment = StringAlignment.Near;

            // Draw the actual text
            using (Graphics g = Graphics.FromImage(image))
            {
                g.SmoothingMode = graphics.SmoothingMode;
                g.TextRenderingHint = graphics.TextRenderingHint;
                g.Clear(Color.White);
                g.ScaleTransform(scale.Width, scale.Height);
                g.DrawString(s, f, Brushes.Black, new PointF(0, 0), strFormat);
            }
            // Find the true boundaries of the glyph

            // Find left margin
            for (;  xLeft < xRight; xLeft++)
                for (int y = yTop; y <= yBottom; y++)
                    if (image.GetPixel(xLeft, y).ToArgb() != Color.White.ToArgb())
                        goto OUTER_BREAK_LEFT;
        OUTER_BREAK_LEFT: ;

            // Find right margin
            for (; xRight > xLeft; xRight--)
                for (int y = yTop; y <= yBottom; y++)
                    if (image.GetPixel(xRight, y).ToArgb() != Color.White.ToArgb())
                        goto OUTER_BREAK_RIGHT;
        OUTER_BREAK_RIGHT: ;

            // Find top margin
            for (; yTop < yBottom; yTop++)
                for (int x = xLeft; x <= xRight; x++)
                    if (image.GetPixel(x, yTop).ToArgb() != Color.White.ToArgb())
                        goto OUTER_BREAK_TOP;
        OUTER_BREAK_TOP: ;

            // Find bottom margin
            for (; yBottom > yTop; yBottom-- )
                for (int x = xLeft; x <= xRight; x++)
                    if (image.GetPixel(x, yBottom).ToArgb() != Color.White.ToArgb())
                        goto OUTER_BREAK_BOTTOM;
        OUTER_BREAK_BOTTOM: ;
        }

        var pt = new PointF(xLeft, yTop);
        var sz = new SizeF(xRight - xLeft + 1, yBottom - yTop + 1);
        return new RectangleF(pt.X / scale.Width, pt.Y / scale.Height,
            sz.Width / scale.Width, sz.Height / scale.Height);
    }
}
peenut
  • 3,366
  • 23
  • 24
  • Disposing the bitmap is a good suggestion, but all those GOTOs reek 'beginner'. You'd have been better off just pasting the VB code into http://converter.telerik.com/ and adding a 'using' for the bitmap. – smirkingman Mar 01 '17 at 11:56
  • @smirkingman GOTOs reek 'begginer', if they are used for unconditional jumps, but they are ok if they are used for multi-level loop breaks (same as return statement). For example, if I rewrite this code into Java, it would not be "goto" but "break" keyword. Read more at https://en.wikipedia.org/wiki/Goto#Common_usage_patterns_of_Goto – peenut May 19 '17 at 11:37
  • @peenut I'll never forget reading Dijkstra's famous paper http://homepages.cwi.nl/~storm/teaching/reader/Dijkstra68.pdf when I was a lad, it left a lasting impression on me >;-), but SO is not a place for debate – smirkingman Jun 20 '17 at 21:28
  • for those who prefer C#, a much simpler solution is just to paste the VB into http://converter.telerik.com/ - it adds all the semicolons, braces and whatnot automatically, without any GOTOs – smirkingman Jun 23 '17 at 09:42
1

Personally, the most efficient way and what I recommend, has always been:

 const TextFormatFlags _textFormatFlags = TextFormatFlags.NoPadding | TextFormatFlags.NoPrefix | TextFormatFlags.PreserveGraphicsClipping;
    
 // Retrieve width
 int width = TextRenderer.MeasureText(element.Currency, Currencyfont, new Size(short.MaxValue, short.MaxValue), _textFormatFlags).Width + 1;

 // Retrieve height
 int _tempHeight1 = TextRenderer.MeasureText("_", Currencyfont).Height;
 int _tempHeight2 = (int)Math.Ceiling(Currencyfont.GetHeight());
 int height = Math.Max(_tempHeight1, _tempHeight2) + 1;
Dezv
  • 156
  • 1
  • 8
-1

try this solution: http://www.codeproject.com/Articles/2118/Bypass-Graphics-MeasureString-limitation (found it at https://stackoverflow.com/a/11708952/908936)

code:

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);
    var ranges  = new System.Drawing.CharacterRange(0, text.Length);
    System.Drawing.Region[] regions = new System.Drawing.Region[1];

    format.SetMeasurableCharacterRanges (new[] {ranges});

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

    return (int)(rect.Right + 1.0f);
}
Community
  • 1
  • 1
razon
  • 3,882
  • 2
  • 33
  • 46
-1

You likely need to add the following the the StringFormat flags:

StringFormatFlags.FitBlackBox
leppie
  • 115,091
  • 17
  • 196
  • 297