【问题标题】:How can I have a ListBox auto-scroll when a new item is added?添加新项目时如何让 ListBox 自动滚动?
【发布时间】:2010-01-05 14:49:00
【问题描述】:

我有一个设置为水平滚动的 WPF 列表框。 ItemsSource 绑定到我的 ViewModel 类中的 ObservableCollection。每次添加新项目时,我都希望 ListBox 向右滚动以便可以查看新项目。

ListBox 是在 DataTemplate 中定义的,因此我无法在我的代码隐藏文件中按名称访问 ListBox。

如何让 ListBox 始终滚动以显示最新添加的项目?

我想知道 ListBox 何时添加了新项目,但我没有看到执行此操作的事件。

【问题讨论】:

    标签: wpf listbox scroll


    【解决方案1】:

    您可以通过使用附加属性来扩展 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 在下面的 cmets 中的建议,我添加了钩子来检测 ListBoxItemsSource 的变化。

    【讨论】:

    • 谢谢,我将此代码添加到我的项目中,它按原样工作!我非常感谢快速准确的回复。我不太明白在线上发生了什么: var incc = listBox.Items as INotifyCollectionChanged;如何将 listBox 项强制转换为 INotifyCollectionChanged?在哪里可以了解有关创建附加属性的更多信息?
    • 更新:上面的代码对我来说大部分时间都有效 - 有时会添加列表框项目并且列表框不会滚动。
    • 我想我是想写 listBox.ItemsSource... 我会试试的。顺便说一句,它每次都对我有用,也许是焦点问题。选择更改是否始终有效?
    • +1 很棒的帖子,我在下面添加了我所做的,使用你的符号,就像一个不同的选项/包装..
    • 现在可以使用了!问题是我多次添加相同的字符串进行测试,但ScrollIntoView 方法和SelectedItem 属性只是获取第一个对象,所以它总是在顶部,当我添加不同的字符串时它会向下滚动。我通过添加时间戳来防止这种行为。毫秒到字符串:)
    【解决方案2】:
    <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();
            }
        }
    

    【讨论】:

    • 非常好,我根本不知道Behavior(Of T) 类!看起来更简洁易读。
    • BringIntoView() 似乎不起作用。在调试中,我可以看到代码正在执行,但 ListBox 没有滚动。我看到其他人有类似的问题:stackoverflow.com/questions/12430923/…
    • 另外,如果用户向上滚动,stackoverflow.com/questions/12255055/… 应该会停止滚动。我仍然无法让这种行为发挥作用。在这两种情况下,项目容器似乎都不存在。
    • 因为您使用的是列表框,您可能应该使用 listBox.ScrollIntoView()。我很确定这应该可行。
    • 我遇到了 UI 线程问题。我有一个后台任务更新绑定此列表框的集合。此行为在 int count = AssociatedObject... 处引发 InvalidOperation excption 使用 Dispatch.Invoke 绕过异常,但列表框不滚动
    【解决方案3】:

    我找到了一种非常巧妙的方法,只需更新列表框 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();
            }
    
        }
    

    【讨论】:

    • 很好的答案!我在第四次尝试时尝试了你的答案,这是唯一有效的!
    • 只有这个解决方案对我有用.. ListBox.ScrollIntoView 不起作用
    • 这在 Windows 10 上效果最好。不幸的是,它使我的应用程序在 Windows 7 上崩溃,我需要支持。
    【解决方案4】:

    我使用这个解决方案:http://michlg.wordpress.com/2010/01/16/listbox-automatically-scroll-currentitem-into-view/

    即使您将列表框的 ItemsSource 绑定到在非 UI 线程中操作的 ObservableCollection,它也可以工作。

    【讨论】:

      【解决方案5】:

      MVVM 风格的附加行为

      添加新项目时,此附加行为会自动将列表框滚动到底部。

      <ListBox ItemsSource="{Binding LoggingStream}">
          <i:Interaction.Behaviors>
              <behaviors:ScrollOnNewItemBehavior 
                 IsActiveScrollOnNewItem="{Binding IfFollowTail, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
          </i:Interaction.Behaviors>
      </ListBox>
      

      在您的ViewModel 中,您可以绑定到布尔值IfFollowTail { get; set; } 来控制是否启用自动滚动。

      行为做所有正确的事情:

      • 如果在 ViewModel 中设置了 IfFollowTail=false,则 ListBox 不再滚动到新项目的底部。
      • 在 ViewModel 中设置 IfFollowTail=true 后,ListBox 会立即滚动到底部,并继续滚动。
      • 速度很快。它只会在几百毫秒的不活动后滚动。幼稚的实现会非常慢,因为它会在添加的每个新项目上滚动。
      • 它适用于重复的 ListBox 项(许多其他实现不适用于重复项 - 它们滚动到第一个项目,然后停止)。
      • 非常适合用于处理连续传入项目的日志控制台。

      行为 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();
              }
          }
      }
      

      从事件到响应式扩展的桥梁

      最后,添加这个扩展方法,这样我们就可以使用所有的 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】:

        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);
                        }));
                    }
        
                }
            }
        

        【讨论】:

        • ListBox 没有“OnCollectionChanged”事件。
        【解决方案7】:

        我对提出的解决方案不满意。

        • 我不想使用“泄漏”的属性描述符。
        • 我不想为看似微不足道的任务添加 Rx 依赖和 8 行查询。我也不想要一个持续运行的计时器。
        • 不过,我确实喜欢 shawnpfiore 的想法,所以我在它之上构建了一个附加行为,到目前为止,这在我的案例中运行良好。

        这就是我最终的结果。也许它会节省一些时间。

        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
        }
        

        【讨论】:

        • 你的不满意让我的日子好过。非常聪明的解决方案。
        【解决方案8】:

        我发现执行此操作的最直接的方法,尤其是对于绑定到数据源的列表框(或列表视图),是将其与集合更改事件挂钩。 您可以在列表框的 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 属性。叹。

        【讨论】:

          【解决方案9】:

          我找到了一种更简单的方法,可以帮助我解决类似的问题,只需几行代码,无需创建自定义行为。检查我对这个问题的回答(并点击里面的链接):

          wpf(C#) DataGrid ScrollIntoView - how to scroll to the first row that is not shown?

          它适用于 ListBox、ListView 和 DataGrid。

          【讨论】:

            【解决方案10】:

            所以我在这个主题中读到的内容对于一个简单的动作来说有点复杂。

            所以我订阅了 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;
                    } 
                }
            

            当我按下按钮时,复选框被选中,如果我用鼠标滚轮向上滚动,则将我的视图保持在底部,复选框将被取消选中,您可以浏览您的列表框。

            【讨论】:

              【解决方案11】:

              这是我使用的有效解决方案,可能对其他人有所帮助;

               statusWindow.SelectedIndex = statusWindow.Items.Count - 1;
               statusWindow.UpdateLayout();
               statusWindow.ScrollIntoView(statusWindow.SelectedItem);
               statusWindow.UpdateLayout();
              

              【讨论】:

                【解决方案12】:

                这对我有用:

                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
                }
                

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 2012-04-08
                  • 2017-05-05
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-03-18
                  • 2017-04-09
                  相关资源
                  最近更新 更多