Multiline Textbox with automatic vertical scroll

2020-02-08 10:07发布

问题:

There are a lot of similiar questions over internet, on SO included, but proposed solutions doesn't work in my case. Scenario : there is a log textbox in xaml

 <TextBox Name="Status"
          Margin="5"
          Grid.Column="1"
          Grid.Row="5"
          HorizontalAlignment="Left"
          VerticalAlignment="Top"
          Width="600"
          Height="310"/>

There are methods in code-behind that do some work and add some multiline (maybe that's the problem?) messages into this textbox:

private static void DoSomeThings(TextBox textBox)
{
   // do work
   textBox.AppendText("Work finished\r\n"); // better way than Text += according to msdn
   // do more
   textBox.AppendText("One more message\r\n");
   ...
}

private static void DoSomething2(TextBox textBox)
{
   // same as first method
}

Need to scroll to bottom of textbox after all actions take place. Tried ScrollToEnd(), ScrollToLine, wrapping textbox into ScrollViewer, Selection and Caret workarounds, attaching ScrollToEnd to TextChanged. None of this works, after execution lines that overflow textbox height still need to be scrolled to manually. Sorry for duplicate question, i guess i'm missing some minor issues that can be resolved quickly by someone that has fresh vision on the problem. Thanks in advance.

回答1:

According to this question: TextBox.ScrollToEnd doesn't work when the TextBox is in a non-active tab

You have to focus the text box, update the caret position and then scroll to end:

Status.Focus();
Status.CaretIndex = Status.Text.Length;
Status.ScrollToEnd();

EDIT

Example TextBox:

<TextBox TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" 
         AcceptsReturn="True" Name="textBox"/>


回答2:

If you make it into a simple custom control then you don't need any code behind to do the scrolling.

public class ScrollingTextBox : TextBox {

    protected override void OnInitialized (EventArgs e) {
        base.OnInitialized(e);
        VerticalScrollBarVisibility = ScrollBarVisibility.Auto;
        HorizontalScrollBarVisibility = ScrollBarVisibility.Auto;
    }

    protected override void OnTextChanged (TextChangedEventArgs e) {
        base.OnTextChanged(e);
        CaretIndex = Text.Length;
        ScrollToEnd();
    }

}

If you're using WPF it would be far better to use binding rather than passing the text box around in the code behind.



回答3:

If you don't like code behind to much, here is an AttachedProperty that will do the trick :

namespace YourProject.YourAttachedProperties
{

    public class TextBoxAttachedProperties
    {

        public static bool GetAutoScrollToEnd(DependencyObject obj)
        {
            return (bool)obj.GetValue(AutoScrollToEndProperty);
        }

        public static void SetAutoScrollToEnd(DependencyObject obj, bool value)
        {
            obj.SetValue(AutoScrollToEndProperty, value);
        }

        // Using a DependencyProperty as the backing store for AutoScrollToEnd.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty AutoScrollToEndProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEnd", typeof(bool), typeof(TextBoxAttachedProperties), new PropertyMetadata(false, AutoScrollToEndPropertyChanged));

        private static void AutoScrollToEndPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            if(d is TextBox textbox && e.NewValue is bool mustAutoScroll)
            {
                textbox.TextChanged += (s, ee)=> AutoScrollToEnd(s, ee, textbox);
            }
        }

        private static void AutoScrollToEnd(object sender, TextChangedEventArgs e, TextBox textbox)
        {
            textbox.ScrollToEnd();
        }
    }
}

And then in your xaml just do :

<TextBox
    AcceptsReturn="True"
    myAttachedProperties:TextBoxAttachedProperties.AutoScrollToEnd="True"/>

Just don't forget to add at the top of your xaml file

xmlns:myAttachedProperties="clr-namespace:YourProject.YourAttachedProperties"

And voila



回答4:

Thanks! I have added this to remember the original focus:

var oldFocusedElement = FocusManager.GetFocusedElement(this);

this.textBox.Focus();
this.textBox.CaretIndex = this.textBox.Text.Length;
this.textBox.ScrollToEnd();

FocusManager.SetFocusedElement(this, oldFocusedElement);