如何在System.Windows.Forms.TextBox上只有当文本不适合时显示滚动条?

28

对于设置Multiline=True的System.Windows.Forms.TextBox,我希望仅当文本无法容纳时才显示滚动条。

这是一个只用于显示的只读文本框。它是一个TextBox,以便用户可以将文本复制出来。是否有内置支持自动显示滚动条的功能?如果没有,我应该使用不同的控件吗?还是需要挂钩TextChanged并手动检查溢出(如果是这样,如何确定文本是否适合?)


在各种WordWrap和Scrollbars设置的组合中没有获得任何成功。我希望最初没有滚动条,并且只有在给定方向上的文本无法容纳时才动态地出现每个滚动条。


@nobugz,谢谢,当禁用WordWrap时它有效。我希望不要禁用wordwrap,但这是两害相权取其轻的选择。


@André Neves,好观点,如果可以由用户编辑,我会这样做。我同意一致性是UI直觉的基本规则。

6个回答

34
当我想解决同样的问题时,我遇到了这个问题。
最简单的方法是改用System.Windows.Forms.RichTextBox。在这种情况下,ScrollBars属性可以保留为RichTextBoxScrollBars.Both的默认值,表示“需要时同时显示水平和垂直滚动条”。如果TextBox提供了这个功能,那就太好了。

2
+1:简单,使用RichTextBox代替。对我有用。谢谢lax。 - MattH
7
请注意,RichTextBox 是为 RTF 文本设计的,与多行文本框相比,它具有更高的渲染和处理成本。除非您确实需要显示富文本,否则建议不要使用它。 - Camille
3
我使用了这个RichTextBox解决方案,没有产生任何负面影响。我相信在大多数情况下,Camille的担忧是没有必要的。谢谢user73892。 - Radim Cernej

14

在您的项目中添加一个新类并粘贴下面显示的代码。编译。从工具箱的顶部拖动新控件到您的表单上。它可能不是完美的,但应该能为您工作。

using System;
using System.Drawing;
using System.Windows.Forms;

public class MyTextBox : TextBox {
  private bool mScrollbars;
  public MyTextBox() {
    this.Multiline = true;
    this.ReadOnly = true;
  }
  private void checkForScrollbars() {
    bool scroll = false;
    int cnt = this.Lines.Length;
    if (cnt > 1) {
      int pos0 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(0)).Y;
      if (pos0 >= 32768) pos0 -= 65536;
      int pos1 = this.GetPositionFromCharIndex(this.GetFirstCharIndexFromLine(1)).Y;
      if (pos1 >= 32768) pos1 -= 65536;
      int h = pos1 - pos0;
      scroll = cnt * h > (this.ClientSize.Height - 6);  // 6 = padding
    }
    if (scroll != mScrollbars) {
      mScrollbars = scroll;
      this.ScrollBars = scroll ? ScrollBars.Vertical : ScrollBars.None;
    }
  }

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

  protected override void OnClientSizeChanged(EventArgs e) {
    checkForScrollbars();
    base.OnClientSizeChanged(e);
  }
}

4
只有在文本中明确添加换行符时才适用。如果要显示换行的长文本,则该方法不起作用,因为TextBox.Lines始终为1。只是想提一下,因为这正是我正在寻找的情况,而这种方法并没有完全解决问题。 - Tim
已在原始帖子中提到。点击“提问”按钮以寻求帮助。 - Hans Passant

7
在nobugz的解决方案中存在一个非常微妙的错误,如果您使用AppendText()来更新TextBox,则会导致堆破坏。
从OnTextChanged设置ScrollBars属性将导致Win32窗口(句柄)被销毁并重新创建。但是OnTextChanged是从Win32编辑控件(EditML_InsertText)的内部调用的,该控件随即期望该Win32编辑控件的内部状态保持不变。不幸的是,由于窗口被重新创建,因此该内部状态已被操作系统释放,从而导致访问冲突。
所以故事的寓意是:如果您要使用nobugz的解决方案,请不要使用AppendText()。

