Solved

.NET System.Drawing.MeasureString is grotesquely flawed?

Posted on 2010-11-23
5
620 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

How your wiki can always stay up-to-date

Quip doubles as a “living” wiki and a project management tool that evolves with your organization. As you finish projects in Quip, the work remains, easily accessible to all team members, new and old.
- Increase transparency
- Onboard new hires faster
- Access from mobile/offline

Join & Write a Comment

It’s quite interesting for me as I worked with Excel using vb.net for some time. Here are some topics which I know want to share with others whom this might help. First of all if you are working with Excel then you need to Download the Following …
This article is for Object-Oriented Programming (OOP) beginners. An Interface contains declarations of events, indexers, methods and/or properties. Any class which implements the Interface should provide the concrete implementation for each Inter…
Polish reports in Access so they look terrific. Take yourself to another level. Equations, Back Color, Alternate Back Color. Write easy VBA Code. Tighten space to use less pages. Launch report from a menu, considering criteria only when it is filled…
You have products, that come in variants and want to set different prices for them? Watch this micro tutorial that describes how to configure prices for Magento super attributes. Assigning simple products to configurable: We assigned simple products…

758 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

21 Experts available now in Live!

Get 1:1 Help Now