【发布时间】:2025-12-19 09:00:12
【问题描述】:
在我的应用程序中,我有一个带有项目的ListBox。该应用程序是用 WPF 编写的。
如何自动滚动到最后添加的项目?我希望在添加新项目后将 ScrollViewer 移到列表末尾。
有像ItemsChanged这样的活动吗?
(我不想使用SelectionChanged 事件)
【问题讨论】:
在我的应用程序中,我有一个带有项目的ListBox。该应用程序是用 WPF 编写的。
如何自动滚动到最后添加的项目?我希望在添加新项目后将 ScrollViewer 移到列表末尾。
有像ItemsChanged这样的活动吗?
(我不想使用SelectionChanged 事件)
【问题讨论】:
试试这个:
lstBox.SelectedIndex = lstBox.Items.Count -1;
lstBox.ScrollIntoView(lstBox.SelectedItem) ;
在您的 MainWindow 中,这将选择并关注列表中的最后一项!
【讨论】:
lstBox.SelectedIndex = 0 ;)
struct 或record(它实现了一个比较值而不是引用的比较器)。另外,这个问题已经回答了一半:你打算在什么情况下做?
最简单的方法:
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 使用自定义模板,这可能不起作用,所以要小心。
InitializeComponent(); 之后,您必须添加 ((INotifyCollectionChanged).Items).CollectionChanged += YourListboxCollectionChanged;
ListBoxChrome。将演员阵容从 Border 更改为 FrameworkElement 并且效果很好,谢谢!
Border border = (Border)... 必须改为FrameworkElement border = (FrameworkElement)...。
请记住,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="..." />。
ListBox 有自定义模板,它可能没有ScrollViewer。
ScrollViewer,则没有可滚动的内容,事件根本不会引发。
ScrollViewer 属性将不可用。但是,您仍然需要为每个ListBox(或每个包含列表框的控件至少一个处理程序)实现一个单独的事件处理程序的缺点。而附加属性只需要一个实现。很遗憾你不能调用静态方法事件处理程序。
listBox.ScrollIntoView(listBox.Items[listBox.Items.Count - 1]);
【讨论】:
这里的答案都没有满足我的需要。因此,我编写了自己的行为,即自动滚动项目控件,并在用户向上滚动时暂停自动滚动,并在用户向下滚动到底部时恢复自动滚动。
/// <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"
对我来说,最简单的工作方式是这样的:(没有绑定)
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]);
例如,不要为我工作。
【讨论】:
您可以尝试ListBox.ScrollIntoView() 方法,尽管在某些情况下有一些problems...
这是 Tamir Khason 的一个例子:Auto scroll ListBox in WPF
【讨论】:
实现自动滚动最简单的方法是挂钩 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>
【讨论】:
这是对我 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 解决方案,但我做了一些修复和简化。
【讨论】:
使用 .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();
}
注意:您无法在构造函数中获取滚动查看器,因为组件未初始化。
【讨论】: