如何自动滚动 ScrollViewer - 仅当用户未更改滚动位置时

Posted

技术标签:

【中文标题】如何自动滚动 ScrollViewer - 仅当用户未更改滚动位置时【英文标题】:How to automatically scroll ScrollViewer - only if the user did not change scroll position 【发布时间】:2011-02-28 09:55:03 【问题描述】:

我想在包含ContentControlScrollViewer 中创建以下行为: 当 ContentControl 高度增长时,ScrollViewer 应该会自动滚动到末尾。这很容易通过使用ScrollViewer.ScrollToEnd() 来实现。 但是,如果用户使用滚动条,则不应再发生自动滚动。这类似于 VS 输出窗口中发生的情况。

问题是要知道什么时候因为用户滚动而发生了滚动,什么时候因为内容大小发生了变化。我尝试使用ScrollChangedEventArgsScrollChangedEvent,但无法正常工作。

理想情况下,我不想处理所有可能的鼠标和键盘事件。

【问题讨论】:

【参考方案1】:

如果内容之前一直向下滚动,则此代码将在内容增长时自动滚动到结束。

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

【讨论】:

此代码将全天每 2 秒检查一次是否有要滚动的内容。与下面的事件驱动解决方案相比,这既慢又效率低。【参考方案2】:

您可以使用 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);
        
    

【讨论】:

我希望这种行为与 TextBox 一起使用,结果证明使用此代码并将 TextBox 嵌入到 ScrollViewer 中而不是尝试使用 TextBox 的内置滚动是最简单的。 谢谢,我发现让 ScrollViewer 根据 TextBlock 的内容自动滚动非常有用。我确实做了一些小的修改,比如使用private bool AutoScroll = true 并将其放入方法中。 private Boolean AutoScroll = true 导致“无效的表达式术语 'private'”错误。问题,这是“有效的 WPF 样式”吗?还是不使用绑定破坏了 WPF 的“精神”? 我试图做一个更简单的解决方案,但最终很像这个。不过,我将 AutoScroll 变量放在处理程序中而不是外部,请参阅***.com/questions/25761795/… 你,我的朋友,是个英雄!【参考方案3】:

这里有几个来源的改编。

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>

【讨论】:

完美运行。滚动到底部时自动滚动(从初始设置或用户恢复时)。当用户滚动位置不是底部时保持固定。很好的信息汇总。 +1 也适用于可以添加到我的工具包并减少重复代码隐藏的附加属性。 这太棒了。拥有干净利落的附加属性总是好的。 这个答案有误。 _autoScroll 字段是静态的,这意味着如果多次使用此类,则状态将交叉使用。该状态需要明确绑定到ScrollViewer。此外,ReSharper 报告浮点类型之间的相等比较,这是一个禁忌。 完美满足我的需求。谢谢! 可以和 ListView 一起使用吗?有什么办法可以将它附加到 ListView 的 ScrollViewer 上?【参考方案4】:

这是我使用的一种方法,效果很好。基于两个依赖属性。它避免了代码落后和计时器,如另一个答案所示。

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

【讨论】:

【参考方案5】:
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);
        

Вот так вроде-бы привельнее чему 华尔街程序员

【讨论】:

В английском языке на этом сайте / 本网站仅提供英文版。你需要修复你的代码(缩进)。【参考方案6】:

使用TextBox的“TextChanged”事件和ScrollToEnd()方法怎么样?

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

【讨论】:

【参考方案7】:

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

【讨论】:

【参考方案8】:

重写以前的答案以使用浮点比较。请注意,此解决方案虽然简单,但会在内容滚动到底部时阻止用户滚动。

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

【讨论】:

【参考方案9】:

在 Windows 17763 及更高版本上,可以在ScrollViewer 上设置VerticalAnchorRatio="1",仅此而已。

但是:仍有一个错误:https://github.com/Microsoft/microsoft-ui-xaml/issues/562

【讨论】:

【参考方案10】:

根据第二个答案,为什么不能这样:

private void ScrollViewer_ScrollChanged(Object sender, ScrollChangedEventArgs e)

    if (e.ExtentHeightChange != 0)
    
        ScrollViewer.ScrollToVerticalOffset(ScrollViewer.ExtentHeight);
    

我已经在我的应用程序上对其进行了测试,它可以工作。

【讨论】:

以上是关于如何自动滚动 ScrollViewer - 仅当用户未更改滚动位置时的主要内容,如果未能解决你的问题,请参考以下文章

WPF ScrollViewer 滚动条问题

WPF 如何流畅地滚动ScrollViewer 简单实现下

ScrollViewer 鼠标滚轮不滚动

WPF 如何流畅地滚动ScrollViewer

如何在 Blend 的设计时滚动 ScrollViewer

wpf 动态添加滚动条