2010-06-01 17 views
48

ListView.ScrollIntoView(object) encuentra actualmente un objeto en el ListView y se desplaza hasta él. Si se encuentra debajo del objeto al que se desplaza, desplaza el objeto a la fila superior. Si está posicionado arriba, lo desplaza a la vista en la fila inferior.Hacer ListView.ScrollIntoView Desplazar el elemento en el centro de ListView (C#)

Me gustaría que el elemento se desplazara hacia la derecha en el centro de mi vista de lista si actualmente no está visible. ¿Hay una manera fácil de lograr esto?

Respuesta

73

Es muy fácil de hacer esto en WPF con un método de extensión que escribí. Todo lo que tiene que hacer para desplazar un elemento al centro de la vista es llamar a un único método.

Suponga que tiene este XAML:

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

Su método ScrollIntoView será simplemente:

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

Obviamente, esto se podría hacer uso de un modelo de vista, así en lugar de hacer referencia a los controles de forma explícita.

La siguiente es la implementación. Es muy general, maneja todas las posibilidades de IScrollInfo. Funciona con ListBox o cualquier otro ItemsControl, y funciona con cualquier panel que incluye StackPanel, VirtualizingStackPanel, WrapPanel, DockPanel, Lona, rejilla, etc.

sólo hay que poner esto en un archivo .cs en algún lugar de su proyecto:

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

Me encanta. ¡Muchas gracias! Funcionó perfectamente –

+3

En realidad, no funciona con * cualquier * otro 'ItemsControl'. No probé todas las posibilidades, pero al menos no funciona con 'DataGrid' con la virtualización activada. Verá, en caso de que el elemento de destino esté demasiado lejos de la ventana gráfica, 'ContainerForItem' devuelve nulo, y su método se da por vencido en ese punto y devuelve falso. Y programarlo hasta "después de que todo se carga" tampoco ayuda mucho, porque nada se va a cargar hasta que la posición de desplazamiento cambie. (vea el siguiente comentario) –

+0

Se puede agregar un caso especial para esto, tal como lo hizo para 'ListBox', pero estoy bastante seguro de que cualquier otra situación de virtualización arrojará el mismo resultado. ¿Alguna otra idea que sea poderosa "encapsulada" y que "cubra todas las posibilidades limpiamente"? –

1

Parece que recuerdo haber hecho algo así en algún momento. En lo que respecta a mi memoria, lo que hice fue:

  1. Determine si el objeto ya está visible o no.
  2. Si no está visible, obtenga el índice del objeto que desea y la cantidad de objetos que se muestran actualmente.
  3. (index you want) - (number of objects displayed/2) debería ser la fila superior, por lo que vaya a (asegurándose de que no se va negativo, por supuesto)
+0

Quédate atascado en los pasos 1 y 2. ¿Conoces la sintaxis para verificar todos los objetos que son visibles en un ListView en C#/WPF? –

+0

En realidad esa es una muy buena pregunta. Estaba haciendo esto en WinForms, y creo que era solo un "ListBox" normal y viejo ... Parece que no puedo encontrar la forma de hacerlo. ¿Tal vez investigar en Reflector descubrirá algo o alguien más lo sabe? –

1

Si observa la plantilla de un Listbox, es simplemente un visualizador de desplazamiento con un presentador de elementos dentro. Necesitará calcular el tamaño de sus artículos y usar scroll horizontally o vertically para colocar los artículos en su scrollviewer. El kit de herramientas de abril de silverlight tiene un método de extensión GetScrollHost al que puedes llamar en un cuadro de lista para obtener tu scrollviewer subyacente.

Una vez que tenga eso, puede usar el actual Horizontal o Vertical Offset como un marco de referencia y mover su lista de manera acorde.

1

El siguiente ejemplo encontrará el scrollviewer de la vista de lista y lo usará para desplazar el elemento hacia la mitad de la vista de lista.

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> 

Código atrás: excelente respuesta

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

Esto funciona en el caso muy restringido donde tienes un ListView predeterminado sin una plantilla personalizada y un panel predeterminado, tus datos están disponibles en la misma clase y están ligados trivialmente (sin filtro, agrupación, clasificación, etc.), y no lo haces Me importa programar todo junto. Tampoco me gusta porque no está limpio o WPF-ish y no funcionará bien con ViewModel. Prefiero encapsular todo en un solo método de extensión que maneja todos los escenarios posibles limpiamente. Ver mi respuesta para más detalles. –

9

Ray Burns anterior es WPF específica.

Aquí es una versión modificada que funciona en 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); 
     } 
    } 
1

me encontré con un enfoque adicional para resolver este problema, suponiendo que algunos de nosotros sólo tiene una manera de averiguar la altura del elemento visual de acuerdo con la la plantilla del elemento esto le ahorraría mucho tiempo.

Ok, asuma que su XAML está estructurado de alguna manera similar a esto:

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

Y si desea calcular con el fin de desplazarse hasta el centro, pero no tiene idea de lo que es la altura actual de cada artículo en su cuadro de lista .. esto es cómo se puede averiguar: excelente respuesta

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

Ray Burns arriba y el comentario de Fyodor Soikin:

"Actu aliado, que no funciona con ningún otro ItemsControl ... no funciona con DataGrid con virtualización encendido ..."

Uso:

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

@all: no puedo comentar por el momento, necesita 50 reputación

Cuestiones relacionadas