Solved

.NET System.Drawing.MeasureString is grotesquely flawed?

Posted on 2010-11-23
5
627 Views
Last Modified: 2012-05-10
Hey, I am having problems with System.Drawing.MeasureString.  I'm putting text on a form and I would like to detect which character the user clicked on, so I'm doing some measurements with MeasureString.  However, it doesn't seem to work very well.  I've confirmed this with the code below, please cut-and-paste it into a new VB.NET project and you'll see what I mean.

Basically the measurements are all off, and this remains true for various fonts and sizes.  You'll notice at the end it doesn't even seem to get the width of the 'x' right, and there is a cumulative error as the series proceeds.

As a minor side issue I can't get it to stop "trimming" the spaces off the end of my strings before measuring, but I can work around that.

Is there an alternative to MeasureString that works better?  Or is what I'm trying to do just not possible, I'm at the mercy of the text renderer?

Thanks for any thoughts or insights.
Public Class Form1



    Public Sub New()



        ' This call is required by the designer.

        InitializeComponent()



        ' Add any initialization after the InitializeComponent() call.



        Me.SetStyle(ControlStyles.ResizeRedraw, True)

        SetStyle(ControlStyles.AllPaintingInWmPaint, True)

        SetStyle(ControlStyles.OptimizedDoubleBuffer, True)



    End Sub



    Dim myfont As New Font("Tahoma", 12)

    Dim mybrush As New SolidBrush(Color.Black)

    Dim mytext As String = "four score and seven years ago our fathers brought forth on this continent a new nationxxxxxxxxxxxxxxxxxxxxxxxxxx"



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



        Dim g As Graphics = e.Graphics





        Dim start_x As Single = 20

        Dim y As Single = 20

        Dim h As Single = 20

        Dim btm As Single = y + h



        g.DrawRectangle(Pens.LightBlue, New Rectangle(start_x, y, 2000, h))

        g.DrawString(mytext, myfont, mybrush, start_x, y)



        Dim temp_sz As SizeF



        For n As Integer = 0 To mytext.Length - 1



            temp_sz = g.MeasureString(mytext.Substring(0, n + 1), myfont, 2000, System.Drawing.StringFormat.GenericDefault)



            Dim xtemp As Single = temp_sz.Width + start_x

            g.DrawLine(Pens.Gray, xtemp, y, xtemp, btm)



        Next



    End Sub



End Class

Open in new window

0
Comment
Question by:riceman0
5 Comments
 
LVL 18

Assisted Solution

by:Richard Lee
Richard Lee earned 100 total points
ID: 34198218
The problem you are encountering could be caused by kerning. When the string is displayed to the user an kerning is applied the width of each character within the string may/may not match the width of itself when measured independently of the string. To prove that concept the Rectangle you have drawn should match the length of the string.
0
 
LVL 32

Expert Comment

by:Erick37
ID: 34198332
If you set the StringFormat to .GenericTypographic it will work better, but it's probably not what you want.  Each measurement will contain some padding to the right of the last letter so you will not see a neat box around each letter.

g.DrawString(mytext, myfont, mybrush, start_x, y, StringFormat.GenericTypographic)

...

temp_sz = g.MeasureString(mytext.Substring(0, n + 1), myfont, 2000, System.Drawing.StringFormat.GenericTypographic)

0
 

Author Comment

by:riceman0
ID: 34198618
DaTribe:  not sure I follow, the rectangle I drew was just an arbitrary large width.  And isn't getting a read on things like the kerning what MeasureString is all about?  I thought that's why you provided the entire string, the font, and the graphics context.  If it's not considering kerning then MeasureString is just  looking up and totaling character widths for the given font -- that would indeed be useless and I have to think MeasureString is doing more than that.  

Erick37: looks no better with GenericTypographic unfortunately.  Still not even getting the 'x' width right.  In fact, the drawn 'x' seems *wider* than the measurestring calculation, so the padding wouldn't fully explain that.

However I should play with other options on that stringformat maybe.  

So... do I gather so far there is no better alternative to MeasureString?

To compare, maybe I'll try that old technique of writing to an invisible Autosize label control and seeing what width it assumes.
0
 
LVL 32

Accepted Solution

by:
Erick37 earned 200 total points
ID: 34199319
Using the sample from MS, I was able to get a bit closer...

http://msdn.microsoft.com/en-us/library/system.drawing.graphics.measurecharacterranges.aspx 
Private Sub MeasureCharacterRangesRegions(ByVal e As PaintEventArgs)



        ' Set up string.

        Dim measureRect1 As New RectangleF

        Dim measureString As String = "four score and seven years ago our fathers brought forth on this continent a new nation xxxxxxxxxxxxX"

        Dim stringFont As New Font("Times New Roman", 16.0F)

        Dim pen As New Pen(Color.Blue, 1)



        ' Create rectangle for layout.

        Dim x As Single = 50.0F

        Dim y As Single = 50.0F

        Dim width As Single = 2000.0F

        Dim height As Single = 50.0F

        Dim layoutRect As New RectangleF(x, y, width, height)



        Dim stringFormat As New StringFormat

        stringFormat.FormatFlags = StringFormatFlags.MeasureTrailingSpaces



        ' Draw string to screen.

        e.Graphics.DrawString(measureString, stringFont, Brushes.Black, x, y, stringFormat)



        ' Set character ranges to "First" and "Second".

        Dim characterRanges As New List(Of CharacterRange)





        For i As Integer = 0 To measureString.Length - 1

            characterRanges.Clear()

            characterRanges.Add(New CharacterRange(i, 1))

            ' Set string format.

            stringFormat.SetMeasurableCharacterRanges(characterRanges.ToArray)

            ' Measure.

            Dim stringRegions() As [Region] = e.Graphics.MeasureCharacterRanges(measureString, stringFont, layoutRect, stringFormat)

            ' Draw rectangles

            measureRect1 = stringRegions(0).GetBounds(e.Graphics)

            e.Graphics.DrawRectangle(pen, Rectangle.Round(measureRect1))



        Next



    End Sub