我遇到了一个访问冲突问题,似乎与显示一个带有 TextBox 的表单有关(https://dev59.com/nFvUa4cB1Zd3GeqPs2Gu)。看到您的帖子后,我认为这可能是由于调用 AppendText() 导致的。但是,我用 TextBox.Text = 替换了对 AppendText 的调用,有时仍然会出现访问冲突。从您的答复中,我是否可以理解 TextBox.Text = 应该没问题,而 AppendText() 则不行?还是它们都存在相同的问题(它们都会调用 OnTextChanged 吗)? - Jimmy

7
我还进行了一些实验,发现如果启用垂直滚动条,则它将始终显示,而只要启用了水平滚动条且“WordWrap == false”,则水平滚动条始终显示。
我认为你在这里不会得到完全想要的结果。然而,我相信用户更喜欢Windows的默认行为,而不是你试图强制的行为。如果我使用你的应用程序,我可能会因为文本框需要适应意外滚动条而突然缩小而感到困扰!
也许让你的应用程序遵循Windows的外观和感觉是个好主意。

2
我使用以下代码取得了一些成功。
  public partial class MyTextBox : TextBox
  {
    private bool mShowScrollBar = false;

    public MyTextBox()
    {
      InitializeComponent();

      checkForScrollbars();
    }

    private void checkForScrollbars()
    {
      bool showScrollBar = false;
      int padding = (this.BorderStyle == BorderStyle.Fixed3D) ? 14 : 10;

      using (Graphics g = this.CreateGraphics())
      {
        // Calcualte the size of the text area.
        SizeF textArea = g.MeasureString(this.Text,
                                         this.Font,
                                         this.Bounds.Width - padding);

        if (this.Text.EndsWith(Environment.NewLine))
        {
          // Include the height of a trailing new line in the height calculation        
          textArea.Height += g.MeasureString("A", this.Font).Height;
        }

        // Show the vertical ScrollBar if the text area
        // is taller than the control.
        showScrollBar = (Math.Ceiling(textArea.Height) >= (this.Bounds.Height - padding));

        if (showScrollBar != mShowScrollBar)
        {
          mShowScrollBar = showScrollBar;
          this.ScrollBars = showScrollBar ? ScrollBars.Vertical : ScrollBars.None;
        }
      }
    }

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

    protected override void OnResize(EventArgs e)
    {
      checkForScrollbars();
      base.OnResize(e);
    }
  }

0
Aidan所描述的几乎完全是我所面临的UI场景。由于文本框是只读的,我不需要它响应TextChanged事件。而且我更喜欢自动滚动重新计算被延迟,这样在调整窗口大小时就不会每秒触发数十次。
对于大多数UI来说,同时具有垂直和水平滚动条的文本框都很难用,因此我只关注垂直滚动条。
我还发现MeasureString生成的高度实际上比所需的要大。使用文本框的PreferredHeight作为行高(没有边框)可以得到更好的结果。
以下代码似乎运行得非常好,无论是否有边框,而且它也适用于WordWrap。
需要时只需调用AutoScrollVertically(),并可选择指定recalculateOnResize。
public class TextBoxAutoScroll : TextBox
{
    public void AutoScrollVertically(bool recalculateOnResize = false)
    {
        SuspendLayout();

        if (recalculateOnResize)
        {
            Resize -= OnResize;
            Resize += OnResize;
        }

        float linesHeight = 0;
        var   borderStyle = BorderStyle;

        BorderStyle       = BorderStyle.None;

        int textHeight    = PreferredHeight;

        try
        {
            using (var graphics = CreateGraphics())
            {
                foreach (var text in Lines)
                {
                    var textArea = graphics.MeasureString(text, Font);

                    if (textArea.Width < Width)
                        linesHeight += textHeight;
                    else
                    {
                        var numLines = (float)Math.Ceiling(textArea.Width / Width);

                        linesHeight += textHeight * numLines;
                    }
                }
            }

            if (linesHeight > Height)
                ScrollBars = ScrollBars.Vertical;
            else
                ScrollBars = ScrollBars.None;
        }
        catch (Exception ex)
        {
            System.Diagnostics.Debug.WriteLine(ex);
        }
        finally
        {
            BorderStyle = borderStyle;

            ResumeLayout();
        }
    }

    private void OnResize(object sender, EventArgs e)
    {
        m_timerResize.Stop();

        m_timerResize.Tick    -= OnDelayedResize;
        m_timerResize.Tick    += OnDelayedResize;
        m_timerResize.Interval = 475;

        m_timerResize.Start();
    }

    Timer m_timerResize = new Timer();

    private void OnDelayedResize(object sender, EventArgs e)
    {
        m_timerResize.Stop();

        Resize -= OnResize;

        AutoScrollVertically();

        Resize += OnResize;
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接