开发者

How to automatically scroll ScrollViewer - only if the user did not change scroll position

I would like to create the following behaviour in a ScrollViewer that wraps ContentControl:

When the ContentControl height grow开发者_如何学JAVAs , the ScrollViewer should automatically scroll to the end. This is easy to achive by using ScrollViewer.ScrollToEnd().

However, if the user uses the scroll bar, the automatic scrolling shouldn't happen anymore. This is similar to what happens in VS output window for example.

The problem is to know when a scrolling has happened because of user scrolling and when it happened because the content size changed. I tried to play with the ScrollChangedEventArgsof ScrollChangedEvent, but couldn't get it to work.

Ideally, I do not want to handle all possible Mouse and keyboard events.


You can use ScrollChangedEventArgs.ExtentHeightChange to know if a ScrollChanged is due to a change in the content or to a user action... When the content is unchanged, the ScrollBar position sets or unsets the auto-scroll mode. When the content has changed you can apply auto-scrolling.

Code behind:

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


Here is an adaptation from several sources.

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

Use it in your XAML like so:

<ScrollViewer Height="230" HorizontalScrollBarVisibility="Auto" extensionProperties:ScrollViewerExtension.AlwaysScrollToEnd="True">
    <TextBlock x:Name="Trace"/>
</ScrollViewer>


This code will automatically scroll to end when the content grows if it was previously scrolled all the way down.

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>

Code behind:

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


Here is a method I have used with good results. Based on two dependency properties. It avoids code behind and timers as shown in the other answer.

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

This uses a handler defined as.

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

Then simply use this in XAML as:

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

With local being a namespace import at the top of XAML file in question. This avoids the static bool seen in other answers.


What about using the "TextChanged" event of the TextBox and the ScrollToEnd() method?

 private void consolebox_TextChanged(object sender, TextChangedEventArgs e)
    {
        this.consolebox.ScrollToEnd();
    }


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 Programmer


On Windows builds 17763 and newer, one can set VerticalAnchorRatio="1" on the ScrollViewer and that's it.

HOWEVER: There's a bug that is still open: https://github.com/Microsoft/microsoft-ui-xaml/issues/562


In windows 10, .ScrollToVerticalOffset is obsolete. so I use ChangeView like this.

TextBlock messageBar;
ScrollViewer messageScroller; 

    private void displayMessage(string message)
    {

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

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


Previous answer rewritten to work with floating point comparison. Be aware that this solution, though simple, will PREVENT the user from scrolling as soon as the content is scrolled to the bottom.

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


Based on the second answer, why can't it just be:

private void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)
{
    if (e.ExtentHeightChange != 0)
    {
        ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
    }
}

I have tested it on my application and it works.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