54 votes

Faire en sorte que ListView.ScrollIntoView fasse défiler l'élément au centre de la ListView (C#)

ListView.ScrollIntoView(object) trouve actuellement un objet dans le ListView et le fait défiler jusqu'à lui. Si vous êtes positionné sous l'objet vers lequel vous souhaitez défiler, il fait défiler l'objet vers la ligne supérieure. Si vous êtes positionné au-dessus, l'objet défile dans la rangée inférieure.

J'aimerais que l'élément défile au centre de ma vue de liste s'il n'est pas visible actuellement. Existe-t-il un moyen simple d'y parvenir ?

88voto

Ray Burns Points 38537

Il est très facile de faire cela dans WPF avec une méthode d'extension que j'ai écrite. Tout ce que vous avez à faire pour faire défiler un élément au centre de la vue est d'appeler une seule méthode.

Supposons que vous ayez ce XAML :

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

Votre méthode ScrollIntoView sera simplement :

private void ScrollIntoView(object sender, SelectionChangedEventArgs e)
{
  view.ScrollToCenterOfView(box.SelectedItem);
} 

Il est évident que cela pourrait être fait en utilisant un ViewModel plutôt que de référencer explicitement les contrôles.

Voici l'implémentation. Elle est très générale et gère toutes les possibilités de IScrollInfo. Elle fonctionne avec ListBox ou tout autre ItemsControl, et fonctionne avec tout panneau, y compris StackPanel, VirtualizingStackPanel, WrapPanel, DockPanel, Canvas, Grid, etc.

Il suffit de le placer dans un fichier .cs quelque part dans votre projet :

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 votes

J'adore. Merci beaucoup ! Il a parfaitement fonctionné.

4 votes

En fait, cela ne fonctionne pas avec tout autre ItemsControl . Je n'ai pas testé toutes les possibilités, mais à tout le moins, cela ne fonctionne pas avec DataGrid avec la virtualisation activée. Vous voyez, dans le cas où l'élément cible est trop loin de la fenêtre d'affichage, ContainerForItem renvoie null, et votre méthode abandonne à ce moment-là et renvoie false. Et le fait de le programmer jusqu'à "après que tout soit chargé" n'aide pas beaucoup non plus, parce que rien ne va se charger jusqu'à ce que la position du scroll change. (voir commentaire suivant)

1 votes

On peut ajouter un cas spécial pour cela, tout comme vous l'avez fait pour ListBox mais je suis sûr que toute autre situation de virtualisation donnera le même résultat. D'autres idées qui pourraient être "encapsulées" et "couvrir proprement toutes les possibilités" ?

9voto

Scrappydog Points 2403

L'excellente réponse de Ray Burns ci-dessus est spécifique à WPF.

Voici une version modifiée qui fonctionne dans 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);
        }
    }

1voto

lc. Points 50297

Je crois me rappeler avoir fait quelque chose comme ça moi-même à un moment donné. D'après mes souvenirs, ce que j'ai fait était :

  1. Déterminer si l'objet est déjà visible ou non.
  2. S'il n'est pas visible, obtenez l'index de l'objet que vous voulez, et le nombre d'objets actuellement affichés.
  3. (index you want) - (number of objects displayed / 2) devrait être la ligne supérieure, donc faites défiler jusqu'à elle (en veillant à ne pas aller en négatif, bien sûr).

0 votes

Vous êtes bloqué aux étapes 1 et 2. Vous connaissez la syntaxe permettant de vérifier tous les objets visibles dans une ListView en C#/WPF ?

0 votes

En fait, c'est une très bonne question. Je faisais ça dans WinForms, et je pense que c'était juste un bon vieux truc normal. ListBox ...je n'arrive pas à trouver un moyen de le faire. Peut-être qu'en creusant dans Reflector, vous trouverez quelque chose ou que quelqu'un d'autre le saura ?

1voto

Ragepotato Points 1060

Si vous regardez le modèle d'une boîte de liste, c'est simplement un scrollviewer avec un présentateur d'articles à l'intérieur. Vous devrez calculer la taille de vos éléments et utiliser scrollviewer horizontalement ou verticalement pour positionner les éléments dans votre scrollviewer. La boîte à outils April Silverlight possède une méthode d'extension GetScrollHost que vous pouvez appeler sur une boîte de liste pour obtenir votre scrollviewer sous-jacent.

Une fois que vous l'avez, vous pouvez utiliser l'actuel Horizontal ou Vertical Le décalage est un cadre de référence et vous pouvez déplacer votre liste en conséquence.

1voto

L'exemple ci-dessous trouvera le scrollviewer du listview et l'utilisera pour faire défiler l'élément au milieu du listview.

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>

Code derrière :

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

0 votes

Cela fonctionne dans le cas très restreint où vous avez une ListView par défaut sans modèle personnalisé et un panneau par défaut, vos données sont disponibles dans la même classe et sont liées de manière triviale (pas de filtrage, de regroupement, de tri, etc.), et cela ne vous dérange pas de tout coder en dur. Je n'aime pas non plus ce système parce qu'il n'est pas propre ou WPF-ish et qu'il ne fonctionnera pas bien avec un ViewModel. Je préfère tout encapsuler dans une seule méthode d'extension qui gère proprement tous les scénarios possibles. Voir ma réponse pour plus de détails.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X