Auto Height TextBox

Background 

I while back I created a control that inherited from a textbox. The requirements were that I needed to keep a new line below the currently typed text and that the control should not only grow in height as the rows wrapped, but that it should also push down any controls that were below the textbox on the form, as well as to grow the form as the user typed. What I came up with was the following. It has been a while since I used this code, but I quickly tested it and it seems to work, just put the textbox in multiline mode and drop it on a form and type away!!

Just wanted to share. Enjoy!

-Jeff
blog: www.savij.com

”’ <summary>
”’ This textbox will auto adjust it’s height with multi-line and wordwrap on.
”’ </summary>
”’ <remarks>After the parent form or control is Initialized, it MUST call Init()
”’ on this control. If not, the incorrect Parent.Height could get set and that
”’ will cause unexpected results.
”’ </remarks>
Public Class AutoHeightTextBox
Inherits TextBox

Private m_strHeight As Single = 0
Private m_txtHeightStart As Single = 0
Private m_ControlHeightStart As Single = 0
Private m_HeightofOneChar As Single = 0
Private m_LastHeightofTextbox As Single = 0
Private m_CollisionBuffer As Integer = 6

”’ <summary>
”’ Constructor
”’ </summary>
”’ <remarks></remarks>
Public Sub New()
MyBase.New()

m_txtHeightStart = Me.Height
m_LastHeightofTextbox = Me.Height
m_strHeight = Me.Height
End Sub

”’ <summary>
”’ The amount of space to pad when checking for a collision.
”’ </summary>
”’ <value>vale As Integer (pixels)</value>
”’ <returns></returns>
”’ <remarks>When the textbox increases in size, it will check to see if it is in
”’ danger of a collision with control drawn below it. It will add this buffer amount to
”’ it’s calculations. Setting this property to 0 will cause the textbox to touch the
”’ control below it before that control will get moved.
”’ </remarks>
Public Property CollisionBuffer() As Integer
Get
Return m_CollisionBuffer
End Get
Set(ByVal value As Integer)
m_CollisionBuffer = value
End Set
End Property

”’ <summary>
”’ Call this method after the parent has called it’s Initialize() method. This will read the
”’ actual parent.Height and set the value in this control.
”’ </summary>
”’ <remarks></remarks>
Public Sub Init()
m_ControlHeightStart = Parent.Height
End Sub

Private Sub AutoHeightTextBox_ParentChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.ParentChanged
If Parent Is Nothing Then Exit Sub

m_ControlHeightStart = Parent.Height
End Sub

