• Status: Solved
  • Priority: Medium
  • Security: Public
  • Views: 691
  • Last Modified:

.NET System.Drawing.MeasureString is grotesquely flawed?

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
riceman0
Asked:
riceman0
3 Solutions
 
Richard LeeSoftware EnthusiastCommented:
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
 
Erick37Commented:
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
 
riceman0Author Commented:
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
 
Erick37Commented:
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
 
Todd GerbertIT ConsultantCommented:
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

Independent Software Vendors: We Want Your Opinion

We value your feedback.

Take our survey and automatically be enter to win anyone of the following:
Yeti Cooler, Amazon eGift Card, and Movie eGift Card!

Tackle projects and never again get stuck behind a technical roadblock.
Join Now