我一直在尝试以下列方式改进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>
答案 0 :(得分:1)
使用以下代码实现了所需的列表框行为,并且感谢Roel提供了初始行为&lt;&gt;上面的框架。 这是一个包含行为代码的示例项目,以及可用于测试交互性的最小WPF窗口。
测试窗口包含一个ListBox,通过后台任务异步添加项目。行为的重点如下:
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;
}
}