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

54

ListView.ScrollIntoView(object)方法可以在ListView中查找一个对象并滚动到它的位置。如果你当前在目标对象的下方,该方法会将目标对象滚动到顶部行;如果你在上方,该方法会将其滚动到底部行。

如果想让该项在列表视图中居中显示,有没有简单的方法来实现呢?

10个回答

90

我用一个扩展方法在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);
  }
}

2
太棒了,非常感谢!完美地运作了。 - Kirk Ouimet
4
实际上,它不能与任何其他“ItemsControl”一起使用。我没有测试所有可能性,但至少,当启用虚拟化的“DataGrid”时无法使用。您看,如果目标项距离视口太远,“ContainerForItem”会返回null,而您的方法则在该点放弃并返回false。而将其调度到“加载所有内容后”也没有什么帮助,因为在滚动位置改变之前不会加载任何内容。(请参见下一个评论) - Fyodor Soikin
1
可以为此添加一个特殊情况,就像您为 ListBox 所做的那样,但我相信任何其他虚拟化情况都会产生相同的结果。还有其他的想法是非常“封装”和“干净地覆盖所有可能性”的吗? - Fyodor Soikin
1
你能否发布一个编辑过的版本,使其可以滚动到顶部行? - Jon Koivula
2
@RayBurns 这段代码在关闭虚拟化的 DataGrid 上无法工作。它甚至无法滚动。 - Vishal
显示剩余2条评论

9

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

4

Ray Burns的上面的回答以及Fyodor Soikin的评论:

“实际上,它不适用于任何其他ItemsControl...不适用于启用虚拟化的DataGrid…”

使用:

if (listBox.SelectedItem != null)
{
   listBox.ScrollIntoView(listBox.SelectedItem);
   listBox.ScrollToCenterOfView(listBox.SelectedItem);
}

@all: 目前无法发表评论,需要50点声望值


1

我使用了Ray Burns的出色回答。然而,当VirtualizingStackPanel.ScrollUnit设置为“Pixel”时,它将无法工作,仅当滚动单位设置为“Item”时才有效。当单位为像素时,不需要进行逻辑滚动的调整。一个快速修复就可以解决问题,并且代码在两种情况下都可以正常工作:

更改

// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)

To

// Adjust for logical scrolling
if (scrollInfo is StackPanel || (scrollInfo is VirtualizingStackPanel && VirtualizingPanel.GetScrollUnit(itemsControl) == ScrollUnit.Item))

它将在按像素滚动时绕过逻辑滚动的调整。

这并没有回答问题。一旦您拥有足够的声望,您将能够评论任何帖子;相反,提供不需要询问者澄清的答案。- 来自审核 - trenton-ftw

1
以下示例将查找列表视图的滚动查看器,并使用它将项目滚动到列表视图的中间。
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不兼容。我更喜欢将所有可能的情况封装在单个扩展方法中,以清晰地处理所有可能的情况。有关更多详细信息,请参见我的答案。 - Ray Burns

1
我找到了另一种解决这个问题的方法,假设我们中的一些人只需要找到根据项模板的可视项的高度,这将极大地节省您的时间。
好的,我假设你的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);

1

我好像在某个时候也做过类似的事情。就我记忆所及,我所做的是:

  1. 确定对象是否已经可见。
  2. 如果它不可见,获取您想要的对象的索引和当前显示的对象数量。
  3. (您想要的索引) - (显示的对象数量 / 2) 应该是顶部行,因此滚动到该行(当然要确保不会出现负数)

卡在步骤1和2。知道如何检查C#/WPF中ListView中可见的所有对象的语法吗? - Kirk Ouimet
其实这是一个非常好的问题。我在WinForms中处理这个问题,我认为它只是一个普通的 ListBox ... 我似乎找不到解决方法。也许深入研究Reflector会发现一些东西,或者有人知道? - lc.

1

如果你查看Listbox的模板,它只是一个包含itemspresenter的滚动查看器。 您需要计算您的项目的大小,并使用水平或垂直滚动来定位滚动查看器中的项目。 四月银光工具包有一个扩展方法GetScrollHost,您可以在listbox上调用该方法以获取底层滚动查看器。

一旦您拥有它,您可以使用当前的水平或垂直偏移作为参考框架并相应地移动列表。


0
如果问题是滚动不一致(从上方/下方滚动的差异),可以通过先滚动到列表顶部,然后滚动到所需行数加上可见行数的一半来解决。还需要进行额外的范围检查以避免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]);

0

我知道这篇文章有点旧了,但是我想提供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));
        }

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接