Как автоматически прокрутить ScrollViewer - только если пользователь не изменил положение прокрутки - PullRequest
30 голосов
/ 06 июня 2010

Я хотел бы создать следующее поведение в ScrollViewer, который включает ContentControl:Когда высота ContentControl увеличивается, ScrollViewer автоматически прокручивается до конца.Этого легко достичь, используя ScrollViewer.ScrollToEnd().Однако, если пользователь использует полосу прокрутки, автоматическая прокрутка больше не должна происходить.Это похоже на то, что происходит в окне вывода VS, например.

Проблема состоит в том, чтобы знать, когда прокрутка произошла из-за пользовательской прокрутки, а когда - из-за изменения размера контента.Я пытался играть с ScrollChangedEventArgs из ScrollChangedEvent, но не смог заставить его работать.

В идеале я не хочу обрабатывать все возможные события мыши и клавиатуры.

Ответы [ 8 ]

56 голосов
/ 11 октября 2013

Вы можете использовать ScrollChangedEventArgs.ExtentHeightChange, чтобы узнать, вызван ли ScrollChanged изменением содержимого или действием пользователя ... Когда содержимое не изменяется, позиция ScrollBar устанавливает или отменяет режим автоматической прокрутки.Когда содержимое изменилось, вы можете применить автоматическую прокрутку.

Код позади:

    private Boolean AutoScroll = true;

    private void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
    {
        // User scroll event : set or unset auto-scroll mode
        if (e.ExtentHeightChange == 0)
        {   // Content unchanged : user scroll event
            if (ScrollViewer.VerticalOffset == ScrollViewer.ScrollableHeight)
            {   // Scroll bar is in bottom
                // Set auto-scroll mode
                AutoScroll = true;
            }
            else
            {   // Scroll bar isn't in bottom
                // Unset auto-scroll mode
                AutoScroll = false;
            }
        }

        // Content scroll event : auto-scroll eventually
        if (AutoScroll && e.ExtentHeightChange != 0)
        {   // Content changed and auto-scroll mode set
            // Autoscroll
            ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
        }
    }
27 голосов
/ 13 февраля 2014

Вот адаптация из нескольких источников.

public class ScrollViewerExtensions
    {
        public static readonly DependencyProperty AlwaysScrollToEndProperty = DependencyProperty.RegisterAttached("AlwaysScrollToEnd", typeof(bool), typeof(ScrollViewerExtensions), new PropertyMetadata(false, AlwaysScrollToEndChanged));
        private static bool _autoScroll;

        private static void AlwaysScrollToEndChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scroll = sender as ScrollViewer;
            if (scroll != null)
            {
                bool alwaysScrollToEnd = (e.NewValue != null) && (bool)e.NewValue;
                if (alwaysScrollToEnd)
                {
                    scroll.ScrollToEnd();
                    scroll.ScrollChanged += ScrollChanged;
                }
                else { scroll.ScrollChanged -= ScrollChanged; }
            }
            else { throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); }
        }

        public static bool GetAlwaysScrollToEnd(ScrollViewer scroll)
        {
            if (scroll == null) { throw new ArgumentNullException("scroll"); }
            return (bool)scroll.GetValue(AlwaysScrollToEndProperty);
        }

        public static void SetAlwaysScrollToEnd(ScrollViewer scroll, bool alwaysScrollToEnd)
        {
            if (scroll == null) { throw new ArgumentNullException("scroll"); }
            scroll.SetValue(AlwaysScrollToEndProperty, alwaysScrollToEnd);
        }

        private static void ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            ScrollViewer scroll = sender as ScrollViewer;
            if (scroll == null) { throw new InvalidOperationException("The attached AlwaysScrollToEnd property can only be applied to ScrollViewer instances."); }

            // User scroll event : set or unset autoscroll mode
            if (e.ExtentHeightChange == 0) { _autoScroll = scroll.VerticalOffset == scroll.ScrollableHeight; }

            // Content scroll event : autoscroll eventually
            if (_autoScroll && e.ExtentHeightChange != 0) { scroll.ScrollToVerticalOffset(scroll.ExtentHeight); }
        }
    }

Используйте его в своем XAML следующим образом:

<ScrollViewer Height="230" HorizontalScrollBarVisibility="Auto" extensionProperties:ScrollViewerExtension.AlwaysScrollToEnd="True">
    <TextBlock x:Name="Trace"/>
</ScrollViewer>
8 голосов
/ 08 июня 2010

Этот код будет автоматически прокручиваться до конца при увеличении контента, если он был прокручен до конца.

XAML:

<Window x:Class="AutoScrollTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">
    <ScrollViewer Name="_scrollViewer">
        <Border BorderBrush="Red" BorderThickness="5" Name="_contentCtrl" Height="200" VerticalAlignment="Top">
        </Border>
    </ScrollViewer>
</Window>

Код:

using System;
using System.Windows;
using System.Windows.Threading;

namespace AutoScrollTest
{
    public partial class Window1 : Window
    {
        public Window1()
        {
            InitializeComponent();

            DispatcherTimer timer = new DispatcherTimer();
            timer.Interval = new TimeSpan(0, 0, 2);
            timer.Tick += ((sender, e) =>
                {
                    _contentCtrl.Height += 10;

                    if (_scrollViewer.VerticalOffset == _scrollViewer.ScrollableHeight)
                    {
                        _scrollViewer.ScrollToEnd();
                    }
                });
            timer.Start();
        }
    }
}
4 голосов
/ 09 мая 2014

