在我的应用程序中,我有一个带有项目的 ListBox
。这个应用程序是使用 WPF 编写的。
我该如何自动滚动到最后添加的项目?当新项目被添加时,我想让 ScrollViewer
移动到列表的末尾。
是否有像 ItemsChanged
这样的事件?(我不想使用 SelectionChanged
事件)
最简单的方法是:
if (VisualTreeHelper.GetChildrenCount(listView) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(listView, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
这段代码始终适用于 ListView 和 ListBox 控件。将此代码附加到 listView.Items.SourceCollection.CollectionChanged
事件上,您将拥有完全自动的自动滚动行为。
试试这个:
lstBox.SelectedIndex = lstBox.Items.Count -1;
lstBox.ScrollIntoView(lstBox.SelectedItem) ;
在您的 MainWindow 中,这将选择并聚焦于列表中的最后一项!
lstBox.SelectedIndex = 0
;) - DerpyNerdstruct
或实现比较器以比较值而非引用的 record
。此外,问题只回答了一半:你会在哪个事件中执行它? - Emy Blacksmith请注意,listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
只在没有重复项时起作用。如果有内容相同的项目,则向下滚动到第一个查找结果。
我找到的解决方案如下:
ListBoxAutomationPeer svAutomation =
ListBoxAutomationPeer)ScrollViewerAutomationPeer.
CreatePeerForElement(myListBox);
IScrollProvider scrollInterface =
(IScrollProvider)svAutomation.GetPattern(PatternInterface.Scroll);
System.Windows.Automation.ScrollAmount scrollVertical =
System.Windows.Automation.ScrollAmount.LargeIncrement;
System.Windows.Automation.ScrollAmount scrollHorizontal =
System.Windows.Automation.ScrollAmount.NoAmount;
// If the vertical scroller is not available,
// the operation cannot be performed, which will raise an exception.
if (scrollInterface.VerticallyScrollable)
scrollInterface.Scroll(scrollHorizontal, scrollVertical);
最佳解决方案是使用 ListBox 控件内部的 ItemCollection 对象,该集合专门为内容查看器而设计。它具有预定义的方法来选择最后一个项目并保持光标位置引用...
myListBox.Items.MoveCurrentToLast();
myListBox.ScrollIntoView(myListBox.Items.CurrentItem);
一个略微不同的方法,与迄今为止提出的方法有所不同。
您可以使用 ScrollViewer
的 ScrollChanged
事件,并观察 ScrollViewer
内容变得更大。
private void ListBox_OnLoaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox) sender;
var scrollViewer = FindScrollViewer(listBox);
if (scrollViewer != null)
{
scrollViewer.ScrollChanged += (o, args) =>
{
if (args.ExtentHeightChange > 0)
scrollViewer.ScrollToBottom();
};
}
}
这样可以避免与 ListBox
的 ItemsSource
绑定更改相关的问题。
ScrollViewer
也可以在不假设 ListBox
使用默认控件模板的情况下找到。
// Search for ScrollViewer, breadth-first
private static ScrollViewer FindScrollViewer(DependencyObject root)
{
var queue = new Queue<DependencyObject>(new[] {root});
do
{
var item = queue.Dequeue();
if (item is ScrollViewer)
return (ScrollViewer) item;
for (var i = 0; i < VisualTreeHelper.GetChildrenCount(item); i++)
queue.Enqueue(VisualTreeHelper.GetChild(item, i));
} while (queue.Count > 0);
return null;
}
然后将此附加到 ListBox
Loaded
事件:
<ListBox Loaded="ListBox_OnLoaded" />
这可以很容易地修改为附加属性,使其更具通用性。
或者yarik的建议:
<ListBox ScrollViewer.ScrollChanged="ScrollViewer_OnScrollChanged" />
并且在代码后台:
private void ScrollViewer_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (e.OriginalSource is ScrollViewer scrollViewer &&
Math.Abs(e.ExtentHeightChange) > 0.0)
{
scrollViewer.ScrollToBottom();
}
}
<ListBox ScrollViewer.ScrollChanged="..." />
。 - YarikListBox
有自定义模板,它可能没有 ScrollViewer
。 - Scroog1ScrollViewer
,那么就没有可滚动的内容,此事件也不会被触发。 - YarikScrollViewer
属性将不可用。但是,使用这种方法仍然存在一个缺点,即必须为每个ListBox
实现单独的事件处理程序(或者至少为包含列表框的每个控件实现一个处理程序)。而附加属性只需要一个实现。很遗憾您无法调用静态方法事件处理程序。 - Scroog1listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
的意思是让listBox自动滚动到最后一个选项。这里的所有答案都不能满足我的需求。因此,我编写了自己的行为来自动滚动项目控件,并在用户向上滚动时暂停自动滚动,并在用户向底部滚动时恢复自动滚动。
/// <summary>
/// This will auto scroll a list view to the bottom as items are added.
/// Automatically suspends if the user scrolls up, and recommences when
/// the user scrolls to the end.
/// </summary>
/// <example>
/// <ListView sf:AutoScrollToBottomBehavior="{Binding viewModelAutoScrollFlag}" />
/// </example>
public class AutoScrollToBottomBehavior
{
/// <summary>
/// Enumerated type to keep track of the current auto scroll status
/// </summary>
public enum StatusType
{
NotAutoScrollingToBottom,
AutoScrollingToBottom,
AutoScrollingToBottomButSuppressed
}
public static StatusType GetAutoScrollToBottomStatus(DependencyObject obj)
{
return (StatusType)obj.GetValue(AutoScrollToBottomStatusProperty);
}
public static void SetAutoScrollToBottomStatus(DependencyObject obj, StatusType value)
{
obj.SetValue(AutoScrollToBottomStatusProperty, value);
}
// Using a DependencyProperty as the backing store for AutoScrollToBottomStatus. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoScrollToBottomStatusProperty =
DependencyProperty.RegisterAttached(
"AutoScrollToBottomStatus",
typeof(StatusType),
typeof(AutoScrollToBottomBehavior),
new PropertyMetadata(StatusType.NotAutoScrollingToBottom, (s, e) =>
{
if (s is DependencyObject viewer && e.NewValue is StatusType autoScrollToBottomStatus)
{
// Set the AutoScrollToBottom property to mirror this one
bool? autoScrollToBottom = autoScrollToBottomStatus switch
{
StatusType.AutoScrollingToBottom => true,
StatusType.NotAutoScrollingToBottom => false,
StatusType.AutoScrollingToBottomButSuppressed => false,
_ => null
};
if (autoScrollToBottom.HasValue)
{
SetAutoScrollToBottom(viewer, autoScrollToBottom.Value);
}
// Only hook/unhook for cases below, not when suspended
switch(autoScrollToBottomStatus)
{
case StatusType.AutoScrollingToBottom:
HookViewer(viewer);
break;
case StatusType.NotAutoScrollingToBottom:
UnhookViewer(viewer);
break;
}
}
}));
public static bool GetAutoScrollToBottom(DependencyObject obj)
{
return (bool)obj.GetValue(AutoScrollToBottomProperty);
}
public static void SetAutoScrollToBottom(DependencyObject obj, bool value)
{
obj.SetValue(AutoScrollToBottomProperty, value);
}
// Using a DependencyProperty as the backing store for AutoScrollToBottom. This enables animation, styling, binding, etc...
public static readonly DependencyProperty AutoScrollToBottomProperty =
DependencyProperty.RegisterAttached(
"AutoScrollToBottom",
typeof(bool),
typeof(AutoScrollToBottomBehavior),
new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, (s, e) =>
{
if (s is DependencyObject viewer && e.NewValue is bool autoScrollToBottom)
{
// Set the AutoScrollToBottomStatus property to mirror this one
if (autoScrollToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
}
else if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.NotAutoScrollingToBottom);
}
// No change if autoScrollToBottom = false && viewer.AutoScrollToBottomStatus = AutoScrollToBottomStatusType.AutoScrollingToBottomButSuppressed;
}
}));
private static Action GetUnhookAction(DependencyObject obj)
{
return (Action)obj.GetValue(UnhookActionProperty);
}
private static void SetUnhookAction(DependencyObject obj, Action value)
{
obj.SetValue(UnhookActionProperty, value);
}
// Using a DependencyProperty as the backing store for MyProperty. This enables animation, styling, binding, etc...
private static readonly DependencyProperty UnhookActionProperty =
DependencyProperty.RegisterAttached("UnhookAction", typeof(Action), typeof(AutoScrollToBottomBehavior), new PropertyMetadata(null));
private static void ItemsControl_Loaded(object sender, RoutedEventArgs e)
{
if (sender is ItemsControl itemsControl)
{
itemsControl.Loaded -= ItemsControl_Loaded;
HookViewer(itemsControl);
}
}
private static void HookViewer(DependencyObject viewer)
{
if (viewer is ItemsControl itemsControl)
{
// If this is triggered the xaml setup then the control won't be loaded yet,
// and so won't have a visual tree which we need to get the scrollviewer,
// so defer this hooking until the items control is loaded.
if (!itemsControl.IsLoaded)
{
itemsControl.Loaded += ItemsControl_Loaded;
return;
}
if (FindScrollViewer(viewer) is ScrollViewer scrollViewer)
{
scrollViewer.ScrollToBottom();
// Scroll to bottom when the item count changes
NotifyCollectionChangedEventHandler itemsCollectionChangedHandler = (s, e) =>
{
if (GetAutoScrollToBottom(viewer))
{
scrollViewer.ScrollToBottom();
}
};
((INotifyCollectionChanged)itemsControl.Items).CollectionChanged += itemsCollectionChangedHandler;
ScrollChangedEventHandler scrollChangedEventHandler = (s, e) =>
{
bool userScrolledToBottom = (e.VerticalOffset + e.ViewportHeight) > (e.ExtentHeight - 1.0);
bool userScrolledUp = e.VerticalChange < 0;
// Check if auto scrolling should be suppressed
if (userScrolledUp && !userScrolledToBottom)
{
if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottom)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottomButSuppressed);
}
}
// Check if auto scrolling should be unsuppressed
if (userScrolledToBottom)
{
if (GetAutoScrollToBottomStatus(viewer) == StatusType.AutoScrollingToBottomButSuppressed)
{
SetAutoScrollToBottomStatus(viewer, StatusType.AutoScrollingToBottom);
}
}
};
scrollViewer.ScrollChanged += scrollChangedEventHandler;
Action unhookAction = () =>
{
((INotifyCollectionChanged)itemsControl.Items).CollectionChanged -= itemsCollectionChangedHandler;
scrollViewer.ScrollChanged -= scrollChangedEventHandler;
};
SetUnhookAction(viewer, unhookAction);
}
}
}
/// <summary>
/// Unsubscribes the event listeners on the ItemsControl and ScrollViewer
/// </summary>
/// <param name="viewer"></param>
private static void UnhookViewer(DependencyObject viewer)
{
var unhookAction = GetUnhookAction(viewer);
SetUnhookAction(viewer, null);
unhookAction?.Invoke();
}
/// <summary>
/// A recursive function that drills down a visual tree until a ScrollViewer is found.
/// </summary>
/// <param name="viewer"></param>
/// <returns></returns>
private static ScrollViewer FindScrollViewer(DependencyObject viewer)
{
if (viewer is ScrollViewer scrollViewer)
return scrollViewer;
return Enumerable.Range(0, VisualTreeHelper.GetChildrenCount(viewer))
.Select(i => FindScrollViewer(VisualTreeHelper.GetChild(viewer, i)))
.Where(child => child != null)
.FirstOrDefault();
}
}
AutoScrollToBottomBehavior.AutoScrollToBottomStatus="AutoScrollingToBottom"
。 - MHolzmayrusing System.Collections.Specialized;
using System.Windows.Controls;
using System.Windows.Media;
namespace YourProgram.CustomControls
{
public class AutoScrollListBox : ListBox
{
public AutoScrollListBox()
{
if (Items != null)
{
// Hook to the CollectionChanged event of your ObservableCollection
((INotifyCollectionChanged)Items).CollectionChanged += CollectionChange;
}
}
// Is called whenever the item collection changes
private void CollectionChange(object sender, NotifyCollectionChangedEventArgs e)
{
if (Items.Count > 0)
{
// Get the ScrollViewer object from the ListBox control
Border border = (Border)VisualTreeHelper.GetChild(this, 0);
ScrollViewer SV = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
// Scroll to bottom
SV.ScrollToBottom();
}
}
}
}
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:YourProgram"
xmlns:cc="clr-namespace:YourProgram.CustomControls"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<cc:AutoScrollListBox ItemsSource="{Binding YourObservableCollection}"/>
</Window>
private void WriteMessage(string message, Brush color, ListView lv)
{
Dispatcher.BeginInvoke(new Action(delegate
{
ListViewItem ls = new ListViewItem
{
Foreground = color,
Content = message
};
lv.Items.Add(ls);
lv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
}));
}
myLv.Items.Add(ls);
myLv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
只有在最后一个添加的项目是列表中的最后一个时,才滚动到底部。
if (lstBox.SelectedIndex == lstBox.Items.Count - 1)
{
// Scroll to bottom
lstBox.ScrollIntoView(lstBox.SelectedItem);
}
InitializeComponent();
之后,您需要添加((INotifyCollectionChanged).Items).CollectionChanged += YourListboxCollectionChanged;
。请注意,您需要保持原文意思不变,并使翻译更加通俗易懂。 - MarkusEgleListBoxChrome
。将类型转换从Border
改为FrameworkElement
,现在它完美地工作了,谢谢! - AlfieBorder border = (Border)...
必须更改为FrameworkElement border = (FrameworkElement)...
。 - Mike Nakis