可扩展面板中精心设计的可扩展文本框

Well-designed Expandable TextBoxes in an Expandable Panel

我的 .NET WinForms 应用程序中有记录,当记录可编辑时,我在面板上使用增强的 TextBox 控件进行布局,但当记录不可编辑时,我将 TextBox 设置为 ReadOnly。单击可编辑记录上的保存按钮会将文本保存到数据库,然后它显示为不可编辑的记录(直到单击编辑按钮)。请看下面的截屏:

如您所见,第一条记录不可编辑,但第二条可以。我遇到的问题是,如果文本太多而不适合,我希望 TextBox 的高度增长。看起来 TextBox 正在做 WordWrap,但它要么只显示一行文本,要么只显示前两行。总有东西在底部被切断。

我查看了该站点上的其他几个 post,尤其是 Expandable WinForms TextBox

下面是面板的一些示例代码:

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);

如您所见,我已将面板的 AutoSize 设置为 true。 Field2 的代码与 Field1 类似。

ExpandoField 基于我在 Expandable WinForms TextBox 中 dstran 的回复中看到的示例代码。它似乎是对标记为 post 答案的建议的最完整实施。这是代码:

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;
    }
}

这显然不太奏效。我已将面板设置为 AutoSize,但它不会增长以容纳第二个 TextBox。另外,当第一个 TextBox 变大时,我需要以某种方式将第二个 TextBox 向下推。面板是否有一些好方法可以知道 ExpandoField 何时获得 OnSizeChanged 事件?似乎该面板的增长需要导致面板列表的其余部分在较低位置重新绘制。我不确定如何让这个级联效果正常工作...

我也认为使用计时器似乎是一种低效的方式...

我还在学习 WinForms。是否有一些精心设计的方法可以获得我想要的行为?当 WordWrap 发生时(或当文本超过 TextBox 的大小时),我能捕捉到一些事件吗?这将允许我调整 TextBox 的大小。 TextBox 如何让面板知道它已更改?是否需要为其父面板调用 OnSizeChanged 处理程序?面板是否需要为其父列表调用 OnSizeChanged 处理程序?

有什么建议吗?

我相信我已经找到了答案,经过 3 或 4 次尝试失败...

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();
    }
}

请注意,在构造函数中,此 TextBox 的子 class 现在接受委托回调,让父 class 知道 TextBox 已更改其大小。 (我想我应该在这里处理空值的可能性......)

谢天谢地,此解决方案不再需要计时器。

我已经很好地测试了这段代码,并且我看到了它的增长和收缩。它遵守 MaximumSize,甚至可以处理 carriage-return/line-feed 对的存在。 (此代码假定 Windows;为 Linux 等修改它应该是微不足道的)欢迎提出改进建议。