0

I have records in my .NET WinForms app that I lay out with enhanced TextBox controls on panels when the records are editable, but I set the TextBoxes to ReadOnly when the records are not editable. Clicking the save button on an editable record saves the text to the database, and then it is displayed as an un-editable record (until the edit button is clicked). Please see the following screen grab:

Non-editable and editable record panels

As you can hopefully see, the first record is not editable, but the second one is. The problem I have is that I would like the TextBox to grow in Height if the text is too much to fit. It seems that the TextBox is doing the WordWrap, but it either only shows one line of the text or only the first two. Something is always cut off at the bottom.

I have looked at several other posts on this site, including, especially, Expandable WinForms TextBox.

Here is some sample code for the panel:

AutoSize = true;
AutoSizeMode = AutoSizeMode.GrowAndShrink;
        ...
Field1 = new ExpandoField { Multiline = true, WordWrap = true };
Field1.Location = new System.Drawing.Point(42, 3);
if (CanEdit)
{
    Field1.BackColor = System.Drawing.Color.White;
    Field1.TabIndex = 20;
}
else
{
    ((ExpandoField) Field1).ReadOnly = true;
    Field1.ForeColor = System.Drawing.Color.FromArgb(0, 50, 0);
    Field1.BackColor = System.Drawing.Color.Snow;
    Field1.TabIndex = 0;
    Field1.TabStop = false;
}
Field1.Text = Text1;
Field1.Dock = DockStyle.None;
Field1.Size = new System.Drawing.Size(538 - 25, 34);
Field1.MinimumSize = Field1.Size;
Field1.AutoSize = true;
Controls.Add(Field1);

As you can see, I have AutoSize set to true for the panel. The code for Field2 is similar to Field1.

ExpandoField is based on sample code I saw from a response by dstran in Expandable WinForms TextBox. It seemed to be the most complete implementation of the suggestion marked as the answer to that post. Here's the code:

class ExpandoField : TextBox
{
    private double m_growIndex = 0.0;
    private Timer m_timer;

    public ExpandoField()
    {
        AutoSize = false;
        this.Height = 20;

        // Without the timer, I got a lot of AccessViolationException in the System.Windows.Forms.dll.
        m_timer = new Timer();
        m_timer.Interval = 1;
        m_timer.Enabled = false;
        m_timer.Tick += new EventHandler(m_timer_Tick);

        this.KeyDown += new KeyEventHandler(ExpandoField_KeyDown);
    }

    void ExpandoField_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Modifiers == Keys.Control && e.KeyCode == Keys.A)
            this.SelectAll();
    }

    void m_timer_Tick(object sender, EventArgs e)
    {
        var sz = new System.Drawing.Size(Width, Int32.MaxValue);
        sz = TextRenderer.MeasureText(Text, Font, sz, TextFormatFlags.TextBoxControl);

        m_growIndex = (double)(sz.Width / (double)Width);

        if (m_growIndex > 0)
            Multiline = true;
        else
            Multiline = false;

        int tempHeight = (int) (20 * m_growIndex);

        if (tempHeight <= 20)
            Height = 20;
        else
            Height = tempHeight;

        m_timer.Enabled = false;
    }

    public override sealed bool AutoSize
    {
        get { return base.AutoSize; }
        set { base.AutoSize = value; }
    }

    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);
        m_timer.Enabled = true;
    }

    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);
        m_timer.Enabled = true;
    }

    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        m_timer.Enabled = true;
    }
}

This is obviously not quite working. I have the panel set to AutoSize, but it is not growing to accomodate the second TextBox. Also, I need to somehow push the second TextBox down when the first one grows. Is there some good way for the panel to know when ExpandoField gets an OnSizeChanged event? It seems like the growth of that panel would then need to cause the remainder of the list of panels to be redrawn in lower locations. I'm not sure how to get this cascade effect to work right...

I also think the use of the timer seems like an inefficient kluge...

