デバッグテキスト出力を表示するために使用されている複数行のTextBoxを含むWPFアプリケーションがあります。
テキストがボックスに追加されると、テキストボックスの一番下まで自動的にスクロールするようにTextBoxを設定するにはどうすればよいですか?
- MVVMパターンを使用しています。
- 理想的には、純粋なXAMLアプローチがいいでしょう。
- TextBox自体が必ずしもフォーカスされているわけではありません。
@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;
}
}
}
}
このソリューションは、プロパティが添付された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);
}
}
}
うーん、これは実装するのに面白いもののように思えたので、私はそれを試してみました。いくつかのゴーグルから、テキストボックスにそれ自体を最後までスクロールするように「指示」する簡単な方法はないようです。だから私はそれを別の方法で考えました。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();
}
}
}
より移植性の高い方法は、リストボックスに関するこの同様の質問のように、添付されたプロパティを使用することです。
(プロパティが変更されVerticalOffset
たときに設定するだけです)Text
他の回答と同様の回答ですが、統計イベントと制御ディクショナリはありません。(私見、静的イベントは可能であれば避けるのが最善です)。
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)の単なる代替手段です。
「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;
}
}