我有一个WPF ListBox,设置为水平滚动。 ItemsSource绑定到我的ViewModel类中的ObservableCollection。每次添加新项目时,我都希望ListBox向右滚动,以便新项目可见。
ListBox是在DataTemplate中定义的,因此我无法在我的代码隐藏文件中按名称访问ListBox。
如何让ListBox始终滚动以显示最新添加的项目?
我想知道ListBox何时添加了一个新项目,但我没有看到这样做的事件。
答案 0 :(得分:64)
您可以使用附加属性扩展ListBox的行为。在您的情况下,我将定义一个名为ScrollOnNewItem
的附加属性,当设置为true
挂钩到列表框项目源的INotifyCollectionChanged
事件时,并在检测到新项目时,滚动列表框它。
示例:
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;
}
}
}
用法:
<ListBox ItemsSource="{Binding SourceCollection}"
lb:ListBoxBehavior.ScrollOnNewItem="true"/>
更新根据Andrej在下面评论中的建议,我添加了一些钩子来检测ItemsSource
ListBox
的变化。
答案 1 :(得分:20)
<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();
}
}
答案 2 :(得分:19)
我找到了一个非常灵活的方法,只需更新列表框scrollViewer并将位置设置到底部。例如,在其中一个ListBox事件中调用此函数,例如SelectionChanged。
private void UpdateScrollBar(ListBox listBox)
{
if (listBox != null)
{
var border = (Border)VisualTreeHelper.GetChild(listBox, 0);
var scrollViewer = (ScrollViewer)VisualTreeHelper.GetChild(border, 0);
scrollViewer.ScrollToBottom();
}
}
答案 3 :(得分:9)
我使用此解决方案:http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/。
即使将listbox的ItemsSource绑定到在非UI线程中操作的ObservableCollection,它也能正常工作。
答案 4 :(得分:2)
Datagrid的解决方案(对于ListBox是相同的,只用DataBox替换ListBox类)
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);
}));
}
}
}
答案 5 :(得分:1)
添加新项目时,此附加行为会自动将列表框滚动到底部。
<ListBox ItemsSource="{Binding LoggingStream}">
<i:Interaction.Behaviors>
<behaviors:ScrollOnNewItemBehavior
IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</i:Interaction.Behaviors>
</ListBox>
在ViewModel
中,您可以绑定到布尔IfFollowTail { get; set; }
来控制自动滚动是否处于活动状态。
行为做了所有正确的事情:
IfFollowTail=false
,则ListBox不再滚动到新项目的底部。IfFollowTail=true
,ListBox就会立即滚动到底部,并继续这样做。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();
}
}
}
最后,添加此扩展方法,以便我们可以使用所有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);
}
}
您需要将Reactive Extensions
添加到项目中。我推荐NuGet
。
答案 6 :(得分:1)
我发现这样做的最直接的方式,特别是对于绑定到数据源的listbox(或listview),是将它与集合更改事件挂钩。 您可以在列表框的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); };
}
这实际上只是我找到的所有其他答案的组合。 我觉得这是一个微不足道的功能,我们不需要花费这么多时间(和代码行)。
如果只有Autoscroll = true属性。叹。
答案 7 :(得分:0)
我发现了一种更简单的方法,它帮助我解决了类似的问题,只需要几行代码,无需创建自定义行为。检查我对这个问题的回答(并点击其中的链接):
wpf(C#) DataGrid ScrollIntoView - how to scroll to the first row that is not shown?
适用于ListBox,ListView和DataGrid。
答案 8 :(得分:0)
我对提议的解决方案不满意。
这是我最终的结果。也许它会节省一些时间。
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
}
答案 9 :(得分:0)
所以我在本Topcs中读到的内容对于一个简单的操作来说有点复杂。
所以我订阅了scrollchanged事件,然后使用此代码:
private void TelnetListBox_OnScrollChanged(object sender, ScrollChangedEventArgs e)
{
var scrollViewer = ((ScrollViewer)e.OriginalSource);
scrollViewer.ScrollToEnd();
}
奖金:
在此之后,我做了一个复选框,可以在需要使用自动滚动功能时进行设置,并且我相对地说,如果我发现了一些有趣的信息,则忘记了取消选中列表框的时间。因此,我决定要创建一个智能的自动滚动列表框,以对鼠标操作做出反应。
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;
}
}
当我屈服于botton时,该复选框被选中为true,并且如果我用鼠标滚轮选中该复选框,则我的视图将一直停留在底部,则checkox未被选中,您可以浏览列表框。
答案 10 :(得分:0)
这是我使用的可行的解决方案,可能会帮助其他人;
statusWindow.SelectedIndex = statusWindow.Items.Count - 1;
statusWindow.UpdateLayout();
statusWindow.ScrollIntoView(statusWindow.SelectedItem);
statusWindow.UpdateLayout();
答案 11 :(得分:0)
这对我有用:
DirectoryInfo di = new DirectoryInfo(folderBrowserDialog1.SelectedPath);
foreach (var fi in di.GetFiles("*", SearchOption.AllDirectories))
{
int count = Convert.ToInt32(listBox1.Items.Count); // counts every listbox entry
listBox1.Items.Add(count + " - " + fi.Name); // display entrys
listBox1.TopIndex = count; // scroll to the last entry
}