WPF ListBox 自动滚动到结束
Posted
技术标签:
【中文标题】WPF ListBox 自动滚动到结束【英文标题】:WPF ListBox Scroll to end automatically 【发布时间】:2011-01-21 05:21:40 【问题描述】:在我的应用程序中,我有一个带有项目的ListBox
。该应用程序是用 WPF 编写的。
如何自动滚动到最后添加的项目?我希望在添加新项目后将 ScrollViewer
移到列表末尾。
有像ItemsChanged
这样的活动吗?
(我不想使用SelectionChanged
事件)
【问题讨论】:
【参考方案1】:试试这个:
lstBox.SelectedIndex = lstBox.Items.Count -1;
lstBox.ScrollIntoView(lstBox.SelectedItem) ;
在您的 MainWindow 中,这将选择并关注列表中的最后一项!
【讨论】:
这只是一个有效选项,如果添加的最后一项是列表中的最后一项。但最后添加的项目可能会添加到位置 0。 这个答案应该被接受! @0xBADF00D 如果是这种情况,你应该这样做lstBox.SelectedIndex = 0
;)
不适用于原始值struct
或record
(它实现了一个比较值而不是引用的比较器)。另外,这个问题已经回答了一半:你打算在什么情况下做?【参考方案2】:
最简单的方法:
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
事件中,您将拥有全自动的自动滚动行为。
【讨论】:
其他解决方案根本不适合我。代码已执行(在调试中证明),但它对控件的状态没有影响。这是第一次完美地工作。 如果您为ListBox
使用自定义模板,这可能不起作用,所以要小心。
对于任何想知道如何将 CollectionChanged 附加到您的列表框的人:在 InitializeComponent();
之后,您必须添加 ((INotifyCollectionChanged).Items).CollectionChanged += YourListboxCollectionChanged;
第一个孩子对我来说是ListBoxChrome
。将演员阵容从 Border
更改为 FrameworkElement
并且效果很好,谢谢!
我确认@Alfie 上面写的。所以,Border border = (Border)...
必须改为FrameworkElement border = (FrameworkElement)...
。【参考方案3】:
请记住,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);
【讨论】:
谢谢。对我来说完美无缺。我认为您应该将 chatMessages 删除为 myListBox 之类的内容。 太好了,谢谢。仅供参考:必须将这些引用添加到您的项目中:UIAutomationProvider 和 UIAutomationTypes【参考方案4】:最好的解决方案是使用 ListBox 控件内的 ItemCollection 对象 这个系列是专门为内容观众设计的。它有一个预定义的方法来选择最后一项并保持光标位置参考......
myListBox.Items.MoveCurrentToLast();
myListBox.ScrollIntoView(myListBox.Items.CurrentItem);
【讨论】:
是的,同意@Givanio,设置 SelectedItem 后,我的鼠标光标将不再在列表视图中工作。谢谢!【参考方案5】:与目前介绍的方法略有不同。
您可以使用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();
【讨论】:
这是一个不错的工作解决方案,但由于 WPF 路由事件正在元素树中冒泡,因此大部分代码都不是必需的:<ListBox ScrollViewer.ScrollChanged="..." />
。
你必须小心一点,因为如果ListBox
有自定义模板,它可能没有ScrollViewer
。
如果它没有ScrollViewer
,则没有可滚动的内容,事件根本不会引发。
我的错。我假设如果模板更改,ScrollViewer
属性将不可用。但是,您仍然需要为每个ListBox
(或每个包含列表框的控件至少一个处理程序)实现一个单独的事件处理程序的缺点。而附加属性只需要一个实现。很遗憾你不能调用静态方法事件处理程序。【参考方案6】:
listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
【讨论】:
【参考方案7】:这里的答案都没有满足我的需要。因此,我编写了自己的行为,即自动滚动项目控件,并在用户向上滚动时暂停自动滚动,并在用户向下滚动到底部时恢复自动滚动。
/// <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();
【讨论】:
很好,正是我需要的。必须进行一些调整:FindScrollViewer 现在也在树上搜索,(我的 ItemsControl 被包裹在 ScrollViewer 中); switch-assignment 到 switch-case(仍然在 .net 4.6 上);和用法AutoScrollToBottomBehavior.AutoScrollToBottomStatus="AutoScrollingToBottom"
【参考方案8】:
对我来说,最简单的工作方式是这样的:(没有绑定)
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]);
));
不需要创建类或更改xaml,只需使用此方法编写消息并自动滚动。
只是调用
myLv.Items.Add(ls);
myLv.ScrollIntoView(lv.Items[lv.Items.Count - 1]);
例如,不要为我工作。
【讨论】:
【参考方案9】:您可以尝试ListBox.ScrollIntoView() 方法,尽管在某些情况下有一些problems...
这是 Tamir Khason 的一个例子:Auto scroll ListBox in WPF
【讨论】:
这里的三个链接中有两个已经失效(它们是唯一有可能为问题添加有用信息的两个)【参考方案10】:实现自动滚动最简单的方法是挂钩 CollectionChanged 事件。只需将该功能添加到派生自 ListBox 控件的自定义类:
using 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();
将自定义控件的命名空间添加到您的 WPF 窗口并使用自定义 ListBox 控件:
<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>
【讨论】:
【参考方案11】:这是对我 100% 有效的方法。
初始化部分:
private ObservableCollection<ActionLogData> LogListBind = new ObservableCollection<ActionLogData>();
LogList.ItemsSource = LogListBind;
LogListBind.CollectionChanged += this.OnCollectionChanged;
绑定到我的 ObservableCollection 的 CollectionChanged 的委托,用作我的 ListView 的项目源:
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
if (VisualTreeHelper.GetChildrenCount(LogList) > 0)
Decorator border = VisualTreeHelper.GetChild(LogList, 0) as Decorator;
ScrollViewer scrollViewer = border.Child as ScrollViewer;
scrollViewer.ScrollToBottom();
此解决方案基于 @mateusz-myślak 解决方案,但我做了一些修复和简化。
【讨论】:
【参考方案12】:使用 .NET 5,来自 this answer 和每个人的答案的组合,我想出的最干净的方法是:
在 View 的构造函数中订阅事件(代码隐藏):
var listViewItemsSource = (INotifyCollectionChanged)MyListView.Items.SourceCollection;
listViewItemsSource.CollectionChanged += MyListViewCollectionChanged;
在MyListViewCollectionChanged
委托中,您获取ScrollViewer
并滚动到末尾:
private void MyListViewCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
var border = (Decorator)VisualTreeHelper.GetChild(LoggerListView, 0);
var scrollViewer = (ScrollViewer)border.Child;
scrollViewer.ScrollToEnd();
注意:您无法在构造函数中获取滚动查看器,因为组件未初始化。
【讨论】:
以上是关于WPF ListBox 自动滚动到结束的主要内容,如果未能解决你的问题,请参考以下文章