[RESOLVED] I don't understand why this function doesn't work properly.
Hi,
I can't understand why this function doesn't give the correct result.
It's supposed to generate a label of a specific size. (it does)
It's supposed to set a specific font, family and size. (it does)
It's supposed to add text. (it does)
It's supposed to add the label to Form1 and show it. (it does)
It's supposed to measure the length of the text. (it seems to)
It's supposed to reduce the font size until the text fits inside the Label. (it seems to)
It's supposed to return the font size. (it seems to)
It's supposed to show that the same text inside the Label with the same font and font size, fits inside the Label. (it fails miserably)
Code:
Private Function Fitter(ByVal Siz As Int32) As Int32
Dim Flag As Boolean = True
Dim Measured, TxtSize As Int32, Box As New Label
Box.Width = Siz
Box.Height = Siz
Box.Text = "90"
Box.BorderStyle = BorderStyle.FixedSingle
Box.TextAlign = ContentAlignment.MiddleCenter
Box.AutoSize = False
Box.Location = New Point(5, 5)
Me.Controls.Add(Box)
Box.Show()
TxtSize = 60
While Flag
Box.Font = New Font("Arial Narrow", TxtSize, FontStyle.Bold, GraphicsUnit.Pixel)
Using gr As Graphics = Box.CreateGraphics
Measured = CInt(gr.MeasureString(Box.Text, Box.Font).Width)
End Using
If Measured < Siz Or TxtSize < 21 Then Flag = False
If TxtSize > 20 Then TxtSize -= 1
End While
'Me.Controls.Remove(Box) 'Temporarily excluded for the test.
Return TxtSize '( 1 px less than measured.)
End Function
'Quick Sub to test the function.
Private Sub Ninety()
Dim a, b, c, sz, x, y As Integer
a = (Me.Width) / 10
b = a - 5
c = 0
x = 20
y = 10
sz = Fitter(b)
TextBox1.Text = sz.ToString
End Sub
Poppa.
PS. Further testing has shown that changing the set text from '90' to '90.' gives the result that I'm after, but I still don't understand why it doesn't work properly.
Pop.
Re: I don't understand why this function doesn't work properly.
Well, that's a very inefficient way to go about doing this, not to mention messy.
My best guess is Fitter(), as written, ignores that MeasureString() returns a SizeF() and casts the floating-point values to Integer. That will introduce some inaccuracies that might crop up when displaying the text. For example, I modified it to print out its trials and saw this as it tried to return:
Code:
Fitter: 29.27409 vs. 29
This suggests it will take 29.27 pixels to display the string. But you can't really use partial pixels, you have to use full pixels. So that's really still a measurement of 30. 30 happened to be the "test width" in my trial. So the string's at least a pixel wider than the label, and it ends up getting wrapped. When I switched the algorithm to use Math.Ceiling, I got more reliable results.
In general, my experience is you should still add a few pixels to your width, because in various circumstances the metrics you get back from MeasureString() aren't the same. Blame MS for arbitrarily using GDI or GDI+ when rendering.
But really, your algorithm creates and destroys a control and graphics context for no real reason. I think this is a more elegant implementation of the same thing. TextRenderer.MeasureText() lets you measure things without having to create a Graphics context of your own. Interestingly enough, it returns a Size instead of SizeF, so there's no need to worry about the rounding.
Code:
Function GetFontFitSize(ByVal text As String, ByVal width As Integer, ByVal basisFont As Font) As Integer
Dim smallestFontSize As Integer = 20
Dim startingFontSize As Integer = 100
Dim currentFontSize As Integer = startingFontSize
While True
Dim currentMeasuredWidth As Integer = 0
Using testFont As New Font(basisFont.FontFamily, currentFontSize, basisFont.Style, GraphicsUnit.Pixel)
Dim desiredSize = TextRenderer.MeasureText(text, testFont)
Console.WriteLine("Desired size @ {0}: {1}", currentFontSize, desiredSize)
currentMeasuredWidth = desiredSize.Width
End Using
If currentMeasuredWidth > width AndAlso currentFontSize >= smallestFontSize Then
currentFontSize -= 1
Continue While
End If
Exit While
End While
Console.WriteLine("Returning {0}.", currentFontSize)
Return currentFontSize
End Function
Private Sub GOOD_NAME_C2D33505_8C9E_4BD9_964D_FA20FE97FB05()
Dim testWidth As Integer = Me.Width \ 10
Label1.AutoSize = False
Label1.Size = New Size(testWidth, testWidth)
Label1.BackColor = Color.Green
Label1.Text = "90"
Dim correctFontSize = GetFontFitSize(Label1.Text, testWidth, Label1.Font)
Label1.Font = New Font(Label1.Font.FontFamily, correctFontSize, Label1.Font.Style, GraphicsUnit.Pixel)
End Sub
I, too, lament the absence of an auto-fitting label in Windows (or just about any platform.)
Re: I don't understand why this function doesn't work properly.
Quote:
Originally Posted by
Sitten Spynne
Well, that's a very inefficient way to go about doing this, not to mention messy.
My best guess is Fitter(), as written, ignores that MeasureString() returns a SizeF() and casts the floating-point values to Integer. That will introduce some inaccuracies that might crop up when displaying the text. For example, I modified it to print out its trials and saw this as it tried to return:
Quote:
Fitter: 29.27409 vs. 29
This suggests it will take 29.27 pixels to display the string. But you can't really use partial pixels, you have to use full pixels. So that's really still a measurement of 30. 30 happened to be the "test width" in my trial. So the string's at least a pixel wider than the label, and it ends up getting wrapped. When I switched the algorithm to use Math.Ceiling, I got more reliable results.
In general, my experience is you should still add a few pixels to your width, because in various circumstances the metrics you get back from MeasureString() aren't the same. Blame MS for arbitrarily using GDI or GDI+ when rendering.
But really, your algorithm creates and destroys a control and graphics context for no real reason. I think this is a more elegant implementation of the same thing. TextRenderer.MeasureText() lets you measure things without having to create a Graphics context of your own. Interestingly enough, it returns a Size instead of SizeF, so there's no need to worry about the rounding.
I, too, lament the absence of an auto-fitting label in Windows (or just about any platform.)
Thanks Sitten Spynne,
I'm good at inefficient, and at messy too. :)
Quote:
But really, your algorithm creates and destroys a control and graphics context for no real reason.
I can see that it looks that way... But: -
1. I don't know what size the size the label will be because the form will be resized at run time.
2. There will be ninety labels and I want the labels to be populated with their text already showing at it's largest when the form is shown.
So I make just one label, the same size as the ninety will be, enter the widest text and measure it. Then I remove the 'test' label.
Having slept on the problem, it did occur to me that maybe I should use a Double for the measurement, (it turns out that it should be a Single anyway) and then maybe add one, but I figured that since I returned a font height one pixel less than I'd measured, that ought to take care of that. Looks like that's wrong, it's even wrong until I return the pixel size as 5 less than measured.
Quote:
But you can't really use partial pixels, you have to use full pixels.
I changed from Integer to Single, and tried adding 1 to the measured size, but again, I had to add 5 (and still subtract 1 from the text height) before the text fitted.
Incidentally, it's not just the widest texts which don't fit, the single digits do but not even '11' fits the label until I doctor the result, then everything fits.
I'm going to try your code now, I'll let you know how it performs.
Poppa.
PS, I forgot to mention that newly added, the Padding is set to All = 0.
Pop.
Re: I don't understand why this function doesn't work properly.
Quote:
Originally Posted by
Poppa Mintin
I can see that it looks that way... But: -
1. I don't know what size the size the label will be because the form will be resized at run time.
2. There will be ninety labels and I want the labels to be populated with their text already showing at it's largest when the form is shown.
None of this matters, for a few reasons. The easiest are:
- You could be using Me.CreateGraphics(), because the form is very likely to exist if you're writing code in the context of the form. This way you don't have to create a control, add it, and remove it. You didn't dispose it, either. That can turn out to be a problem. Same with the Font, which I didn't dispose either because it's a little awkward.
- You could be overriding OnPaint() in a custom control and measuring/drawing the text yourself. Then you already have a Graphics context and don't need CreateGraphics. My gut tells me this is a little more reliable than TextRenderer.MeasureText(), and I don't want to go into why because it borders on superstition.
- TextRenderer.MeasureText() doesn't ask you to provide a Graphics context, though I'm suspicious it creates one of its own.
Re: I don't understand why this function doesn't work properly.
Since you are using the Label control, there is no need for you to be measuring the text yourself. The Label's internal layout computations can be used to retrieve what width is needed to accommodate the set Text/Font properties.
All controls expose a GetPreferredSize method. In the case of the Label control, this method is used internally to allow it to auto-size itself, but you can call it even if AutoSize is set to False. The Label control also exposes two additional properties PreferredWidth and PreferredHeight that get call GetPreferredSize and return the requested dimension.
Here is my version of this:
VB.Net Code:
Private Function GetSquareLabel(displayText As String,
sideLength As Int32,
baseFont As Font,
Optional locationX As Int32 = 0,
Optional locationY As Int32 = 0,
Optional minFontHeightPx As Int32 = 20) As Label
Dim lbl As New Label
With lbl
.Text = displayText
.AutoSize = False
.Size = New Size(sideLength, sideLength)
.Location = New Point(locationX, locationY)
.TextAlign = ContentAlignment.MiddleCenter
.BorderStyle = BorderStyle.FixedSingle
End With
' use a bisection method to find the max font size constrained by minFontHeightPx
Dim lo As Int32 = minFontHeightPx
Dim hi As Int32 = Math.Max(sideLength, minFontHeightPx)
Dim zero As New Size ' used to get preferred size
Dim current As Int32
Dim last As Int32 = -1
While current <> last
last = current
current = (lo + hi) \ 2
Using tmpFont As Font = lbl.Font
lbl.Font = New Font(lbl.Font.FontFamily.Name, current, lbl.Font.Style, GraphicsUnit.Pixel)
End Using
Select Case lbl.PreferredWidth
Case Is > sideLength
hi = current
If hi < minFontHeightPx Then hi = minFontHeightPx
Case Is < sideLength
lo = current
Case Else ' = sideLength
Exit While
End Select
End While
Return lbl
End Function
Re: I don't understand why this function doesn't work properly.
Thanks TnTinMN,
Just going to try your code, it looks as though it's written for a square label, just wondering if a label longer then wide, or even vice versa would prohibit it's use ?
Poppa.
Re: I don't understand why this function doesn't work properly.
Quote:
Originally Posted by
Poppa Mintin
Just going to try your code, it looks as though it's written for a square label, just wondering if a label longer then wide, or even vice versa would prohibit it's use ?
It was written for a square label because that is what your code implied was your goal.
Quote:
Private Function Fitter(ByVal Siz As Int32) As Int32
Dim Flag As Boolean = True
Dim Measured, TxtSize As Int32, Box As New Label
Box.Width = Siz
Box.Height = Siz
You could modify it to impose a font height height constraint. If the height constraint code is applied after the width constraint code, I doubt that would ever have much to do as most text is wider than tall unless you are going for multiline text.
Re: I don't understand why this function doesn't work properly.
Quote:
Originally Posted by
TnTinMN
It was written for a square label because that is what your code implied was your goal.
You could modify it to impose a font height height constraint. If the height constraint code is applied after the width constraint code, I doubt that would ever have much to do as most text is wider than tall unless you are going for multiline text.
Yes, I guessed that's what you'd thought. It didn't matter at what size the label starts because when the form is resized according to the resolution of the screen that the application's form may be running on, the aspect ratio of the label can change. My labels are 'AutoSize = False' which probably makes 'em multiline but the texts don't get longer then 2 characters. ("1" to "90"), so although ideally the labels are square erring on the wider than higher side.
Poppa.
Re: I don't understand why this function doesn't work properly.
Hi, Just to close this thread.
I've not been able to get any of these schemes to work so I've devised something else: -
I figured that since the font size is regulated by it's height, I'd find the largest size that would fit the widest text by trial and error at design time. For this application, in a TLP cell the label is originally 64px wide by 61px high, and the largest I could set the font height for Arial Narrow turned out to be 41px, So: -
The original ratio between label height and maximum text height is 41 / 61 (0.67213114754098360655737704918033) Say... 0.672.
My application now measures the height of that label after it's been re-sized and multiplies it by O.672 to give the new font height. Seems to work. :)
Poppa.