27

デバッグテキスト出力を表示するために使用されている複数行のTextBoxを含むWPFアプリケーションがあります。

テキストがボックスに追加されると、テキストボックスの一番下まで自動的にスクロールするようにTextBoxを設定するにはどうすればよいですか?

  • MVVMパターンを使用しています。
  • 理想的には、純粋なXAMLアプローチがいいでしょう。
  • TextBox自体が必ずしもフォーカスされているわけではありません。
4

6 に答える 6

47

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

    }
}
于 2012-04-12T21:51:18.153 に答える
10

このソリューションは、プロパティが添付された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();
            }
        }
    }

私の知る限り、それは希望どおりに動作します。これは、ウィンドウ内にいくつかのテキストボックスがあり、アタッチ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>

コードビハインド:

    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);
            }
        }
    }
于 2012-09-10T06:44:46.697 に答える
4

うーん、これは実装するのに面白いもののように思えたので、私はそれを試してみました。いくつかのゴーグルから、テキストボックスにそれ自体を最後までスクロールするように「指示」する簡単な方法はないようです。だから私はそれを別の方法で考えました。WPFのすべてのフレームワークコントロールにはデフォルトのStyle/ControlTemplateがあり、Textboxコントロールの外観から判断すると、スクロールを処理するScrollViewerが内部に存在する必要があります。したがって、デフォルトのTextbox ControlTemplateのローカルコピーを操作して、プログラムで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();
        }
    }
}
于 2012-04-11T04:39:33.037 に答える
3

より移植性の高い方法は、リストボックスに関するこの同様の質問のように、添付されたプロパティを使用することです。

(プロパティが変更されVerticalOffsetたときに設定するだけです)Text

于 2012-04-11T08:41:47.423 に答える
3

他の回答と同様の回答ですが、統計イベントと制御ディクショナリはありません。(私見、静的イベントは可能であれば避けるのが最善です)。

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

これは、しばらく探した後に見つけた最高のソリューションの1つである他の投稿されたソリューション(つまり、簡潔でmvvm)の単なる代替手段です。

于 2014-06-02T23:31:47.733 に答える
1

「ScrollToEnd」メソッドの問題は、TextBoxを表示する必要があることです。そうしないと、スクロールしません。

したがって、より適切な方法は、TextBoxSelectionプロパティをドキュメントの終わりに設定することです。

  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はパブリッシャーであり、静的なAttachedPropertyイベントハンドラーはサブスクライバーです。パブリッシャーは、サブスクライバーを存続させることができるサブスクライバーへの参照を保持します(その逆ではありません)。したがって、TextBoxがスコープ外になると、静的イベントハンドラーへの参照も保持されます(つまり、メモリリークは発生しません)。

したがって、添付プロパティの接続はより簡単に処理できます。

  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;
     }
  }
于 2013-08-24T06:26:42.160 に答える