Private Sub AutoHeightTextBox_TextChanged(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.TextChanged
Try
Me.SuspendLayout()
Dim iStringHeight As Single = 0
‘ Get a graphics object from the textbox
Using g As Graphics = Graphics.FromHwnd(Me.Handle)

Dim sf As New StringFormat(StringFormat.GenericTypographic)
‘ Create a rectangle equal to the size of the autowrap textbox
Dim oRect As Rectangle = Me.Bounds
g.TextRenderingHint = Drawing.Text.TextRenderingHint.AntiAlias
sf.FormatFlags = StringFormatFlags.NoClip
‘ Estimate the height of one char
m_HeightofOneChar = g.MeasureString(“W”, Me.Font).Height
‘ Get the estimate of the height of the current textbox.
iStringHeight = g.MeasureString(Me.Text, Me.Font, oRect.Width, sf).Height

End Using
‘ The height is wrong sometimes. It doesnt update upon wrap as it should, so we add a
‘ buffer to the bottom of the textbox.
iStringHeight += (Me.Font.Height * 2)

‘ Check if the measured string height is greater than the previous string height.
If iStringHeight > m_strHeight Then

‘ Increase the height of the textbox to that of the measurement (plus our buffer)
Me.Height = iStringHeight

‘ Since the height of the textbox has increased, check if we are in danger of a
‘ collision with another control below it on the parent.
If CheckCollision() Then
‘ We have a collision, so increase the parent height to accomodate the larger
‘ textbox size.
Parent.Height += (Me.Height – m_LastHeightofTextbox)
‘ Move any controls below me down.
MoveParentControlsDown(Me.Height – m_LastHeightofTextbox)
End If

‘Update tracking variables.
m_LastHeightofTextbox = Me.Height
m_strHeight = iStringHeight

Exit Sub
End If

‘ Check if the measured string height is less than the previous string height.
If iStringHeight < m_strHeight Then

‘ Decrease the height of the textbox to that of the measurement (plus our buffer)
Me.Height = iStringHeight

‘ Check if the Textbox height is less than the previous Textbox height. This
‘ would mean that the Textbox has decreased it’s height.
If m_LastHeightofTextbox > Height Then
‘ Move other controls up that are below me
MoveParentControlsUp(m_LastHeightofTextbox – Me.Height)
‘ Decrease the parent height to take up the space left my the Textbox resize.
Parent.Height -= (m_LastHeightofTextbox – Me.Height)
End If

‘Update tracking variables.
m_LastHeightofTextbox = Me.Height
m_strHeight = iStringHeight
Exit Sub
End If

Catch ex As Exception

Finally
Me.ResumeLayout()
End Try
End Sub

Private Function CheckCollision() As Boolean
Try
For Each oControl As Control In Parent.Controls
If (Location.Y + Height + 6) >= oControl.Location.Y AndAlso (oControl.Location.Y + oControl.Height) > (Location.Y + Height + 6) Then
Return True
End If
Next

‘Check for control border collision
If Location.Y + Height >= Parent.Height Then
Return True
End If

Return False
Catch ex As Exception

End Try
End Function

Private Sub MoveParentControlsDown(ByVal AmountToMove As Single)
Try

For Each oControl As Control In Parent.Controls
If Not oControl.Equals(Me) Then
If oControl.Location.Y > Location.Y Then
‘ Move the control down
oControl.Location = New Point(oControl.Location.X, oControl.Location.Y + AmountToMove)
End If
End If
Next

Catch ex As Exception

End Try
End Sub

Private Sub MoveParentControlsUp(ByVal AmountToMove As Single)
Try

For Each oControl As Control In Parent.Controls
If Not oControl.Equals(Me) Then
If oControl.Location.Y > Location.Y Then

oControl.Location = New Point(oControl.Location.X, oControl.Location.Y – AmountToMove)

End If
End If
Next

Catch ex As Exception

End Try
End Sub

End Class

4 Responses to “Auto Height TextBox”

  1. Cheetos72 Says:

    NICE — do you have it in C#?

  2. savij Says:

    No I don’t but it should only take 5 or 10 minutes to convert it. I don’t see any vb specific code in there, so it’s really just reformatting the syntax to c#.

  3. Stuart McConnachie Says:

    Nice, but this code has a problem. I believe MeasureString spaces each line exactly using a float value, whereas the control uses the same precise integer number of pixels between each line. This introduces rounding errors as the number of lines in the control grows. The result is that the returned hight is no longer sufficient to contain all the text as drawn by the control and you lose the top or bottom one or more lines:

    Here’s a workaround (in C# sorry, but should be easy to convert ). This produces EXACTLY the right height of control. It also accounts for line wrap and empty text boxes:

    private const int EM_GETLINECOUNT = 0xBA;

    [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = false)]
    static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);

    private void ReSizeTextBox()
    {
    int iRequiredRows = SendMessage(this.textBox.Handle, EM_GETLINECOUNT, IntPtr.Zero, IntPtr.Zero).ToInt32();
    int iRequiredHeight = iRequiredRows * this.textBox.Font.Height;
    int iDiffHeight = iRequiredHeight – this.textBox.ClientSize.Height;
    if (iDiffHeight != 0)
    {
    this.textBox.Height += iDiffHeight;
    }
    }

    Now just call the above OnTextChanged and OnClientSizeChanged for the TextBox and you are done.

  4. savij Says:

    Thats a great solution! You are right, the size did get off on longer resizes. I am not going to convert this to VB (as I do both). But for those needing it, just convert the DllImport line. The rest is just removing the semicolons and change != to .

    I also stole this code from a friend, I have not tested it, but he says it works correctly.

    private void InfoQuestionUserControl_Paint(object sender, PaintEventArgs e)

    {

    SuspendLayout();

    double numberOfRows = 1;

    if (_painted == false)

    {

    SizeF size = e.Graphics.MeasureString(Question, _lblInfoQuestion.Font);

    if (size.Width > _lblInfoQuestion.Width)

    {

    numberOfRows = Math.Floor(size.Width/_lblInfoQuestion.Width);

    if ((_lblInfoQuestion.Width % size.Width) > 0)

    {

    numberOfRows++;

    }

    }

    _lblInfoQuestion.Height = (int)(numberOfRows * _lblInfoQuestion.Height);

    Height = _lblInfoQuestion.Height + 2;

    _painted = true;

    }

    ResumeLayout();

    }

    Hope this helps!!!

    Thanks for the comment Stuart!

    -Jeff

Leave a Reply