如果文本不适合,如何在System.Windows.Forms.TextBox上显示滚动条?

时间:2021-10-27 02:53:39

For a System.Windows.Forms.TextBox with Multiline=True, I'd like to only show the scrollbars when the text doesn't fit.

对于Multiline = True的System.Windows.Forms.TextBox,我只想在文本不适合时显示滚动条。

This is a readonly textbox used only for display. It's a TextBox so that users can copy the text out. Is there anything built-in to support auto show of scrollbars? If not, should I be using a different control? Or do I need to hook TextChanged and manually check for overflow (if so, how to tell if the text fits?)

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


Not having any luck with various combinations of WordWrap and Scrollbars settings. I'd like to have no scrollbars initially and have each appear dynamically only if the text doesn't fit in the given direction.

没有任何运气与WordWrap和Scrollbars设置的各种组合。我想最初没有滚动条,只有当文本不适合给定的方向时才会动态显示。


@nobugz, thanks, that works when WordWrap is disabled. I'd prefer not to disable wordwrap, but it's the lesser of two evils.

@nobugz,谢谢,这在WordWrap被禁用时有效。我不想禁用wordwrap,但它是两个邪恶中较小的一个。


@André Neves, good point, and I would go that way if it was user-editable. I agree that consistency is the cardinal rule for UI intuitiveness.

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

6 个解决方案

#1


13  

Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.

在项目中添加一个新类并粘贴下面显示的代码。编译。将新控件从工具箱顶部拖放到表单上。它不是很完美,但应该适合你。

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

#2


31  

I came across this question when I wanted to solve the same problem.

当我想解决同样的问题时,我遇到了这个问题。

The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.

最简单的方法是更改​​为System.Windows.Forms.RichTextBox。在这种情况下,ScrollBars属性可以保留为RichTextBoxScrollBars.Both的默认值,表示“在需要时显示水平和垂直滚动条”。如果在TextBox上提供此功能,那将是很好的。

#3


7  

I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.

我还做了一些实验,发现如果启用它,垂直条将始终显示,并且水平条始终显示,只要它已启用且WordWrap == false。

I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!

我想你不会在这里得到你想要的东西。但是,我相信用户希望Windows的默认行为比您试图强制的更好。如果我使用你的应用程序,如果我的文本框房地产突然缩小,我可能会感到困扰,因为它需要容纳一个意想不到的滚动条,因为我给了它太多文本!

Perhaps it would be a good idea just to let your application follow Windows' look and feel.

也许让您的应用程序遵循Windows的外观和感觉是个好主意。

#4


6  

There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.

在nobugz的解决方案中有一个非常微妙的错误导致堆损坏,但前提是你使用AppendText()来更新TextBox。

Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.

从OnTextChanged设置ScrollBars属性将导致Win32窗口(句柄)被销毁并重新创建。但OnTextChanged是从Win32编辑控件(EditML_InsertText)的内容调用的,此后立即要求Win32编辑控件的内部状态保持不变。不幸的是,由于窗口被重新创建,操作系统已释放该内部状态,从而导致访问冲突。

So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.

所以故事的寓意是:如果你打算使用nobugz的解决方案,请不要使用AppendText()。

#5


2  

I had some success with the code below.

我在下面的代码中取得了一些成功。

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

#6


0  

What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.

Aidan所描述的几乎就是我面临的UI场景。由于文本框是只读的,我不需要它来响应TextChanged。而且我更喜欢自动滚动重新计算被延迟,因此在调整窗口大小时,它不会每秒发射数十次。

For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.

对于大多数UI来说,包含垂直和水平滚动条的文本框都是邪恶的,所以我只对这里的垂直滚动条感兴趣。

I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.

我还发现MeasureStrin*生的高度实际上大于所需的高度。使用文本框的PreferredHeight没有边框作为行高会产生更好的结果。

The following seems to work pretty well, with or without a border, and it works with WordWrap on.

以下似乎工作得很好,有或没有边框,它适用于WordWrap。

Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.

只需在需要时调用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;
    }
}

#1


13  

Add a new class to your project and paste the code shown below. Compile. Drop the new control from the top of the toolbox onto your form. It's not quite perfect but ought to work for you.

在项目中添加一个新类并粘贴下面显示的代码。编译。将新控件从工具箱顶部拖放到表单上。它不是很完美,但应该适合你。

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

