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

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

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

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

【问题讨论】:

    标签: c# wpf .net-3.5 wpf-controls


    【解决方案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 变量放在处理程序中而不是外部,请参阅stackoverflow.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);
                  }
              }
              

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

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2023-03-29
                • 1970-01-01
                相关资源
                最近更新 更多