Вот метод, который я использовал с хорошими результатами. На основе двух свойств зависимости. Он избегает кода и таймеров, как показано в другом ответе.

public static class ScrollViewerEx
{
    public static readonly DependencyProperty AutoScrollProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEnd", 
            typeof(bool), typeof(ScrollViewerEx), 
            new PropertyMetadata(false, HookupAutoScrollToEnd));

    public static readonly DependencyProperty AutoScrollHandlerProperty =
        DependencyProperty.RegisterAttached("AutoScrollToEndHandler", 
            typeof(ScrollViewerAutoScrollToEndHandler), typeof(ScrollViewerEx));

    private static void HookupAutoScrollToEnd(DependencyObject d, 
            DependencyPropertyChangedEventArgs e)
    {
        var scrollViewer = d as ScrollViewer;
        if (scrollViewer == null) return;

        SetAutoScrollToEnd(scrollViewer, (bool)e.NewValue);
    }

    public static bool GetAutoScrollToEnd(ScrollViewer instance)
    {
        return (bool)instance.GetValue(AutoScrollProperty);
    }

    public static void SetAutoScrollToEnd(ScrollViewer instance, bool value)
    {
        var oldHandler = (ScrollViewerAutoScrollToEndHandler)instance.GetValue(AutoScrollHandlerProperty);
        if (oldHandler != null)
        {
            oldHandler.Dispose();
            instance.SetValue(AutoScrollHandlerProperty, null);
        }
        instance.SetValue(AutoScrollProperty, value);
        if (value)
            instance.SetValue(AutoScrollHandlerProperty, new ScrollViewerAutoScrollToEndHandler(instance));
    }

Используется обработчик, определенный как.

public class ScrollViewerAutoScrollToEndHandler : DependencyObject, IDisposable
{
    readonly ScrollViewer m_scrollViewer;
    bool m_doScroll = false;

    public ScrollViewerAutoScrollToEndHandler(ScrollViewer scrollViewer)
    {
        if (scrollViewer == null) { throw new ArgumentNullException("scrollViewer"); }

        m_scrollViewer = scrollViewer;
        m_scrollViewer.ScrollToEnd();
        m_scrollViewer.ScrollChanged += ScrollChanged;
    }

    private void ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        // User scroll event : set or unset autoscroll mode
        if (e.ExtentHeightChange == 0) 
        { m_doScroll = m_scrollViewer.VerticalOffset == m_scrollViewer.ScrollableHeight; }

        // Content scroll event : autoscroll eventually
        if (m_doScroll && e.ExtentHeightChange != 0) 
        { m_scrollViewer.ScrollToVerticalOffset(m_scrollViewer.ExtentHeight); }
    }

    public void Dispose()
    {
        m_scrollViewer.ScrollChanged -= ScrollChanged;
    }

Затем просто используйте это в XAML как:

<ScrollViewer VerticalScrollBarVisibility="Auto" 
              local:ScrollViewerEx.AutoScrollToEnd="True">
    <TextBlock x:Name="Test test test"/>
</ScrollViewer>

* local - импорт пространства имен вверху рассматриваемого файла XAML. Это позволяет избежать static bool, замеченного в других ответах.

2 голосов
/ 05 января 2015
bool autoScroll = false;

        if (e.ExtentHeightChange != 0)
        {   
            if (infoScroll.VerticalOffset == infoScroll.ScrollableHeight - e.ExtentHeightChange)
            { 
                autoScroll = true;
            }
            else
            {   
                autoScroll = false;
            }
        }
        if (autoScroll)
        {   
            infoScroll.ScrollToVerticalOffset(infoScroll.ExtentHeight);
        }

Вот так вроде-бы привельнее чем у Программиста Wallstreet

1 голос
/ 04 июля 2016

Как насчет использования события "TextChanged" TextBox и метода ScrollToEnd ()?

 private void consolebox_TextChanged(object sender, TextChangedEventArgs e)
    {
        this.consolebox.ScrollToEnd();
    }
0 голосов
/ 13 марта 2017

Предыдущий ответ переписан для работы с сравнением с плавающей запятой. Имейте в виду, что это решение, хотя и простое, предотвратит прокрутку пользователя, как только содержимое будет прокручено вниз.

private bool _should_auto_scroll = true;
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e) {
    if (Math.Abs(e.ExtentHeightChange) < float.MinValue) {
        _should_auto_scroll = Math.Abs(ScrollViewer.VerticalOffset - ScrollViewer.ScrollableHeight) < float.MinValue;
    }
    if (_should_auto_scroll && Math.Abs(e.ExtentHeightChange) > float.MinValue) {
        ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
    }
}
0 голосов
/ 23 июля 2016

В Windows 10 .ScrollToVerticalOffset устарел. поэтому я использую ChangeView вот так.

TextBlock messageBar;
ScrollViewer messageScroller; 

    private void displayMessage(string message)
    {

                messageBar.Text += message + "\n";

                double pos = this.messageScroller.ExtentHeight;
                messageScroller.ChangeView(null, pos, null);
    } 
...