簡體   English   中英

使 ListView.ScrollIntoView 將項目滾動到 ListView 的中心(C#)

[英]Make ListView.ScrollIntoView Scroll the Item into the Center of the ListView (C#)

ListView.ScrollIntoView(object)當前在ListView中找到一個 object 並滾動到它。 如果您位於要滾動到的 object 下方,它會將 object 滾動到頂行。 如果您位於上方,它會將其滾動到底行的視圖中。

如果該項目當前不可見,我希望將該項目直接滾動到我的列表視圖的中心。 有沒有簡單的方法來實現這一點?

使用我編寫的擴展方法在 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);
  }
}

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

上面雷·伯恩斯的出色回答和費奧多·索金的評論:

“實際上,它不適用於任何其他 ItemsControl ......不適用於打開虛擬化的DataGrid ......”

用:

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

@all:目前無法評論,需要 50 聲望

我找到了一個額外的方法來解決這個問題,假設我們中的一些人只需要一種方法來根據項目模板找出視覺項目的高度,這將大大節省您的時間。

好的,我假設您的 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. 確定對象是否已經可見。
  2. 如果它不可見,請獲取所需對象的索引以及當前顯示的對象數。
  3. (index you want) - (number of objects displayed / 2)應該是第一行,所以滾動到那個(當然,確保你不會變成負數)

如果您查看列表框的模板,它只是一個帶有 itemspresenter 的滾動查看器。 您需要計算項目的大小並使用水平垂直滾動來定位滾動查看器中的項目。 april silverlight 工具包有一個擴展方法 GetScrollHost,您可以在列表框上調用它來獲取底層滾動查看器。

一旦你有了它,你就可以使用當前的水平垂直偏移作為參考框架並相應地移動你的列表。

下面的示例將找到列表視圖的滾動查看器並使用它來將項目滾動到列表視圖的中間。

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

如果問題是滾動不一致(從上方/下方滾動之間的差異),則可以通過先滾動到列表頂部,然后滾動到所需行 + 可見行數的一半來解決。 需要額外的范圍檢查以避免 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]);

我知道這篇文章很舊,但我想在上面提供 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;
        }

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

我使用了 Ray Burns 的出色答案。 但是,VirtualizingStackPanel.ScrollUnit 設置為“Pixel”時不起作用,只有當滾動單元設置為“Item”時才起作用。 當單位為像素時,無需調整邏輯滾動。 一個快速修復就可以解決問題,並且代碼適用於兩種情況:

改變

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

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

當按像素滾動時,它將繞過邏輯滾動的調整。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM