webentwicklung-frage-antwort-db.com.de

Wie erstelle ich eine Autoscrolling-Textbox?

Ich habe eine WPF-Anwendung, die eine mehrzeilige TextBox enthält, die zum Anzeigen der Debugging-Textausgabe verwendet wird.

Wie kann ich das Textfeld so einstellen, dass beim Anhängen von Text an das Feld automatisch ein Bildlauf zum unteren Rand des Textfelds ausgeführt wird?

  • Ich verwende das MVVM-Muster.
  • Ideal wäre ein reiner XAML-Ansatz.
  • Die TextBox selbst ist nicht unbedingt im Fokus.
22
Scott Ferguson

Die Antwort von @BojinLi funktioniert gut. Nachdem ich die von @GazTheDestroyer verlinkte Antwort gelesen hatte, entschloss ich mich jedoch, meine eigene Version für die TextBox zu implementieren, da sie sauberer aussah.

Zusammenfassend können Sie das Verhalten des TextBox-Steuerelements mithilfe einer angefügten Eigenschaft erweitern. (Aufgerufen ScrollOnTextChanged)

Die Benutzung ist einfach:

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

Hier ist die TextBoxBehaviour-Klasse:

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

    }
}
40
Scott Ferguson

Diese Lösung ist inspiriert von Scott Fergusons Lösung mit der angehängten Eigenschaft, vermeidet jedoch das Speichern eines internen Assoziationswörterbuchs und hat daher einen etwas kürzeren 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();
            }
        }
    }

Soweit ich das beurteilen kann, verhält es sich genau wie gewünscht. Hier ist ein Testfall mit mehreren Textfeldern in einem Fenster, in dem die angehängte Eigenschaft AlwaysScrollToEnd auf verschiedene Arten festgelegt werden kann (fest codiert, mit einer CheckBox.IsChecked-Bindung und in CodeBehind):

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);
            }
        }
    }
9
O. R. Mapper

Ähnliche Antwort zu den anderen Antworten, jedoch ohne statische Ereignisse und Kontrollwörterbuch. (Nach meiner Meinung werden statische Ereignisse am besten vermieden, wenn möglich).

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

Dies ist nur eine Alternative zu den anderen veröffentlichten Lösungen, die zu den besten gehörten, die ich nach einiger Zeit gefunden habe (d. H. Kurz und bündig).

3
Steve Kinyon

Ein portablerer Weg könnte sein, eine angehängte Eigenschaft wie in diese ähnliche Frage für Listbox zu verwenden.

(Setzen Sie einfach VerticalOffset, wenn sich die Eigenschaft Text ändert.)

3
GazTheDestroyer

Hmm, das schien eine interessante Sache zu sein, also habe ich mich damit beschäftigt. Bei manchen Brillen scheint es keinen direkten Weg zu geben, die Textbox anzuweisen, sich selbst bis zum Ende zu scrollen. Also habe ich es anders gesehen. Alle Framework-Steuerelemente in WPF haben ein Standard-Style/ControlTemplate, und nach dem Aussehen des Textbox-Steuerelements muss ein ScrollViewer vorhanden sein, der das Scrollen übernimmt. Also, warum nicht einfach mit einer lokalen Kopie der Standard-Textbox ControlTemplate arbeiten und programmatisch den ScrollViewer bekommen. Ich kann dann den ScrollViewer anweisen, seinen Inhalt bis zum Ende zu scrollen. Es stellt sich heraus, dass diese Idee funktioniert.

Hier ist das Testprogramm, das ich geschrieben habe. Es könnte etwas überarbeitet werden, aber Sie können die Idee bekommen, indem Sie es sich ansehen:

Hier ist die 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>

Und der Code dahinter:

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

Das Problem mit der "ScrollToEnd" -Methode besteht darin, dass das Textfeld sichtbar sein muss, sonst wird kein Bildlauf ausgeführt.

Daher ist es eine bessere Methode, die TextBox Selection-Eigenschaft auf das Ende des Dokuments festzulegen:

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

Übrigens ist die Speicherverlustbehandlung im ersten Beispiel wahrscheinlich unnötig . Das Textfeld ist der Herausgeber und der Abonnent der statische Ereignishandler für angefügte Eigenschaften. Der Herausgeber behält einen Verweis auf den Abonnenten bei, der den Abonnenten am Leben erhalten kann (nicht umgekehrt). Wenn also eine TextBox den Gültigkeitsbereich verlässt, wird der Verweis auf den statischen Ereignishandler ebenfalls gelöscht (d. H. Kein Speicherverlust).

So kann das Anschließen der angehängten Eigenschaft einfacher gehandhabt werden:

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