I'm still learning WinForms. Is there some well-designed way I can get the behavior that I want? Is there some event I can catch when the WordWrap takes place (or when the text exceeds the size of the TextBox)? That would allow me to resize the TextBox. And how does the TextBox let the panel know that it has changed? Does it need to call the OnSizeChanged handler for it's parent panel? Does the panel need to call the OnSizeChanged handler for it's parent list?

Any suggestions?

Todd Hoatson
  • 123
  • 2
  • 18
  • `AutoSize` on a TextBox is related to the selected Font (the Control sizes itself when the Font changes). You have noticed that the Property is hidden in the PropertyGrid. You can also [see the notes](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/TextBoxBase.cs,238) in the .Net Source Code. Measure the Text. – Jimi Jan 20 '20 at 18:05
  • Thanks for pointing this out, @Jimi, I had not noticed, since I copied this code from another post. Maybe that explains why the OnSizeChanged code is currently not executing... the font is not being changed. – Todd Hoatson Jan 20 '20 at 22:03

1 Answers1

0

I believe I have the answer, after 3 or 4 failed attempts...

class ExpandoField : TextBox
{
    private bool UpdateInProgress = false;
    private static System.Text.RegularExpressions.Regex rgx = new System.Text.RegularExpressions.Regex(@"\r\n");

    public delegate void CallbackFn();
    CallbackFn VSizeChangedCallback;

    public ExpandoField(CallbackFn VSizeChanged)
    {
        AutoSize = false;
        VSizeChangedCallback = VSizeChanged;

        this.KeyDown += new KeyEventHandler(ExpandoField_KeyDown);
    }

    public void ExpandoField_KeyDown(object sender, KeyEventArgs e)
    {
        if (e.Modifiers == Keys.Control && e.KeyCode == Keys.A)
            this.SelectAll();
    }

    public void UpdateSize()
    {
        if (UpdateInProgress == false && Text.Length > 0)
        {
            UpdateInProgress = true;

            int numLines = 0;
            System.Drawing.Size baseSize = new System.Drawing.Size(Width, Int32.MaxValue);
            System.Drawing.Size lineSize = baseSize;  // compiler thinks we need something here...

            // replace CR/LF with single character (paragraph mark '¶')
            string tmpText = rgx.Replace(Text, "\u00B6");

            // split text at paragraph marks
            string[] parts = tmpText.Split(new char[1] { '\u00B6' });

            numLines = parts.Count();

            foreach (string part in parts)
            {
                // if the width of this line is greater than the width of the text box, add needed lines
                lineSize = TextRenderer.MeasureText(part, Font, baseSize, TextFormatFlags.TextBoxControl);
                numLines += (int) Math.Floor(((double) lineSize.Width / (double) Width));
            }

            if (numLines > 1)
                Multiline = true;
            else
                Multiline = false;

            int tempHeight = Margin.Top + (lineSize.Height * numLines) + Margin.Bottom;

            if (tempHeight > Height ||                 // need to grow...
                Height - tempHeight > lineSize.Height) // need to shrink...
            {
                Height = tempHeight;
                VSizeChangedCallback();
            }

            UpdateInProgress = false;
        }
    }

    public override sealed bool AutoSize
    {
        get { return base.AutoSize; }
        set { base.AutoSize = value; }
    }

    protected override void OnTextChanged(EventArgs e)
    {
        base.OnTextChanged(e);
        UpdateSize();
    }

    protected override void OnFontChanged(EventArgs e)
    {
        base.OnFontChanged(e);
        UpdateSize();
    }

    protected override void OnSizeChanged(EventArgs e)
    {
        base.OnSizeChanged(e);
        UpdateSize();
    }
}

Note that on the constructor this subclass of TextBox now accepts a delegate callback to let the parent class know that the TextBox has changed its size. (I suppose I should have handled the possibility of a null value here...)

Thankfully, this solution no longer required a timer.

I have tested this code pretty well, and I have watched it both grow & shrink. It respects MaximumSize, and it even handles the presence of carriage-return/line-feed pairs. (This code assumes Windows; it should be trivial to modify it for Linux, etc.) Feel free to suggest improvements.

Todd Hoatson
  • 123
  • 2
  • 18