Solved

.NET System.Drawing.MeasureString is grotesquely flawed?

Posted on 2010-11-23
5
649 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 our DevOps Teams Maximize Uptime

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

Question has a verified solution.

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

Suggested Solutions

Title # Comments Views Activity
IDE for Python 5 72
Upgrade code from VS 2010 to VS 2015 7 33
DataMember property 'tablename' cannot be found on the DataSource. 4 52
vb.net background worker crossthread 11 30
Many of us here at EE write code. Many of us write exceptional code; just as many of us write exception-prone code. As we all should know, exceptions are a mechanism for handling errors which are typically out of our control. From database errors, t…
Exception Handling is in the core of any application that is able to dignify its name. In this article, I'll guide you through the process of writing a DRY (Don't Repeat Yourself) Exception Handling mechanism, using Aspect Oriented Programming.
Nobody understands Phishing better than an anti-spam company. That’s why we are providing Phishing Awareness Training to our customers. According to a report by Verizon, only 3% of targeted users report malicious emails to management. With compan…

733 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