2014-09-12 3 views
2

Я пытаюсь улучшить поведение элемента управления ListBox WPF следующим образом: ListBox ниже автоматически прокручивается вниз, когда новые элементы добавляются. Он делает это, используя показанную функцию ScrollToBottom. Используя отображаемые события предварительного просмотра, если пользователь нажимает на элемент, он останавливает прокрутку, даже если добавлено больше элементов. (Было бы неприятно, если бы он продолжал прокручивать!) Если пользователь вручную прокручивает мышью или колесом, то он останавливает прокрутку таким же образом.WPF ListBox автоматический запуск и остановка прокрутки

Прямо сейчас у меня есть кнопка в коде ниже, который запускает автоматическую прокрутку снова.

Мой вопрос заключается в следующем: Как я могу начать автоматическую прокрутку, если пользователь прокручивает либо ListBox весь путь вниз к нижней, или же эквивалент с помощью колеса мыши или клавиатуры. Вот как мои старые списки 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> 

ответ

1

желаемое поведение ListBox было достигнуто, используя следующий код, с добрыми благодаря Roel для обеспечения начального поведения <> рамки выше. Это пример проекта, который содержит код поведения, а также минимальное окно WPF, которое можно использовать для проверки интерактивности.

В тестовом окне содержится ListBox, к которому элементы добавляются асинхронно с помощью фоновой задачи. Важными точками поведения являются следующие:

  1. В окне списка автоматически прокручивается, чтобы показать новые элементы, поскольку они добавляются асинхронно.
  2. Пользовательское взаимодействие со списком прекращает автоматическую прокрутку - неприятное поведение AKA.
  3. После завершения взаимодействия, чтобы продолжить автоматическую прокрутку, пользователь перетаскивает полосу прокрутки в нижнюю часть и позволяет перейти или использовать колесико мыши или клавиатуру для того, чтобы сделать то же самое. Это означает, что пользователь хочет, чтобы автоматическая прокрутка возобновлялась.

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> 
+0

Отредактировано AutoScrolBehavior.cs, чтобы разрешить одиночное событие колеса мыши остановить прокрутку. – Dean

+0

Я пробовал много разных реализаций этого поведения, и это единственное, что действительно работает так, как я хочу, после небольших адаптаций, чтобы заставить его работать для 'ItemsControl'. –

0

Вы могли бы попробовать создать BlendBehavior, что делает это для вас. Это небольшой старт:

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

спасибо за подробный ответ - буду Посмотрите на это в выходные. Нам все еще нужно обрабатывать некоторые случаи - например, если вы удерживаете полосу прокрутки, прокручиваете (то есть, вы захватили мышь), и одновременно добавляется еще один элемент. Мне нужно составить список подобных возможных действий. – Dean

+0

Я расширил ваше решение выше и разместил полученный ниже код вместе с небольшим тестовым проектом. Я бы оценил ваше мнение об удобстве использования - это поведение интуитивное и ясное? Благодаря! – Dean

+1

@Dean Единственная неприятность, которую я мог найти, нужно было прокручивать с помощью колеса мыши более одного раза, прежде чем он остановит автоматический прокрутки, но, кроме этого, он работает очень хорошо. Кстати, вам, возможно, также потребуется удалить событие ScrollViewerOnScrollChanged. –

Смежные вопросы