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

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

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

【问题讨论】:

    标签: c# wpf visual-studio 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】:

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

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

        【讨论】:

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

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

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

        【讨论】:

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

              【讨论】:

                猜你喜欢
                • 2016-01-28
                • 1970-01-01
                • 1970-01-01
                • 2017-10-02
                • 1970-01-01
                • 1970-01-01
                • 2020-09-18
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多