WPF ListBox自动滚动启动和停止行为

时间:2014-09-12 03:54:44

标签: wpf listbox scroll behavior

我一直在尝试以下列方式改进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>

2 个答案:

答案 0 :(得分:1)

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

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

  1. 列表框会自动滚动显示新项目,因为它们是异步添加的。
  2. 用户与列表框的交互会停止自动滚动 - AKA令人讨厌的行为。
  3. 完成交互后,要继续自动滚动,用户将滚动条拖动到底部然后放开,或使用鼠标滚轮或键盘执行相同操作。这表示用户希望自动滚动恢复。
  4. AutoScrolBehavior.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>
    

答案 1 :(得分:0)

您可以尝试创建一个为您执行此操作的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;
    }
}