#2


31  

I came across this question when I wanted to solve the same problem.

当我想解决同样的问题时,我遇到了这个问题。

The easiest way to do it is to change to System.Windows.Forms.RichTextBox. The ScrollBars property in this case can be left to the default value of RichTextBoxScrollBars.Both, which indicates "Display both a horizontal and a vertical scroll bar when needed." It would be nice if this functionality were provided on TextBox.

最简单的方法是更改​​为System.Windows.Forms.RichTextBox。在这种情况下,ScrollBars属性可以保留为RichTextBoxScrollBars.Both的默认值,表示“在需要时显示水平和垂直滚动条”。如果在TextBox上提供此功能,那将是很好的。

#3


7  

I also made some experiments, and found that the vertical bar will always show if you enable it, and the horizontal bar always shows as long as it's enabled and WordWrap == false.

我还做了一些实验,发现如果启用它,垂直条将始终显示,并且水平条始终显示,只要它已启用且WordWrap == false。

I think you're not going to get exactly what you want here. However, I believe that users would like better Windows' default behavior than the one you're trying to force. If I were using your app, I probably would be bothered if my textbox real-estate suddenly shrinked just because it needs to accomodate an unexpected scrollbar because I gave it too much text!

我想你不会在这里得到你想要的东西。但是,我相信用户希望Windows的默认行为比您试图强制的更好。如果我使用你的应用程序,如果我的文本框房地产突然缩小,我可能会感到困扰,因为它需要容纳一个意想不到的滚动条,因为我给了它太多文本!

Perhaps it would be a good idea just to let your application follow Windows' look and feel.

也许让您的应用程序遵循Windows的外观和感觉是个好主意。

#4


6  

There's an extremely subtle bug in nobugz's solution that results in a heap corruption, but only if you're using AppendText() to update the TextBox.

在nobugz的解决方案中有一个非常微妙的错误导致堆损坏,但前提是你使用AppendText()来更新TextBox。

Setting the ScrollBars property from OnTextChanged will cause the Win32 window (handle) to be destroyed and recreated. But OnTextChanged is called from the bowels of the Win32 edit control (EditML_InsertText), which immediately thereafter expects the internal state of that Win32 edit control to be unchanged. Unfortunately, since the window is recreated, that internal state has been freed by the OS, resulting in an access violation.

从OnTextChanged设置ScrollBars属性将导致Win32窗口(句柄)被销毁并重新创建。但OnTextChanged是从Win32编辑控件(EditML_InsertText)的内容调用的,此后立即要求Win32编辑控件的内部状态保持不变。不幸的是,由于窗口被重新创建,操作系统已释放该内部状态,从而导致访问冲突。

So the moral of the story is: don't use AppendText() if you're going to use nobugz's solution.

所以故事的寓意是:如果你打算使用nobugz的解决方案,请不要使用AppendText()。

#5


2  

I had some success with the code below.

我在下面的代码中取得了一些成功。

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

#6


0  

What Aidan describes is almost exactly the UI scenario I am facing. As the text box is read only, I don't need it to respond to TextChanged. And I'd prefer the auto-scroll recalculation to be delayed so it's not firing dozens of times per second while a window is being resized.

Aidan所描述的几乎就是我面临的UI场景。由于文本框是只读的,我不需要它来响应TextChanged。而且我更喜欢自动滚动重新计算被延迟,因此在调整窗口大小时,它不会每秒发射数十次。

For most UIs, text boxes with both vertical and horizontal scroll bars are, well, evil, so I'm only interested in vertical scroll bars here.

对于大多数UI来说,包含垂直和水平滚动条的文本框都是邪恶的,所以我只对这里的垂直滚动条感兴趣。

I also found that MeasureString produced a height that was actually bigger than what was required. Using the text box's PreferredHeight with no border as the line height gives a better result.

我还发现MeasureStrin*生的高度实际上大于所需的高度。使用文本框的PreferredHeight没有边框作为行高会产生更好的结果。

The following seems to work pretty well, with or without a border, and it works with WordWrap on.

以下似乎工作得很好,有或没有边框,它适用于WordWrap。

Simply call AutoScrollVertically() when you need it, and optionally specify recalculateOnResize.

只需在需要时调用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;
    }
}