【问题标题】:WPF ListBox automatic scrolling start and stop behaviorWPF ListBox 自动滚动开始和停止行为
【发布时间】:2014-09-12 03:54:44
【问题描述】:

我一直在尝试通过以下方式改进 WPF ListBox 控件的行为:添加新项目时,下面的 ListBox 会自动滚动到底部。它使用所示的 ScrollToBottom 函数执行此操作。使用显示的预览事件,如果用户单击一个项目,它会停止滚动,即使添加了更多项目。 (让它继续滚动会很讨厌!)如果用户用鼠标或滚轮手动滚动,那么它会以同样的方式停止滚动。

现在我在下面的代码中有一个按钮,可以再次开始自动滚动。

我的问题是这样的:如果用户将列表框一直滚动到底部,我该如何开始自动滚动,或者使用鼠标滚轮或键盘。这就是我以前的旧 Borland 列表框开箱即用的方式。

using System;
using System.Collections.ObjectModel;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;

// Note requires .NET framework 4.5
namespace MMP
{
  public partial class MainWindow : Window
  {
    public ObservableCollection<String> data { get; set; }

    public MainWindow()
    {
      InitializeComponent();
      data = new ObservableCollection<String>();
      DataContext = this;
      BeginAddingItems();
    }

    private async void BeginAddingItems()
    {
      await Task.Factory.StartNew(() =>
      {
        for (int i = 0; i < Int32.MaxValue; ++i)
        {
          if (i > 20) 
            Thread.Sleep(1000);
          AddToList("Added " + i.ToString());
        }
      });
    }

    void AddToList(String item)
    {
        Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
            new Action(() => { data.Add(item); ScrollToBottom(); }));
    }

    bool autoScroll = true;
    public void ScrollToBottom()
    {
      if (!autoScroll)
        return;
      if (listbox.Items.Count > 0)
        listbox.ScrollIntoView(listbox.Items[listbox.Items.Count - 1]);
    }

    private void listbox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
      autoScroll = false;
      Console.WriteLine("PreviewMouseDown: setting autoScroll to false");
    }

    private void listbox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
      Console.WriteLine("PreviewMouseWheel: setting autoScroll to false");
      autoScroll = false;
    }

    private void startButton_Click(object sender, RoutedEventArgs e)
    {
      ScrollToBottom(); // Catch up with the current last item.
      Console.WriteLine("startButton_Click: setting autoScroll to true");
      autoScroll = true;
    }

    private void listbox_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
      // Can this be useful?
    }
  }
}



<Window x:Class="MMP.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Test Scrolling"
        FontFamily="Verdana"
        Width="400" Height="250"
        WindowStartupLocation="CenterScreen">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="*" />
      <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>
    <ListBox x:Name="listbox" Grid.Row="0" 
             PreviewMouseWheel="listbox_PreviewMouseWheel" 
             PreviewMouseDown="listbox_PreviewMouseDown" 
             ItemsSource="{Binding data}" ScrollViewer.ScrollChanged="listbox_ScrollChanged" 
             >
    </ListBox>
    <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
      <Button x:Name="startButton" Click="startButton_Click" MinWidth="80" >Auto Scroll</Button>
    </StackPanel>
  </Grid>
</Window>

【问题讨论】:

    标签: wpf listbox scroll behavior


    【解决方案1】:

    使用以下代码实现了所需的列表框行为,感谢 Roel 提供了上面的初始 Behavior 框架。 这是一个包含行为代码的示例项目,以及可用于测试交互性的最小 WPF 窗口。

    测试窗口包含一个 ListBox,项目通过后台任务异步添加到其中。该行为的要点如下:

    1. 列表框会自动滚动以显示异步添加的新项目。
    2. 用户与列表框的交互会停止自动滚动 - AKA 令人讨厌的行为。
    3. 一旦完成交互,要继续自动滚动,用户将滚动条拖到底部并松开,或者使用鼠标滚轮或键盘来执行相同操作。这表明用户希望恢复自动滚动。

    AutoScrollBehavior.cs:

    using System;
    using System.Collections.Specialized;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Interactivity;
    using System.Windows.Media;
    
    namespace BehaviorTest.Code
    {
      // List box automatically scrolls to show new items as they are added asynchronously.
      // A user interaction with the listbox stops automatic scrolling - AKA obnoxious behavior.
      // Once finished interacting, to continue automatic scrolling, drag the scroll bar to 
      // the bottom and let go, or use the mouse wheel or keyboard to do the same. 
      // This indicates that the user wants automatic scrolling to resume.
    
      public class AutoScrollBehavior : Behavior<ListBox>
      {
        private ScrollViewer scrollViewer;
        private bool autoScroll = true;
        private bool justWheeled = false;
        private bool userInteracting = false;
        protected override void OnAttached()
        {
          AssociatedObject.Loaded += AssociatedObjectOnLoaded;
          AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;
        }
    
        private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs)
        {
          if (scrollViewer != null)
          {
            scrollViewer.ScrollChanged -= ScrollViewerOnScrollChanged;
          }
          AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
          AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
          AssociatedObject.GotMouseCapture -= AssociatedObject_GotMouseCapture;
          AssociatedObject.LostMouseCapture -= AssociatedObject_LostMouseCapture;
          AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
    
          scrollViewer = null;
        }
    
        private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs)
        {
          scrollViewer = GetScrollViewer(AssociatedObject);
          if (scrollViewer != null)
          {
            scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
    
            AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
            AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
            AssociatedObject.GotMouseCapture += AssociatedObject_GotMouseCapture;
            AssociatedObject.LostMouseCapture += AssociatedObject_LostMouseCapture;
            AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
          }
        }
    
        private static ScrollViewer GetScrollViewer(DependencyObject root)
        {
          int childCount = VisualTreeHelper.GetChildrenCount(root);
          for (int i = 0; i < childCount; ++i)
          {
            DependencyObject child = VisualTreeHelper.GetChild(root, i);
            ScrollViewer sv = child as ScrollViewer;
            if (sv != null)
              return sv;
    
            return GetScrollViewer(child);
          }
          return null;
        }
    
        void AssociatedObject_GotMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
        {
          // User is actively interacting with listbox. Do not allow automatic scrolling to interfere with user experience.
          userInteracting = true;
          autoScroll = false;
        }
    
        void AssociatedObject_LostMouseCapture(object sender, System.Windows.Input.MouseEventArgs e)
        {
          // User is done interacting with control.
          userInteracting = false;
        }
    
        private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e)
        {
          // diff is exactly zero if the last item in the list is visible. This can occur because of scroll-bar drag, mouse-wheel, or keyboard event.
          double diff = (scrollViewer.VerticalOffset - (scrollViewer.ExtentHeight - scrollViewer.ViewportHeight));
    
          // User just wheeled; this event is called immediately afterwards.
          if (justWheeled && diff != 0.0)
          {
            justWheeled = false;
            autoScroll = false;
            return;
          }
    
          if (diff == 0.0)
          {
            // then assume user has finished with interaction and has indicated through this action that scrolling should continue automatically.
            autoScroll = true;
          }
        }
    
        private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e)
        {
          if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset)
          { 
            // An item was added to the listbox, or listbox was cleared.
            if (autoScroll && !userInteracting)
            {
              // If automatic scrolling is turned on, scroll to the bottom to bring new item into view.
              // Do not do this if the user is actively interacting with the listbox.
              scrollViewer.ScrollToBottom();
            }
          }
        }
    
        private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs)
        {
          // User selected (clicked) an item, or used the keyboard to select a different item. 
          // Turn off automatic scrolling.
          autoScroll = false;
        }
    
        void AssociatedObject_PreviewMouseWheel(object sender, System.Windows.Input.MouseWheelEventArgs e)
        {
          // User wheeled the mouse. 
          // Cannot detect whether scroll viewer right at the bottom, because the scroll event has not occurred at this point.
          // Same for bubbling event.
          // Just indicated that the user mouse-wheeled, and that the scroll viewer should decide whether or not to stop autoscrolling.
          justWheeled = true;
        }
      }
    }
    

    MainWindow.xaml.cs:

    using BehaviorTest.Code;
    using System;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows;
    using System.Windows.Documents;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    using System.Windows.Threading;
    
    namespace BehaviorTest
    {
      public partial class MainWindow : Window
      {
        public ObservableCollection<String> data { get; set; }
        public MainWindow()
        {
          InitializeComponent();
          data = new ObservableCollection<String>();
          DataContext = this;
          Interaction.GetBehaviors(listbox).Add(new AutoScrollBehavior());
          BeginAddingItems();
        }
        private async void BeginAddingItems()
        {
          List<Task> tasks = new List<Task>();
    
          await Task.Factory.StartNew(() =>
          {
            for (int i = 0; i < Int32.MaxValue; ++i)
            {
              AddToList("Added Slowly: " + i.ToString());
              Thread.Sleep(2000);
              if (i % 3 == 0)
              {
                for (int j = 0; j < 5; ++j)
                {
                  AddToList("Added Quickly: " + j.ToString());
                  Thread.Sleep(200);
                }
              }
            }
          });
        }
    
        void AddToList(String item)
        {
          if (Application.Current == null)
            return; // Application is shutting down.
          Application.Current.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
              new Action(() => { data.Add(item); }));
        }
    
        private void clearButton_Click(object sender, RoutedEventArgs e)
        {
          data.Clear();
        }
    
        private void listbox_MouseDoubleClick(object sender, MouseButtonEventArgs e)
        {
          MessageBox.Show("Launch a modal dialog. Items are still added to the list in the background.");
        }
      }
    }
    

    MainWindow.xaml.cs:

    <Window x:Class="BehaviorTest.MainWindow"
            xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
            xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
            Title="Test Scrolling"
            FontFamily="Verdana"
            Width="400" Height="250"
            WindowStartupLocation="CenterScreen">
      <Grid>
        <Grid.RowDefinitions>
          <RowDefinition Height="*" />
          <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListBox x:Name="listbox" Grid.Row="0" 
                 ItemsSource="{Binding data}"
                 MouseDoubleClick="listbox_MouseDoubleClick" >
        </ListBox>
        <StackPanel Orientation="Horizontal" Grid.Row="1" HorizontalAlignment="Right">
          <Button x:Name="startButton" Click="clearButton_Click" MinWidth="80" >Clear</Button>
        </StackPanel>
      </Grid>
    </Window>
    

    【讨论】:

    • 编辑了 AutoScrollBehavior.cs 以允许单个鼠标滚轮事件停止滚动。
    • 我已经尝试了许多不同的这种行为的实现,这是唯一一个在经过轻微调整以使其适用于ItemsControl 后真正按照我想要的方式工作的方法。
    【解决方案2】:

    您可以尝试创建一个 Blend Behavior 来为您执行此操作。这是一个小小的开始:

    public class AutoScrollBehavior:Behavior<ListBox> 
    {
        private ScrollViewer scrollViewer;
        private bool autoScroll = true;
        protected override void OnAttached() 
        {
            AssociatedObject.Loaded += AssociatedObjectOnLoaded;
            AssociatedObject.Unloaded += AssociatedObjectOnUnloaded;      
        }
    
        private void AssociatedObjectOnUnloaded(object sender, RoutedEventArgs routedEventArgs) 
        {
            AssociatedObject.SelectionChanged -= AssociatedObjectOnSelectionChanged;
            AssociatedObject.ItemContainerGenerator.ItemsChanged -= ItemContainerGeneratorItemsChanged;
    
            scrollViewer = null;
        }
    
        private void AssociatedObjectOnLoaded(object sender, RoutedEventArgs routedEventArgs) 
        {
            scrollViewer = GetScrollViewer(AssociatedObject);
            if(scrollViewer != null) 
            {
                scrollViewer.ScrollChanged += ScrollViewerOnScrollChanged;
    
                AssociatedObject.SelectionChanged += AssociatedObjectOnSelectionChanged;
                AssociatedObject.ItemContainerGenerator.ItemsChanged += ItemContainerGeneratorItemsChanged;
            }
        }
    
        private void ScrollViewerOnScrollChanged(object sender, ScrollChangedEventArgs e) {
            if (e.VerticalOffset == e.ExtentHeight-e.ViewportHeight) {
                autoScroll = true;
            }
        }
    
        private static ScrollViewer GetScrollViewer(DependencyObject root) 
        {
            int childCount = VisualTreeHelper.GetChildrenCount(root);
            for (int i = 0; i < childCount; i++) 
            {
                DependencyObject child = VisualTreeHelper.GetChild(root, i);
                ScrollViewer sv = child as ScrollViewer;
                if (sv != null)
                    return sv;
    
                return GetScrollViewer(child);
            }
    
            return null;
        }
    
        private void ItemContainerGeneratorItemsChanged(object sender, System.Windows.Controls.Primitives.ItemsChangedEventArgs e) 
        {
            if (e.Action == NotifyCollectionChangedAction.Add || e.Action == NotifyCollectionChangedAction.Reset) {
                if (autoScroll) {
                    scrollViewer.ScrollToBottom();
    
                }
            }
        }
    
        private void AssociatedObjectOnSelectionChanged(object sender, SelectionChangedEventArgs selectionChangedEventArgs) 
        {
            autoScroll = false;
        }
    }
    

    【讨论】:

    • 感谢您的详细回复 - 我会在周末看看这个。我们仍然需要处理一些情况——例如如果您按住滚动条,滚动(也就是说,您已捕获鼠标),同时另一个项目被添加到列表中。我需要列出类似的可能操作。
    • 我在上面扩展了您的解决方案,并在下面发布了生成的代码以及一个小型测试项目。我会重视你对可用性的看法——行为是否直观清晰?谢谢!
    • @Dean 我能找到的唯一麻烦是在它停止自动滚动之前需要多次滚动鼠标滚轮,但除此之外,它工作得很好。顺便说一句,您可能还需要删除 ScrollViewerOnScrollChanged 事件。
    • 我用单个鼠标滚轮事件解决了这个问题。 AutoScrollBehavior.cs 中的代码已更新。 wheel 事件在滚动查看器更改之前被触发,因此它需要两个轮子事件才能工作。感谢您的帮助。
    猜你喜欢
    • 1970-01-01
    • 2011-01-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-26
    • 2020-05-09
    • 1970-01-01
    • 2021-08-18
    相关资源
    最近更新 更多