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 ;) 不适用于原始值structrecord(它实现了一个比较值而不是引用的比较器)。另外,这个问题已经回答了一半:你打算在什么情况下做?【参考方案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 路由事件正在元素树中冒泡,因此大部分代码都不是必需的:&lt;ListBox ScrollViewer.ScrollChanged="..." /&gt; 你必须小心一点,因为如果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 自动滚动到结束的主要内容,如果未能解决你的问题,请参考以下文章

WPF 禁用ItemControl 自动滚动

C# wpf怎么在grid添加自定义控件时显示滚动条

wpf 一行图片无缝的滚动

WPF 的Listbox 滚动处理

WPF 中的 ListBox、VirtualizingStackPanel 和平滑滚动

C# ListBox 自动滚动到底部 方法: