How do I create an Autoscrolling TextBox

前端 未结 6 1341
囚心锁ツ
囚心锁ツ 2021-01-31 04:02

I have a WPF application that contains a multiline TextBox that is being used to display debugging text output.

How can I set the TextBox so that as text is appended to

相关标签:
6条回答
  • 2021-01-31 04:34

    Similar answer to the other answers, but without the statics events and control dictionary. (IMHO, static events are best avoided if possible).

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

    This is just an alternative to the other posted solutions, which were among the best I found after looking for awhile (i.e. concise & mvvm).

    0 讨论(0)
  • 2021-01-31 04:44

    This solution is inspired by Scott Ferguson's solution with the attached property, but avoids storing an internal dictionary of associations and thereby has somewhat shorter code:

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

    As far as I can tell, it behaves exactly as desired. Here's a test case with several text boxes in a window that allows the attached AlwaysScrollToEnd property to be set in various ways (hard-coded, with a CheckBox.IsChecked binding and in code-behind):

    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);
                }
            }
        }
    
    0 讨论(0)
  • 2021-01-31 04:47

    A more portable way might be to use an attached property such as in this similar question for listbox.

    (Just set VerticalOffset when the Text property changes)

    0 讨论(0)
  • 2021-01-31 04:49

    The answer provided by @BojinLi works well. After reading through the answer linked to by @GazTheDestroyer however, I decided to implement my own version for the TextBox, because it looked cleaner.

    To summarize, you can extend the behavior of the TextBox control by using an attached property. (Called ScrollOnTextChanged)

    Using it is simple:

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

    Here is the TextBoxBehaviour class:

    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;
                }
            }
    
        }
    }
    
    0 讨论(0)
  • 2021-01-31 04:51

    Hmm this seemed like an interesting thing to implement so I took a crack at it. From some goggling it doesn't seem like there is a straight forward way to "tell" the Textbox to scroll itself to the end. So I thought of it a different way. All framework controls in WPF have a default Style/ControlTemplate, and judging by the looks of the Textbox control there must be a ScrollViewer inside which handles the scrolling. So, why not just work with a local copy of the default Textbox ControlTemplate and programmaticlly get the ScrollViewer. I can then tell the ScrollViewer to scroll its Contents to the end. Turns out this idea works.

    Here is the test program I wrote, could use some refactoring but you can get the idea by looking at it:

    Here is the 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>
    

    And the code behind:

    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();
            }
        }
    }
    
    0 讨论(0)
  • 2021-01-31 04:53

    The problem with the "ScrollToEnd" method is that the TextBox has to be visible, or it won't scroll.

    Therefore a better method is to set the TextBox Selection property to end of document:

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

    BTW, the memory leak handling in the first example is likely unnecessary. The TextBox is the publisher and the static Attached Property event handler is the subscriber. The publisher keeps a reference to the subscriber which can keep the subscriber alive (not the other way around.) So if a TextBox goes out of scope, so will the reference to the static event handler (i.e., no memory leak.)

    So hooking up the Attached Property can be handled simpler:

      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;
         }
      }
    
    0 讨论(0)
提交回复
热议问题