【问题标题】:WPF ListBox Scroll to end automaticallyWPF ListBox 自动滚动到结束
【发布时间】:2025-12-19 09:00:12
【问题描述】:

在我的应用程序中,我有一个带有项目的ListBox。该应用程序是用 WPF 编写的。

如何自动滚动到最后添加的项目?我希望在添加新项目后将 ScrollViewer 移到列表末尾。

有像ItemsChanged这样的活动吗? (我不想使用SelectionChanged 事件)

【问题讨论】:

    标签: wpf listbox scroll


    【解决方案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();
            }
            

            注意:您无法在构造函数中获取滚动查看器,因为组件未初始化。

            【讨论】: