Подтвердить что ты не робот

Как создать Autoscrolling TextBox

У меня есть приложение WPF, которое содержит многострочный TextBox, который используется для отображения вывода отладочного текста.

Как я могу установить TextBox, чтобы при добавлении текста к нему он автоматически прокручивался до нижней части текстового поля?

  • Я использую шаблон MVVM.
  • В идеале чистый подход XAML был бы приятным.
  • Сам TextBox не обязательно находится в фокусе.
4b9b3361

Ответ 1

Ответ, предоставленный @BojinLi, хорошо работает. После прочтения ответа, связанного с @GazTheDestroyer, однако я решил реализовать свою собственную версию для TextBox, потому что он выглядел чище.

Подводя итог, вы можете расширить поведение элемента управления TextBox, используя прикрепленное свойство. (Вызывается ScrollOnTextChanged)

Использование просто:

<TextBox src:TextBoxBehaviour.ScrollOnTextChanged="True" VerticalScrollBarVisibility="Auto" />

Вот класс TextBoxBehaviour:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;

namespace MyNamespace
{
    public class TextBoxBehaviour
    {
        static readonly Dictionary<TextBox, Capture> _associations = new Dictionary<TextBox, Capture>();

        public static bool GetScrollOnTextChanged(DependencyObject dependencyObject)
        {
            return (bool)dependencyObject.GetValue(ScrollOnTextChangedProperty);
        }

        public static void SetScrollOnTextChanged(DependencyObject dependencyObject, bool value)
        {
            dependencyObject.SetValue(ScrollOnTextChangedProperty, value);
        }

        public static readonly DependencyProperty ScrollOnTextChangedProperty =
            DependencyProperty.RegisterAttached("ScrollOnTextChanged", typeof (bool), typeof (TextBoxBehaviour), new UIPropertyMetadata(false, OnScrollOnTextChanged));

        static void OnScrollOnTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            var textBox = dependencyObject as TextBox;
            if (textBox == null)
            {
                return;
            }
            bool oldValue = (bool) e.OldValue, newValue = (bool) e.NewValue;
            if (newValue == oldValue)
            {
                return;
            }
            if (newValue)
            {
                textBox.Loaded += TextBoxLoaded;
                textBox.Unloaded += TextBoxUnloaded;
            }
            else
            {
                textBox.Loaded -= TextBoxLoaded;
                textBox.Unloaded -= TextBoxUnloaded;
                if (_associations.ContainsKey(textBox))
                {
                    _associations[textBox].Dispose();
                }
            }
        }

        static void TextBoxUnloaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var textBox = (TextBox) sender;
            _associations[textBox].Dispose();
            textBox.Unloaded -= TextBoxUnloaded;
        }

        static void TextBoxLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
            var textBox = (TextBox) sender;
            textBox.Loaded -= TextBoxLoaded;
            _associations[textBox] = new Capture(textBox);
        }

        class Capture : IDisposable
        {
            private TextBox TextBox { get; set; }

            public Capture(TextBox textBox)
            {
                TextBox = textBox;
                TextBox.TextChanged += OnTextBoxOnTextChanged;
            }

            private void OnTextBoxOnTextChanged(object sender, TextChangedEventArgs args)
            {
                TextBox.ScrollToEnd();
            }

            public void Dispose()
            {
                TextBox.TextChanged -= OnTextBoxOnTextChanged;
            }
        }

    }
}

Ответ 2

Это решение вдохновлено решением Скотта Фергюсона с прикрепленным свойством, но избегает хранения внутреннего словаря ассоциаций и тем самым имеет несколько более короткий код:

    using System;
    using System.Windows;
    using System.Windows.Controls;

    namespace AttachedPropertyTest
    {
        public static class TextBoxUtilities
        {
            public static readonly DependencyProperty AlwaysScrollToEndProperty = DependencyProperty.RegisterAttached("AlwaysScrollToEnd",
                                                                                                                      typeof(bool),
                                                                                                                      typeof(TextBoxUtilities),
                                                                                                                      new PropertyMetadata(false, AlwaysScrollToEndChanged));

            private static void AlwaysScrollToEndChanged(object sender, DependencyPropertyChangedEventArgs e)
            {
                TextBox tb = sender as TextBox;
                if (tb != null) {
                    bool alwaysScrollToEnd = (e.NewValue != null) && (bool)e.NewValue;
                    if (alwaysScrollToEnd) {
                        tb.ScrollToEnd();
                        tb.TextChanged += TextChanged;
                    } else {
                        tb.TextChanged -= TextChanged;
                    }
                } else {
                    throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to TextBox instances.");
                }
            }

            public static bool GetAlwaysScrollToEnd(TextBox textBox)
            {
                if (textBox == null) {
                    throw new ArgumentNullException("textBox");
                }

                return (bool)textBox.GetValue(AlwaysScrollToEndProperty);
            }

            public static void SetAlwaysScrollToEnd(TextBox textBox, bool alwaysScrollToEnd)
            {
                if (textBox == null) {
                    throw new ArgumentNullException("textBox");
                }

                textBox.SetValue(AlwaysScrollToEndProperty, alwaysScrollToEnd);
            }

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

Насколько я могу судить, он ведет себя точно так, как хотелось бы. Здесь тестовый пример с несколькими текстовыми полями в окне, который позволяет установить прикрепленное свойство AlwaysScrollToEnd различными способами (жестко закодированное с привязкой CheckBox.IsChecked и в коде):

Xaml:

    <Window x:Class="AttachedPropertyTest.Window1"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="AttachedPropertyTest" Height="800" Width="300"
        xmlns:local="clr-namespace:AttachedPropertyTest">
        <Window.Resources>
            <Style x:Key="MultiLineTB" TargetType="TextBox">
                <Setter Property="IsReadOnly" Value="True"/>
                <Setter Property="VerticalScrollBarVisibility" Value="Auto"/>
                <Setter Property="Height" Value="60"/>
                <Setter Property="Text" Value="{Binding Text, ElementName=tbMaster}"/>
            </Style>
        </Window.Resources>

        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="Auto"/>
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
                <RowDefinition Height="Auto"/>
            </Grid.RowDefinitions>

            <TextBox Background="LightYellow" Name="tbMaster" Height="150" AcceptsReturn="True"/>

            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="1" local:TextBoxUtilities.AlwaysScrollToEnd="True"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="2"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="3" Name="tb3" local:TextBoxUtilities.AlwaysScrollToEnd="True"/>
            <TextBox Style="{StaticResource MultiLineTB}" Grid.Row="4" Name="tb4"/>
            <CheckBox Grid.Column="1" Grid.Row="4" IsChecked="{Binding (local:TextBoxUtilities.AlwaysScrollToEnd), Mode=TwoWay, ElementName=tb4}"/>
            <Button Grid.Row="5" Click="Button_Click"/>
        </Grid>
    </Window>

Code-Behind:

    using System;
    using System.Windows;
    using System.Windows.Controls;

    namespace AttachedPropertyTest
    {
        public partial class Window1 : Window
        {
            public Window1()
            {
                InitializeComponent();
            }

            void Button_Click(object sender, RoutedEventArgs e)
            {
                TextBoxUtilities.SetAlwaysScrollToEnd(tb3, true);
            }
        }
    }

Ответ 3

Хм, мне показалось, что это было интересно, поэтому я сделал треск. Из некоторых взглядов не кажется, что есть прямой способ "рассказать" текстовому полю, чтобы прокручивать себя до конца. Поэтому я подумал об этом по-другому. Все элементы управления каркасом в WPF имеют стиль Style/ControlTemplate по умолчанию, и, судя по внешнему виду элемента управления Textbox, должен быть ScrollViewer, внутри которого выполняется прокрутка. Итак, почему бы просто не работать с локальной копией текстового элемента управления TextBoxTemplate и программно получить ScrollViewer. Затем я могу сказать ScrollViewer прокрутить содержимое до конца. Оказывается, эта идея работает.

Вот тестовая программа, которую я написал, может использовать некоторый рефакторинг, но вы можете получить эту идею, посмотрев на нее:

Вот XAML:

<Window x:Class="WpfApplication3.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:WpfApplication3="clr-namespace:WpfApplication3"
        Title="MainWindow" Height="350" Width="525">
  <Window.Resources>
    <!--The default Style for the Framework Textbox-->
    <SolidColorBrush x:Key="DisabledForegroundBrush" Color="#888" />
    <SolidColorBrush x:Key="DisabledBackgroundBrush" Color="#EEE" />
    <SolidColorBrush x:Key="WindowBackgroundBrush" Color="#FFF" />
    <SolidColorBrush x:Key="SolidBorderBrush" Color="#888" />
    <ControlTemplate x:Key="MyTextBoxTemplate" TargetType="{x:Type TextBoxBase}">
      <Border x:Name="Border" CornerRadius="2" Padding="2" Background="{StaticResource WindowBackgroundBrush}"
              BorderBrush="{StaticResource SolidBorderBrush}" BorderThickness="1">
        <ScrollViewer Margin="0" x:Name="PART_ContentHost" />
      </Border>
      <ControlTemplate.Triggers>
        <Trigger Property="IsEnabled" Value="False">
          <Setter TargetName="Border" Property="Background" Value="{StaticResource DisabledBackgroundBrush}" />
          <Setter TargetName="Border" Property="BorderBrush" Value="{StaticResource DisabledBackgroundBrush}" />
          <Setter Property="Foreground" Value="{StaticResource DisabledForegroundBrush}" />
        </Trigger>
      </ControlTemplate.Triggers>
    </ControlTemplate>
    <Style x:Key="MyTextBox" TargetType="{x:Type TextBoxBase}">
      <Setter Property="SnapsToDevicePixels" Value="True" />
      <Setter Property="OverridesDefaultStyle" Value="True" />
      <Setter Property="KeyboardNavigation.TabNavigation" Value="None" />
      <Setter Property="FocusVisualStyle" Value="{x:Null}" />
      <Setter Property="MinWidth" Value="120" />
      <Setter Property="MinHeight" Value="20" />
      <Setter Property="AllowDrop" Value="true" />
      <Setter Property="Template" Value="{StaticResource MyTextBoxTemplate}"></Setter>
    </Style>

  </Window.Resources>
  <Grid>
    <WpfApplication3:AutoScrollTextBox x:Name="textbox" TextWrapping="Wrap" Style="{StaticResource MyTextBox}"
                                       VerticalScrollBarVisibility="Visible" AcceptsReturn="True" Width="100" Height="100">test</WpfApplication3:AutoScrollTextBox>
  </Grid>
</Window>

И код позади:

using System;
using System.Windows;
using System.Windows.Controls;

namespace WpfApplication3
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            for (int i = 0; i < 10; i++)
            {
                textbox.AppendText("Line " + i + Environment.NewLine);
            }
        }
    }

    public class AutoScrollTextBox : TextBox
    {
        protected override void OnTextChanged(TextChangedEventArgs e)
        {
            base.OnTextChanged(e);
            // Make sure the Template is in the Visual Tree: 
            // http://stackoverflow.com/questions/2285491/wpf-findname-returns-null-when-it-should-not
            ApplyTemplate();
            var template = (ControlTemplate) FindResource("MyTextBoxTemplate");
            var scrollViewer = template.FindName("PART_ContentHost", this) as ScrollViewer;
            //SelectionStart = Text.Length;
            scrollViewer.ScrollToEnd();
        }
    }
}

Ответ 4

Более переносимым способом может быть использование прикрепленного свойства, такого как этот аналогичный вопрос для списка.

(Просто установите VerticalOffset при изменении свойства Text)

Ответ 5

Подобный ответ на другие ответы, но без статистических событий и словаря управления. (IMHO, статические события лучше всего избегать, если это возможно).

public class ScrollToEndBehavior
{
    public static readonly DependencyProperty OnTextChangedProperty =
                DependencyProperty.RegisterAttached(
                "OnTextChanged",
                typeof(bool),
                typeof(ScrollToEndBehavior),
                new UIPropertyMetadata(false, OnTextChanged)
                );

    public static bool GetOnTextChanged(DependencyObject dependencyObject)
    {
        return (bool)dependencyObject.GetValue(OnTextChangedProperty);
    }

    public static void SetOnTextChanged(DependencyObject dependencyObject, bool value)
    {
        dependencyObject.SetValue(OnTextChangedProperty, value);
    }

    private static void OnTextChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var newValue = (bool)e.NewValue;

        if (textBox == null || (bool)e.OldValue == newValue)
        {
            return;
        }

        TextChangedEventHandler handler = (object sender, TextChangedEventArgs args) =>
            ((TextBox)sender).ScrollToEnd();

        if (newValue)
        {
            textBox.TextChanged += handler;
        }
        else
        {
            textBox.TextChanged -= handler;
        }
    }
}

Это просто альтернатива другим опубликованным решениям, которые были одними из лучших, которые я нашел после долгого поиска (т.е. сжатого и mvvm).

Ответ 6

Проблема с методом "ScrollToEnd" заключается в том, что TextBox должен быть видимым или не будет прокручиваться.

Поэтому лучшим способом является установка свойства TextBox Selection для завершения документа:

  static void tb_TextChanged(object sender, TextChangedEventArgs e)
  {
     TextBox tb = sender as TextBox;
     if (tb == null)
     {
        return;
     }

     // set selection to end of document
     tb.SelectionStart = int.MaxValue;
     tb.SelectionLength = 0;         
  }

Кстати, обработка утечки памяти в первом примере вероятно, не требуется. TextBox является издателем, а статический обработчик события Attached Property - это подписчик. Издатель сохраняет ссылку на подписчика, который может поддерживать абонента (не наоборот). Поэтому, если TextBox выходит из области видимости, то будет ссылка на обработчик статического события (т.е. Утечка памяти).

Таким образом, подключение Attached Property может быть проще:

  static void OnAutoTextScrollChanged
      (DependencyObject obj, DependencyPropertyChangedEventArgs args)
  {
     TextBox tb = obj as TextBox;
     if (tb == null)
     {
        return;
     }

     bool b = (bool)args.NewValue;

     if (b)
     {
        tb.TextChanged += tb_TextChanged;
     }
     else
     {
        tb.TextChanged -= tb_TextChanged;
     }
  }