使 ListView.ScrollIntoView 将项目滚动到 ListView 的中心(C#)

Posted

技术标签:

【中文标题】使 ListView.ScrollIntoView 将项目滚动到 ListView 的中心(C#)【英文标题】:Make ListView.ScrollIntoView Scroll the Item into the Center of the ListView (C#) 【发布时间】:2011-02-26 04:17:53 【问题描述】:

ListView.ScrollIntoView(object) 当前在ListView 中找到一个对象并滚动到它。如果您位于要滚动到的对象下方,它会将对象滚动到顶行。如果您位于上方,它会将其滚动到底行的视图中。

如果项目当前不可见,我希望将项目向右滚动到列表视图的中心。有没有简单的方法可以做到这一点?

【问题讨论】:

【参考方案1】:

使用我编写的扩展方法在 WPF 中很容易做到这一点。将项目滚动到视图中心所需要做的就是调用一个方法。

假设你有这个 XAML:

<ListView x:Name="view" ItemsSource="Binding Data" /> 
<ComboBox x:Name="box"  ItemsSource="Binding Data"
                        SelectionChanged="ScrollIntoView" /> 

您的 ScrollIntoView 方法将是:

private void ScrollIntoView(object sender, SelectionChangedEventArgs e)

  view.ScrollToCenterOfView(box.SelectedItem);
 

显然,这也可以使用 ViewModel 来完成,而不是显式引用控件。

以下是实现。它非常通用,处理所有 IScrollInfo 可能性。它适用于 ListBox 或任何其他 ItemsControl,适用于任何面板,包括 StackPanel、VirtualizingStackPanel、WrapPanel、DockPanel、Canvas、Grid 等。

只需将其放在项目中某个位置的 .cs 文件中即可:

public static class ItemsControlExtensions

  public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
  
    // Scroll immediately if possible
    if(!itemsControl.TryScrollToCenterOfView(item))
    
      // Otherwise wait until everything is loaded, then scroll
      if(itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
      itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
        
          itemsControl.TryScrollToCenterOfView(item);
        ));
    
  

  private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
  
    // Find the container
    var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
    if(container==null) return false;

    // Find the ScrollContentPresenter
    ScrollContentPresenter presenter = null;
    for(Visual vis = container; vis!=null && vis!=itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
      if((presenter = vis as ScrollContentPresenter)!=null)
        break;
    if(presenter==null) return false;

    // Find the IScrollInfo
    var scrollInfo = 
        !presenter.CanContentScroll ? presenter :
        presenter.Content as IScrollInfo ??
        FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
        presenter;

    // Compute the center point of the container relative to the scrollInfo
    Size size = container.RenderSize;
    Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width/2, size.Height/2));
    center.Y += scrollInfo.VerticalOffset;
    center.X += scrollInfo.HorizontalOffset;

    // Adjust for logical scrolling
    if(scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
    
      double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
      Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
      if(orientation==Orientation.Horizontal)
        center.X = logicalCenter;
      else
        center.Y = logicalCenter;
    

    // Scroll the center of the container to the center of the viewport
    if(scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
    if(scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
    return true;
  

  private static double CenteringOffset(double center, double viewport, double extent)
  
    return Math.Min(extent - viewport, Math.Max(0, center - viewport/2));
  
  private static DependencyObject FirstVisualChild(Visual visual)
  
    if(visual==null) return null;
    if(VisualTreeHelper.GetChildrenCount(visual)==0) return null;
    return VisualTreeHelper.GetChild(visual, 0);
  

【讨论】:

爱它。非常感谢!完美运行。 实际上,它不适用于 any other ItemsControl。我没有测试所有的可能性,但至少,它不适用于 DataGrid 打开虚拟化。你看,如果目标项目离视口太远,ContainerForItem 返回 null,你的方法在那个点放弃并返回 false。并且将其安排到“在所有内容加载之后”也无济于事,因为在滚动位置更改之前不会加载任何内容。 (见下一条评论) 可以为此添加一个特殊情况,就像您为 ListBox 所做的那样,但我很确定任何其他虚拟化情况都会产生相同的结果。还有其他强大的“封装”和“干净地涵盖所有可能性”的想法吗? 您能否发布一个编辑后的版本以便滚动到第一行? @RayBurns 此代码不适用于关闭虚拟化的 DataGrid。它甚至不滚动..【参考方案2】:

Ray Burns 的上述出色答案是 WPF 特定的。

这是一个适用于 Silverlight 的修改版本:

 public static class ItemsControlExtensions
    
        public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
        
            // Scroll immediately if possible 
            if (!itemsControl.TryScrollToCenterOfView(item))
            
                // Otherwise wait until everything is loaded, then scroll 
                if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
                itemsControl.Dispatcher.BeginInvoke( new Action(() =>
                
                    itemsControl.TryScrollToCenterOfView(item);
                ));
            
        

        private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
        
            // Find the container 
            var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
            if (container == null) return false;

            // Find the ScrollContentPresenter 
            ScrollContentPresenter presenter = null;
            for (UIElement vis = container; vis != null ; vis = VisualTreeHelper.GetParent(vis) as UIElement)
                if ((presenter = vis as ScrollContentPresenter) != null)
                    break;
            if (presenter == null) return false;

            // Find the IScrollInfo 
            var scrollInfo =
                !presenter.CanVerticallyScroll ? presenter :
                presenter.Content as IScrollInfo ??
                FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
                presenter;

            // Compute the center point of the container relative to the scrollInfo 
            Size size = container.RenderSize;
            Point center = container.TransformToVisual((UIElement)scrollInfo).Transform(new Point(size.Width / 2, size.Height / 2));
            center.Y += scrollInfo.VerticalOffset;
            center.X += scrollInfo.HorizontalOffset;

            // Adjust for logical scrolling 
            if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
            
                double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
                Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
                if (orientation == Orientation.Horizontal)
                    center.X = logicalCenter;
                else
                    center.Y = logicalCenter;
            

            // Scroll the center of the container to the center of the viewport 
            if (scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
            if (scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
            return true;
        

        private static double CenteringOffset(double center, double viewport, double extent)
        
            return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
        

        private static DependencyObject FirstVisualChild(UIElement visual)
        
            if (visual == null) return null;
            if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
            return VisualTreeHelper.GetChild(visual, 0);
        
     

【讨论】:

【参考方案3】:

上面 Ray Burns 的出色回答和 Fyodor Soikin 的评论:

“实际上,它不适用于任何其他 ItemsControl... 不适用于 DataGrid 并打开了虚拟化...”

用途:

if (listBox.SelectedItem != null)

   listBox.ScrollIntoView(listBox.SelectedItem);
   listBox.ScrollToCenterOfView(listBox.SelectedItem);

@all:暂时不能评论,需要50声望

【讨论】:

【参考方案4】:

我似乎记得自己曾经做过类似的事情。据我的记忆,我所做的是:

    确定对象是否已经可见。 如果不可见,获取所需对象的索引,以及当前显示的对象数量。 (index you want) - (number of objects displayed / 2) 应该是第一行,所以滚动到那一行(当然要确保你没有消极)

【讨论】:

卡在第 1 步和第 2 步。知道检查 C#/WPF 中 ListView 中可见的所有对象的语法吗? 其实这是一个非常好的问题。我在 WinForms 中这样做,我认为这只是一个普通的旧 ListBox...我似乎无法找到一种方法来做到这一点。也许深入 Reflector 会发现一些东西或其他人知道?【参考方案5】:

如果您查看 Listbox 的模板,它只是一个带有 itemspresenter 的滚动查看器。您需要计算项目的大小并使用滚动 horizontally 或 vertically 在滚动查看器中定位项目。 april silverlight 工具包有一个扩展方法 GetScrollHost,您可以在列表框上调用它来获取您的底层滚动查看器。

一旦你有了它,你可以使用当前的Horizontal 或Vertical 偏移作为参考框架,并相应地移动你的列表。

【讨论】:

【参考方案6】:

下面的示例将找到列表视图的滚动查看器,并使用它将项目滚动到列表视图的中间。

XAML:

<Window x:Class="ScrollIntoViewTest.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Height="300" Width="300">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>
        <ListView Grid.Row="0" ItemsSource="Binding Path=Data" Loaded="OnListViewLoaded"/>
        <ComboBox Grid.Row="1" ItemsSource="Binding Path=Data" SelectionChanged="OnScrollIntoView" />
    </Grid>
</Window>

后面的代码:

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace ScrollIntoViewTest

    public partial class Window1 : Window
    
        public Window1()
        
            InitializeComponent();

            Data = new List<string>();
            for (int i = 0; i < 100; i++)
            
                Data.Add(i.ToString());    
            

            DataContext = this;
        

        public List<string> Data  get; set; 

        private void OnListViewLoaded(object sender, RoutedEventArgs e)
        
            // Assumes that the listview consists of a scrollviewer with a border around it
            // which is the default.
            Border border = VisualTreeHelper.GetChild(sender as DependencyObject, 0) as Border;
            _scrollViewer = VisualTreeHelper.GetChild(border, 0) as ScrollViewer;
        

        private void OnScrollIntoView(object sender, SelectionChangedEventArgs e)
        
            string item = (sender as ComboBox).SelectedItem as string;
            double index = Data.IndexOf(item) - Math.Truncate(_scrollViewer.ViewportHeight / 2);
            _scrollViewer.ScrollToVerticalOffset(index);
        

        private ScrollViewer _scrollViewer;
    

【讨论】:

这适用于非常有限的情况,即您有一个没有自定义模板和默认面板的默认 ListView,您的数据在同一个类中可用并且被简单绑定(没有过滤、分组、排序、等),并且您不介意将所有内容硬编码在一起。我也不喜欢它,因为它不干净或 WPF-ish 并且不能很好地与 ViewModel 一起使用。我更喜欢将所有内容封装在一个扩展方法中,该方法可以干净地处理所有可能的场景。有关详细信息,请参阅我的答案。【参考方案7】:

我找到了解决这个问题的另一种方法,假设我们中的一些人只需要一种方法来根据项目模板找出视觉项目的高度,这将大大节省您的时间。

好的,我假设您的 XAML 的结构与此类似:

:
<Window.Resources>
   <DataTemplate x:Key="myTemplate">
      <UserControls1:myControl DataContext="Binding" />
   </DataTemplate>
</Window.Resources>
:
<ListBox Name="myListBox" ItemTemplate="StaticResource ResourceKey=myTemplate" />

并且您想计算以滚动到中心,但您不知道当前高度是多少 列表框中的每个项目.. 您可以通过以下方式找到:

listBoxItemHeight = (double)((DataTemplate)FindResource("myTemplate")).LoadContent().GetValue(HeightProperty);

【讨论】:

【参考方案8】:

如果问题是滚动不一致(从上方/下方滚动的区别),可以通过首先滚动到列表顶部,然后滚动到所需行 + 可见行数的一半来解决。需要进行额外的范围检查以避免 IndexOutOfRange。

// we add +1 to row height for grid width
var offset = (int)(mDataGrid.RenderSize.Height / (mDataGrid.MinRowHeight + 1) / 2);
// index is the item's index in the list
if (index + offset >= mDataGrid.Items.Count) offset = 0;

mDataGrid.ScrollIntoView(mDataGrid.Items[0]);
mDataGrid.ScrollIntoView(mDataGrid.Items[index + offsest]);

【讨论】:

【参考方案9】:

我知道这篇文章很旧,但我想在上面提供 Ray Burns 出色答案的 UWP 版本

        public static async void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
        
            // Scroll immediately if possible
            if (!itemsControl.TryScrollToCenterOfView(item))
            
                // Otherwise wait until everything is loaded, then scroll
                if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);

                await itemsControl.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                
                    itemsControl.TryScrollToCenterOfView(item);
                );
            
        
         


        private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
        
            // Find the container
            var container = itemsControl.ContainerFromItem(item) as FrameworkElement;
            if (container == null) return false;

            var scrollPresenter = container.FindParent(typeof(ScrollContentPresenter)) as ScrollContentPresenter;

            if (scrollPresenter == null) return false;                      
         
            Size size = container.RenderSize;

            var center = container.TransformToVisual(scrollPresenter).TransformPoint(new Point(size.Width / 2, size.Height / 2));

            center.Y += scrollPresenter.VerticalOffset;
            center.X += scrollPresenter.HorizontalOffset;
           

            // Scroll the center of the container to the center of the viewport
            if (scrollPresenter.CanVerticallyScroll) scrollPresenter.SetVerticalOffset(CenteringOffset(center.Y, scrollPresenter.ViewportHeight, scrollPresenter.ExtentHeight));
            if (scrollPresenter.CanHorizontallyScroll) scrollPresenter.SetHorizontalOffset(CenteringOffset(center.X, scrollPresenter.ViewportWidth, scrollPresenter.ExtentWidth));
            return true;
        



        public static FrameworkElement FindParent(this FrameworkElement o, Type type)
        

            for (var element = VisualTreeHelper.GetParent(o) as FrameworkElement;
                    element != null;
                    element = VisualTreeHelper.GetParent(element) as FrameworkElement)
            

                if (element?.GetType() == type) return element;

            

            return null;

        

        private static double CenteringOffset(double center, double viewport, double extent)
        
            return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
        

【讨论】:

以上是关于使 ListView.ScrollIntoView 将项目滚动到 ListView 的中心(C#)的主要内容,如果未能解决你的问题,请参考以下文章

Windows 10 ScrollIntoView() 没有滚动到列表视图中间的项目

wpf datagrid 默认选择为最新的一行,不知道为啥设置不了

第一信使即细胞外信号,胞内信号为第二信使。

“第二信使”都有哪些?与它作用的物质都有哪些?

使引导容器大小变大会使页面无响应

Css使Div自适应居中