wpf 添加新项目时,如何让 ListBox 自动滚动?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/2006729/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How can I have a ListBox auto-scroll when a new item is added?
提问by Rob Buhler
I have a WPF ListBox that is set to scroll horizontally. The ItemsSource is bound to an ObservableCollection in my ViewModel class. Every time a new item is added, I want the ListBox to scroll to the right so that the new item is viewable.
我有一个设置为水平滚动的 WPF ListBox。ItemsSource 绑定到我的 ViewModel 类中的 ObservableCollection。每次添加新项目时,我都希望 ListBox 向右滚动,以便可以查看新项目。
The ListBox is defined in a DataTemplate, so I am unable to access the ListBox by name in my code behind file.
ListBox 是在 DataTemplate 中定义的,因此我无法在我的代码隐藏文件中按名称访问 ListBox。
How can I get a ListBox to always scroll to show a latest added item?
如何让 ListBox 始终滚动以显示最新添加的项目?
I would like a way to know when the ListBox has a new item added to it, but I do not see an event that does this.
我想要一种方法来知道 ListBox 何时添加了新项目,但我没有看到执行此操作的事件。
回答by Aviad P.
You can extend the behavior of the ListBox by using attached properties. In your case I would define an attached property called ScrollOnNewItem
that when set to true
hooks into the INotifyCollectionChanged
events of the list box items source and upon detecting a new item, scrolls the list box to it.
您可以使用附加属性扩展 ListBox 的行为。在您的情况下,我将定义一个附加属性ScrollOnNewItem
,当设置为true
挂钩到INotifyCollectionChanged
列表框项目源的事件时,并在检测到新项目时,将列表框滚动到它。
Example:
例子:
class ListBoxBehavior
{
static readonly Dictionary<ListBox, Capture> Associations =
new Dictionary<ListBox, Capture>();
public static bool GetScrollOnNewItem(DependencyObject obj)
{
return (bool)obj.GetValue(ScrollOnNewItemProperty);
}
public static void SetScrollOnNewItem(DependencyObject obj, bool value)
{
obj.SetValue(ScrollOnNewItemProperty, value);
}
public static readonly DependencyProperty ScrollOnNewItemProperty =
DependencyProperty.RegisterAttached(
"ScrollOnNewItem",
typeof(bool),
typeof(ListBoxBehavior),
new UIPropertyMetadata(false, OnScrollOnNewItemChanged));
public static void OnScrollOnNewItemChanged(
DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var listBox = d as ListBox;
if (listBox == null) return;
bool oldValue = (bool)e.OldValue, newValue = (bool)e.NewValue;
if (newValue == oldValue) return;
if (newValue)
{
listBox.Loaded += ListBox_Loaded;
listBox.Unloaded += ListBox_Unloaded;
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.AddValueChanged(listBox, ListBox_ItemsSourceChanged);
}
else
{
listBox.Loaded -= ListBox_Loaded;
listBox.Unloaded -= ListBox_Unloaded;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
var itemsSourcePropertyDescriptor = TypeDescriptor.GetProperties(listBox)["ItemsSource"];
itemsSourcePropertyDescriptor.RemoveValueChanged(listBox, ListBox_ItemsSourceChanged);
}
}
private static void ListBox_ItemsSourceChanged(object sender, EventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
Associations[listBox] = new Capture(listBox);
}
static void ListBox_Unloaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
if (Associations.ContainsKey(listBox))
Associations[listBox].Dispose();
listBox.Unloaded -= ListBox_Unloaded;
}
static void ListBox_Loaded(object sender, RoutedEventArgs e)
{
var listBox = (ListBox)sender;
var incc = listBox.Items as INotifyCollectionChanged;
if (incc == null) return;
listBox.Loaded -= ListBox_Loaded;
Associations[listBox] = new Capture(listBox);
}
class Capture : IDisposable
{
private readonly ListBox listBox;
private readonly INotifyCollectionChanged incc;
public Capture(ListBox listBox)
{
this.listBox = listBox;
incc = listBox.ItemsSource as INotifyCollectionChanged;
if (incc != null)
{
incc.CollectionChanged += incc_CollectionChanged;
}
}
void incc_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
listBox.ScrollIntoView(e.NewItems[0]);
listBox.SelectedItem = e.NewItems[0];
}
}
public void Dispose()
{
if (incc != null)
incc.CollectionChanged -= incc_CollectionChanged;
}
}
}
Usage:
用法:
<ListBox ItemsSource="{Binding SourceCollection}"
lb:ListBoxBehavior.ScrollOnNewItem="true"/>
UPDATEAs per Andrej's suggestion in the comments below, I added hooks to detect a change in the ItemsSource
of the ListBox
.
UPDATE每安德烈在下面的意见建议作为,我加了钩子,以检测改变ItemsSource
的ListBox
。
回答by denis morozov
<ItemsControl ItemsSource="{Binding SourceCollection}">
<i:Interaction.Behaviors>
<Behaviors:ScrollOnNewItem/>
</i:Interaction.Behaviors>
</ItemsControl>
public class ScrollOnNewItem : Behavior<ItemsControl>
{
protected override void OnAttached()
{
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnLoaded;
}
protected override void OnDetaching()
{
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged += OnCollectionChanged;
}
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
var incc = AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (incc == null) return;
incc.CollectionChanged -= OnCollectionChanged;
}
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e.Action == NotifyCollectionChangedAction.Add)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
var frameworkElement = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
if (frameworkElement == null) return;
frameworkElement.BringIntoView();
}
}
回答by shawnpfiore
I found an really slick way to do this, simply update the listbox scrollViewer and set position to the bottom. Call this function in one of the ListBox Events like SelectionChanged for example.
我找到了一个非常巧妙的方法来做到这一点,只需更新列表框 scrollViewer 并将位置设置到底部。例如,在诸如 SelectionChanged 之类的 ListBox 事件之一中调用此函数。
private void UpdateScrollBar(ListBox listBox)
{
if (listBox != null)
{
var border = (Border)VisualTreeHelper.GetChild(listBox, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
回答by Shuo
I use this solution: http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/.
我使用这个解决方案:http: //michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/。
It works even if you bind listbox's ItemsSource to an ObservableCollection that is manipulated in a non-UI thread.
即使您将列表框的 ItemsSource 绑定到在非 UI 线程中操作的 ObservableCollection,它也能工作。
回答by Contango
MVVM-style Attached Behavior
MVVM 风格的附加行为
This Attached Behavior automatically scrolls the listbox to the bottom when a new item is added.
添加新项目时,此附加行为会自动将列表框滚动到底部。
<ListBox ItemsSource="{Binding LoggingStream}">
<i:Interaction.Behaviors>
<behaviors:ScrollOnNewItemBehavior
IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</ListBox>
In your ViewModel
, you can bind to boolean IfFollowTail { get; set; }
to control whether auto scrolling is active or not.
在您的 中ViewModel
,您可以绑定到 booleanIfFollowTail { get; set; }
以控制自动滚动是否处于活动状态。
The Behavior does all the right things:
行为做所有正确的事情:
- If
IfFollowTail=false
is set in the ViewModel, the ListBox no longer scrolls to the bottom on a new item. - As soon as
IfFollowTail=true
is set in the ViewModel, the ListBox instantly scrolls to the bottom, and continues to do so. - It's fast. It only scrolls after a couple of hundred milliseconds of inactivity. A naive implementation would be extremely slow, as it would scroll on every new item added.
- It works with duplicate ListBox items (a lot of other implementations do not work with duplicates - they scroll to the first item, then stop).
- It's ideal for a logging console that deals with continuous incoming items.
- 如果
IfFollowTail=false
在 ViewModel 中设置了 ,则 ListBox 不再滚动到新项目的底部。 - 一旦
IfFollowTail=true
在 ViewModel 中设置,ListBox 立即滚动到底部,并继续这样做。 - 它很快。它仅在不活动几百毫秒后滚动。天真的实现会非常慢,因为它会在添加的每个新项目上滚动。
- 它适用于重复的 ListBox 项(许多其他实现不适用于重复项 - 它们滚动到第一项,然后停止)。
- 它非常适合处理连续传入项目的日志记录控制台。
Behavior C# Code
行为 C# 代码
public class ScrollOnNewItemBehavior : Behavior<ListBox>
{
public static readonly DependencyProperty IsActiveScrollOnNewItemProperty = DependencyProperty.Register(
name: "IsActiveScrollOnNewItem",
propertyType: typeof(bool),
ownerType: typeof(ScrollOnNewItemBehavior),
typeMetadata: new PropertyMetadata(defaultValue: true, propertyChangedCallback:PropertyChangedCallback));
private static void PropertyChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
{
// Intent: immediately scroll to the bottom if our dependency property changes.
ScrollOnNewItemBehavior behavior = dependencyObject as ScrollOnNewItemBehavior;
if (behavior == null)
{
return;
}
behavior.IsActiveScrollOnNewItemMirror = (bool)dependencyPropertyChangedEventArgs.NewValue;
if (behavior.IsActiveScrollOnNewItemMirror == false)
{
return;
}
ListboxScrollToBottom(behavior.ListBox);
}
public bool IsActiveScrollOnNewItem
{
get { return (bool)this.GetValue(IsActiveScrollOnNewItemProperty); }
set { this.SetValue(IsActiveScrollOnNewItemProperty, value); }
}
public bool IsActiveScrollOnNewItemMirror { get; set; } = true;
protected override void OnAttached()
{
this.AssociatedObject.Loaded += this.OnLoaded;
this.AssociatedObject.Unloaded += this.OnUnLoaded;
}
protected override void OnDetaching()
{
this.AssociatedObject.Loaded -= this.OnLoaded;
this.AssociatedObject.Unloaded -= this.OnUnLoaded;
}
private IDisposable rxScrollIntoView;
private void OnLoaded(object sender, RoutedEventArgs e)
{
var changed = this.AssociatedObject.ItemsSource as INotifyCollectionChanged;
if (changed == null)
{
return;
}
// Intent: If we scroll into view on every single item added, it slows down to a crawl.
this.rxScrollIntoView = changed
.ToObservable()
.ObserveOn(new EventLoopScheduler(ts => new Thread(ts) { IsBackground = true}))
.Where(o => this.IsActiveScrollOnNewItemMirror == true)
.Where(o => o.NewItems?.Count > 0)
.Sample(TimeSpan.FromMilliseconds(180))
.Subscribe(o =>
{
this.Dispatcher.BeginInvoke((Action)(() =>
{
ListboxScrollToBottom(this.ListBox);
}));
});
}
ListBox ListBox => this.AssociatedObject;
private void OnUnLoaded(object sender, RoutedEventArgs e)
{
this.rxScrollIntoView?.Dispose();
}
/// <summary>
/// Scrolls to the bottom. Unlike other methods, this works even if there are duplicate items in the listbox.
/// </summary>
private static void ListboxScrollToBottom(ListBox listBox)
{
if (VisualTreeHelper.GetChildrenCount(listBox) > 0)
{
Border border = (Border)VisualTreeHelper.GetChild(listBox, 0);
ScrollViewer scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
}
Bridge from events to Reactive Extensions
从事件桥接反应式扩展
Finally, add this extension method so we can use all of the RX goodness:
最后,添加此扩展方法,以便我们可以使用所有 RX 优点:
public static class ListBoxEventToObservableExtensions
{
/// <summary>Converts CollectionChanged to an observable sequence.</summary>
public static IObservable<NotifyCollectionChangedEventArgs> ToObservable<T>(this T source)
where T : INotifyCollectionChanged
{
return Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>(
h => (sender, e) => h(e),
h => source.CollectionChanged += h,
h => source.CollectionChanged -= h);
}
}
Add Reactive Extensions
添加反应式扩展
You will need to add Reactive Extensions
to your project. I recommend NuGet
.
您将需要添加Reactive Extensions
到您的项目中。我推荐NuGet
。
回答by Jozef Kemenik
solution for Datagrid (the same for ListBox, only substitute DataGrid with ListBox class)
Datagrid的解决方案(ListBox也一样,只用ListBox类代替DataGrid)
private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
int count = AssociatedObject.Items.Count;
if (count == 0)
return;
var item = AssociatedObject.Items[count - 1];
if (AssociatedObject is DataGrid)
{
DataGrid grid = (AssociatedObject as DataGrid);
grid.Dispatcher.BeginInvoke((Action)(() =>
{
grid.UpdateLayout();
grid.ScrollIntoView(item, null);
}));
}
}
}
回答by mickeymicks
The most straight-forward way i've found to do this, especially for listbox (or listview) that is bound to a data source is to hook it up with the collection change event. You can do this very easily at DataContextChanged event of the listbox:
我发现最直接的方法是将它与集合更改事件挂钩,尤其是对于绑定到数据源的列表框(或列表视图)。您可以在列表框的 DataContextChanged 事件中轻松完成此操作:
//in xaml <ListView x:Name="LogView" DataContextChanged="LogView_DataContextChanged">
private void LogView_DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
{
var src = LogView.Items.SourceCollection as INotifyCollectionChanged;
src.CollectionChanged += (obj, args) => { LogView.Items.MoveCurrentToLast(); LogView.ScrollIntoView(LogView.Items.CurrentItem); };
}
This is actually just a combination of all the other answers i've found. I feel that this is such a trivial feature that we should not need to spend so much time (and lines of code) doing.
这实际上只是我找到的所有其他答案的组合。我觉得这是一个微不足道的功能,我们不应该花那么多时间(和代码行)来做。
If only there was an Autoscroll = true property. Sigh.
如果只有 Autoscroll = true 属性。叹。
回答by Nikita B
I was not happy with proposed solutions.
我对提出的解决方案不满意。
- I didn't want to use "leaky" property descriptors.
- I didn't want to add Rx dependency and 8-line query for seemingly trivial task. Neither did I want a constantly running timer.
- I did like shawnpfiore's idea though, so I've built an attached behavior on top of it, which so far works well in my case.
- 我不想使用“泄漏”的属性描述符。
- 我不想为看似微不足道的任务添加 Rx 依赖项和 8 行查询。我也不想要一个持续运行的计时器。
- 不过我确实喜欢 shawnpfiore 的想法,所以我在它的基础上建立了一个附加的行为,到目前为止在我的情况下效果很好。
Here is what I ended up with. Maybe it will save somebody some time.
这就是我的结果。也许它会节省一些时间。
public class AutoScroll : Behavior<ItemsControl>
{
public static readonly DependencyProperty ModeProperty = DependencyProperty.Register(
"Mode", typeof(AutoScrollMode), typeof(AutoScroll), new PropertyMetadata(AutoScrollMode.VerticalWhenInactive));
public AutoScrollMode Mode
{
get => (AutoScrollMode) GetValue(ModeProperty);
set => SetValue(ModeProperty, value);
}
protected override void OnAttached()
{
base.OnAttached();
AssociatedObject.Loaded += OnLoaded;
AssociatedObject.Unloaded += OnUnloaded;
}
protected override void OnDetaching()
{
Clear();
AssociatedObject.Loaded -= OnLoaded;
AssociatedObject.Unloaded -= OnUnloaded;
base.OnDetaching();
}
private static readonly DependencyProperty ItemsCountProperty = DependencyProperty.Register(
"ItemsCount", typeof(int), typeof(AutoScroll), new PropertyMetadata(0, (s, e) => ((AutoScroll)s).OnCountChanged()));
private ScrollViewer _scroll;
private void OnLoaded(object sender, RoutedEventArgs e)
{
var binding = new Binding("ItemsSource.Count")
{
Source = AssociatedObject,
Mode = BindingMode.OneWay
};
BindingOperations.SetBinding(this, ItemsCountProperty, binding);
_scroll = AssociatedObject.FindVisualChild<ScrollViewer>() ?? throw new NotSupportedException("ScrollViewer was not found!");
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
Clear();
}
private void Clear()
{
BindingOperations.ClearBinding(this, ItemsCountProperty);
}
private void OnCountChanged()
{
var mode = Mode;
if (mode == AutoScrollMode.Vertical)
{
_scroll.ScrollToBottom();
}
else if (mode == AutoScrollMode.Horizontal)
{
_scroll.ScrollToRightEnd();
}
else if (mode == AutoScrollMode.VerticalWhenInactive)
{
if (_scroll.IsKeyboardFocusWithin) return;
_scroll.ScrollToBottom();
}
else if (mode == AutoScrollMode.HorizontalWhenInactive)
{
if (_scroll.IsKeyboardFocusWithin) return;
_scroll.ScrollToRightEnd();
}
}
}
public enum AutoScrollMode
{
/// <summary>
/// No auto scroll
/// </summary>
Disabled,
/// <summary>
/// Automatically scrolls horizontally, but only if items control has no keyboard focus
/// </summary>
HorizontalWhenInactive,
/// <summary>
/// Automatically scrolls vertically, but only if itmes control has no keyboard focus
/// </summary>
VerticalWhenInactive,
/// <summary>
/// Automatically scrolls horizontally regardless of where the focus is
/// </summary>
Horizontal,
/// <summary>
/// Automatically scrolls vertically regardless of where the focus is
/// </summary>
Vertical
}
回答by Birek
So what i read in this topcs is a little bit complex for a simple action.
所以我在这个 topcs 中读到的内容对于一个简单的操作来说有点复杂。
So I subscribed to scrollchanged event and then I used this code:
所以我订阅了 scrollchanged 事件,然后我使用了这个代码:
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = ((ScrollViewer)e.OriginalSource);
scrollViewer.ScrollToEnd();
}
Bonus:
奖金:
After it I made a checkbox where I could set when I want use the autoscroll function and I relaized I forgot some times uncheck the listbox if I saw some interesting information for me. So I decided I would like to create a intelligent autoscrolled listbox what react to my mouse action.
之后,我创建了一个复选框,我可以在其中设置何时需要使用自动滚动功能,然后我忘记了如果我看到一些有趣的信息,有时会取消选中列表框。所以我决定创建一个智能的自动滚动列表框来响应我的鼠标操作。
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = ((ScrollViewer)e.OriginalSource);
scrollViewer.ScrollToEnd();
if (AutoScrollCheckBox.IsChecked != null && (bool)AutoScrollCheckBox.IsChecked)
scrollViewer.ScrollToEnd();
if (_isDownMouseMovement)
{
var verticalOffsetValue = scrollViewer.VerticalOffset;
var maxVerticalOffsetValue = scrollViewer.ExtentHeight - scrollViewer.ViewportHeight;
if (maxVerticalOffsetValue < 0 || verticalOffsetValue == maxVerticalOffsetValue)
{
// Scrolled to bottom
AutoScrollCheckBox.IsChecked = true;
_isDownMouseMovement = false;
}
else if (verticalOffsetValue == 0)
{
}
}
}
private bool _isDownMouseMovement = false;
private void TelnetListBox_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta > 0)
{
_isDownMouseMovement = false;
AutoScrollCheckBox.IsChecked = false;
}
if (e.Delta < 0)
{
_isDownMouseMovement = true;
}
}
When I scolled to botton the checkbox checked true and stay my view on bottom if I scroulled up with mouse wheel the checkox will be unchecked and you can explorer you listbox.
当我滚动到 botton 时,复选框被选中,如果我用鼠标滚轮向上滚动,则将我的视图保持在底部,复选框将被取消选中,您可以浏览您的列表框。
回答by Madison Courto
This is the solution I use that works, might help someone else;
这是我使用的有效解决方案,可能会帮助其他人;
statusWindow.SelectedIndex = statusWindow.Items.Count - 1;
statusWindow.UpdateLayout();
statusWindow.ScrollIntoView(statusWindow.SelectedItem);
statusWindow.UpdateLayout();