Welcome to Experts Exchange

Add your voice to the tech community where 5M+ people, just like you, are talking about what matters.

  • Help others & share knowledge
  • Earn cash & points
  • Learn & ask questions
Solved

.NET System.Drawing.MeasureString is grotesquely flawed?

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

Master Your Team's Linux and Cloud Stack

Come see why top tech companies like Mailchimp and Media Temple use Linux Academy to build their employee training programs.

Question has a verified solution.

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

Suggested Solutions

For those of you who don't follow the news, or just happen to live under rocks, Microsoft Research released a beta SDK (http://www.microsoft.com/en-us/download/details.aspx?id=27876) for the Xbox 360 Kinect. If you don't know what a Kinect is (http:…
Today I had a very interesting conundrum that had to get solved quickly. Needless to say, it wasn't resolved quickly because when we needed it we were very rushed, but as soon as the conference call was over and I took a step back I saw the correct …
In a recent question (https://www.experts-exchange.com/questions/29004105/Run-AutoHotkey-script-directly-from-Notepad.html) here at Experts Exchange, a member asked how to run an AutoHotkey script (.AHK) directly from Notepad++ (aka NPP). This video…
I've attached the XLSM Excel spreadsheet I used in the video and also text files containing the macros used below. https://filedb.experts-exchange.com/incoming/2017/03_w12/1151775/Permutations.txt https://filedb.experts-exchange.com/incoming/201…

856 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