Open in new window

0
 
LVL 33

Assisted Solution

by:Todd Gerbert
Todd Gerbert earned 200 total points
ID: 34199408
I think the thing with kerning is that you're writing a complete string - and measuring that one string would yield the correct result.  However, what you're doing is measuing 30 (or however many characters) different strings - and since you're measuring each character out of context, i.e. by itself instead of how it appears in a string, the gap between characters doesn't come out quite right.

Now, to address your issues:

Measuring spaces - use a custom StringFormat object with the StringFormatFlags.MeasureTrailingSpaces flag set.

As for making the boxes line up - using a fixed-width font, like Courier New, should alleviate the character spacing issues:
Public Class Form1

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        Me.SetStyle(ControlStyles.ResizeRedraw, True)
        SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        SetStyle(ControlStyles.OptimizedDoubleBuffer, True)

    End Sub

    Dim myfont As New Font("Courier New", 12)
    Dim mybrush As New SolidBrush(Color.Black)
    Dim mytext As String = "four score and seven years ago our fathers brought forth on this continent a new nationxxxxxxxxxxxxxxxxxxxxxxxxxx"

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

        Dim g As Graphics = e.Graphics


        Dim start_x As Single = 20
        Dim y As Single = 20
        Dim h As Single = 20
        Dim btm As Single = y + h
        g.PageUnit = GraphicsUnit.Display
        g.DrawRectangle(Pens.LightBlue, New Rectangle(start_x, y, 2000, h))
        g.DrawString(mytext, myfont, mybrush, start_x, y)

        Dim temp_sz As SizeF

        Dim sf As New StringFormat(StringFormat.GenericDefault)
        sf.FormatFlags = sf.FormatFlags Or StringFormatFlags.MeasureTrailingSpaces



        For n As Integer = 0 To mytext.Length - 1

            temp_sz = g.MeasureString(mytext.Substring(0, n + 1), myfont, 2000, sf)


            Dim xtemp As Single = temp_sz.Width + start_x - 2
            g.DrawLine(Pens.Gray, xtemp, y, xtemp, btm)

        Next

    End Sub

End Class
[code]

Also feasible is to write the characters one at a time, since you're measuring them one at a time:
[code]
Public Class Form1

    Public Sub New()

        ' This call is required by the designer.
        InitializeComponent()

        ' Add any initialization after the InitializeComponent() call.

        Me.SetStyle(ControlStyles.ResizeRedraw, True)
        SetStyle(ControlStyles.AllPaintingInWmPaint, True)
        SetStyle(ControlStyles.OptimizedDoubleBuffer, True)

    End Sub

    Dim myfont As New Font("Tahoma", 12)
    Dim mybrush As New SolidBrush(Color.Black)
    Dim mytext As String = "four score and seven years ago our fathers brought forth on this continent a new nationxxxxxxxxxxxxxxxxxxxxxxxxxx"

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

        Dim g As Graphics = e.Graphics


        Dim start_x As Single = 20
        Dim y As Single = 20
        Dim h As Single = 20
        Dim btm As Single = y + h
        g.PageUnit = GraphicsUnit.Display
        g.DrawRectangle(Pens.LightBlue, New Rectangle(start_x, y, 2000, h))

        Dim sf As New StringFormat(StringFormat.GenericDefault)
        sf.FormatFlags = sf.FormatFlags Or StringFormatFlags.MeasureTrailingSpaces

        Dim nextCharPos As New PointF(start_x, y)
        For Each c As Char In mytext
            g.DrawString(c, myfont, mybrush, nextCharPos)
            Dim xtemp As Single = nextCharPos.X + g.MeasureString(c, myfont, 0, sf).Width
            g.DrawLine(Pens.Gray, xtemp, y, xtemp, btm)
            nextCharPos.X = xtemp + 5
        Next

    End Sub

End Class

Open in new window

0

Featured Post

3 Use Cases for Connected Systems

Our Dev teams are like yours. They’re continually cranking out code for new features/bugs fixes, testing, deploying, testing some more, responding to production monitoring events and more. It’s complex. So, we thought you’d like to see what’s working for us.

Question has a verified solution.

If you are experiencing a similar issue, please ask a related question

Creating an analog clock UserControl seems fairly straight forward.  It is, after all, essentially just a circle with several lines in it!  Two common approaches for rendering an analog clock typically involve either manually calculating points with…
It was really hard time for me to get the understanding of Delegates in C#. I went through many websites and articles but I found them very clumsy. After going through those sites, I noted down the points in a easy way so here I am sharing that unde…
This Micro Tutorial will teach you how to censor certain areas of your screen. The example in this video will show a little boy's face being blurred. This will be demonstrated using Adobe Premiere Pro CS6.
Both in life and business – not all partnerships are created equal. As the demand for cloud services increases, so do the number of self-proclaimed cloud partners. Asking the right questions up front in the partnership, will enable both parties …

867 members asked questions and received personalized solutions in the past 7 days.

Join the community of 500,000 technology professionals and ask your questions.

Join & Ask a Question

Need Help in Real-Time?

Connect with top rated Experts

19 Experts available now in Live!

Get 1:1 Help Now