如何创建自动滚动文本框

Sco*_*son 23 wpf xaml mvvm

我有一个WPF应用程序,其中包含一个多行TextBox,用于显示调试文本输出.

如何设置TextBox以便将文本附加到框中,它会自动滚动到文本框的底部?

  • 我正在使用MVVM模式.
  • 理想情况下,纯XAML方法会很好.
  • TextBox本身不一定是焦点.

Sco*_*son 41

@BojinLi提供的答案效果很好.在阅读了@GazTheDestroyer链接的答案之后,我决定为TextBox实现我自己的版本,因为它看起来更干净.

总而言之,您可以使用附加属性来扩展TextBox控件的行为.(称为ScrollOnTextChanged)

使用它很简单:

<TextBox src:TextBoxBehaviour.ScrollOnTextChanged="True" VerticalScrollBarVisibility="Auto" />
Run Code Online (Sandbox Code Playgroud)

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

    }
}
Run Code Online (Sandbox Code Playgroud)


O. *_*per 9

这个解决方案的灵感来自Scott Ferguson的附加属性解决方案,但避免存储关联的内部字典,因此代码更短:

    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();
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)

据我所知,它的行为完全符合要求.这是一个测试用例,在窗口中有几个文本框,允许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>
Run Code Online (Sandbox Code Playgroud)

代码隐藏:

    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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)