滚动到虚拟化ItemsControl的元素

11

我有一个使用ItemsControl控件在ScrollViewer中显示其项目,并进行虚拟化。我正在尝试将ScrollViewer滚动到其中包含的一个(屏幕外,因此是虚拟的)项目。然而,由于该项目是虚拟的,它并不存在于屏幕上并且没有位置(如果我理解正确的话)。

我已经尝试了对子元素使用BringIntoView,但没有滚动到视图中。我还尝试了使用TransformToAncestorTransformBoundsScrollToVerticalOffset手动完成它,但是TransformToAncestor从不返回(我猜测也是因为虚拟化,因为它没有位置,但我没有证据),并且其后的代码从未执行。

是否可以在使用虚拟化的ItemsControl中滚动到一个项目?如果可以,如何实现?

6个回答

20

我一直在研究如何让VirtualizingStackPanel的ItemsControl滚动到某个项目,但一直找到的答案是“使用ListBox”。我不想用ListBox,所以我找到了一种方法。首先,您需要为ItemsControl设置一个控件模板,其中包含一个ScrollViewer(如果您正在使用ItemsControl,则可能已经拥有)。我的基本模板类似于以下内容(包含在ItemsControl的便捷样式中)

<Style x:Key="TheItemsControlStyle" TargetType="{x:Type ItemsControl}">
    <Setter Property="Template">
    <Setter.Value>
            <ControlTemplate TargetType="{x:Type ItemsControl}">
                <Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="{TemplateBinding Control.Padding}" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" SnapsToDevicePixels="True">
                    <ScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False" HorizontalScrollBarVisibility="Auto">
                        <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
                    </ScrollViewer>
                </Border>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

所以,我基本上有一个带有滚动视图的边框,它将包含我的内容。
我的ItemsControl定义为:

<ItemsControl x:Name="myItemsControl" [..snip..] Style="{DynamicResource TheItemsControlStyle}"  ScrollViewer.CanContentScroll="True" VirtualizingStackPanel.IsVirtualizing="True">

现在来到有趣的部分。我已经创建了一个扩展方法,可以将其附加到任何 ItemsControl 上,以便滚动到给定的项:

public static void VirtualizedScrollIntoView(this ItemsControl control, object item) {
        try {
            // this is basically getting a reference to the ScrollViewer defined in the ItemsControl's style (identified above).
            // you *could* enumerate over the ItemsControl's children until you hit a scroll viewer, but this is quick and
            // dirty!
            // First 0 in the GetChild returns the Border from the ControlTemplate, and the second 0 gets the ScrollViewer from
            // the Border.
            ScrollViewer sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild((DependencyObject)control, 0), 0) as ScrollViewer;
            // now get the index of the item your passing in
            int index = control.Items.IndexOf(item);
            if(index != -1) {
                // since the scroll viewer is using content scrolling not pixel based scrolling we just tell it to scroll to the index of the item
                // and viola!  we scroll there!
                sv.ScrollToVerticalOffset(index);
            }
        } catch(Exception ex) {
            Debug.WriteLine("What the..." + ex.Message);
        }
    }

有了扩展方法后,您可以像使用ListBox的伴生方法一样使用它:

myItemsControl.VirtualizedScrollIntoView(someItemInTheList);

非常好用!

请注意,您还可以调用sv.ScrollToEnd()和其他常见的滚动方法来浏览您的项目。


1
很不幸,我使用基于像素的滚动,所以这对我没有用,但我相信这将有助于未来的其他人,+1。 - Seth Carnegie
如果您正在使用基于像素的滚动,则可以获取ItemsControl中单个项的大小(如果其大小固定,则很容易,但是还可以在ItemControls ItemTemplate上执行枚举以获取单个项的大小),然后只需将返回的索引乘以单个项的大小,然后调用ScrollToVerticalOffset并传入该数字。即 sv.ScrollToVerticalOffset((double)index * sizeOfAnItemInTheList); - Aaron Cook

10

我知道这是一个旧的线程,但是如果有其他人(像我一样)遇到这个问题,我想更新答案会很值得。

在 .NET Framework 4.5 版本中,VirtualizingPanel 公开了一个 BringIndexIntoViewPublic 方法,它可以完美地与基于像素的滚动一起使用。你需要子类化你的 ItemsControl 或使用 VisualTreeHelper 来查找其子级的 VirtualizingPanel,但无论哪种方式,现在都非常容易精确地强制你的 ItemsControl 滚动到特定的项目/索引。


1
我爱你。现在它确实像你描述的那样简单明了。 - Pancake5475

10
在.NET源代码中搜索后,我建议您使用ListBox及其ScrollIntoView方法。该方法的实现依赖于一些内部方法,如VirtualizingPanel.BringIndexIntoView,它强制在该索引处创建项目并滚动到该索引处。许多这些机制是内部的,这意味着如果您自己尝试实现此操作,你会遇到问题。(要使其带来的选择不可见,可以重定向ListBoxItems的模板)

我想避免这样做,因为我不需要ListBox的“选择项目”功能。你有什么想法为什么ItemsControl没有ScrollIntoView - Seth Carnegie
@Seth:就像我说的,你可以隐藏选择,谁在乎它是否存在?它没有滚动条是因为它是这样设计的,“ItemsControl”是最基本的项控件,对于这样的基类来说,滚动功能是不需要的。 - H.B.
现在要找出如何使 ListBox 在单击时完全停止滚动项目... - Seth Carnegie
1
好的,明白了,你只需要处理 ListBoxItemMouseDown 事件并将 MouseButtonEventArgs.Handled 设置为 true 就可以了。谢谢。 - Seth Carnegie

2

使用@AaronCook的示例,我创建了一个适用于我的VirtualizingItemsControl的行为。以下是该代码:

public class ItemsControlScrollToSelectedBehavior : Behavior<ItemsControl>
{
    public static readonly DependencyProperty SelectedItemProperty =
        DependencyProperty.Register("SelectedItem", typeof(object), typeof(ItemsControlScrollToSelectedBehavior),
            new FrameworkPropertyMetadata(null,
                new PropertyChangedCallback(OnSelectedItemsChanged)));

    public object SelectedItem
    {
        get => GetValue(SelectedItemProperty);
        set => SetValue(SelectedItemProperty, value);
    }

    private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        ItemsControlScrollToSelectedBehavior target = (ItemsControlScrollToSelectedBehavior)d;
        object oldSelectedItems = e.OldValue;
        object newSelectedItems = target.SelectedItem;
        target.OnSelectedItemsChanged(oldSelectedItems, newSelectedItems);
    }

    protected virtual void OnSelectedItemsChanged(object oldSelectedItems, object newSelectedItems)
    {
        try
        {
            var sv = VisualTreeHelper.GetChild(VisualTreeHelper.GetChild(AssociatedObject, 0), 0) as ScrollViewer;
            // now get the index of the item your passing in
            int index = AssociatedObject.Items.IndexOf(newSelectedItems);
            if (index != -1)
            {
                sv?.ScrollToVerticalOffset(index);
            }
        }
        catch
        {
            // Ignore
        }
    }
}

使用方法如下:

<ItemsControl Style="{StaticResource VirtualizingItemsControl}"                      
                  ItemsSource="{Binding BoundItems}">
        <i:Interaction.Behaviors>
            <behaviors:ItemsControlScrollToSelectedBehavior SelectedItem="{Binding SelectedItem}" />
        </i:Interaction.Behaviors>
    </ItemsControl>

对于那些喜欢行为和干净的XAML,不想要代码后台的人来说非常有用。


1

我知道我来晚了,但希望这能帮助其他寻找解决方案的人...

int index = myItemsControl.Items.IndexOf(*your item*).FirstOrDefault();
int rowHeight = *height of your rows*;
myScrollView.ScrollToVerticalOffset(index*rowHeight);
//this will bring the given item to the top of the scrollViewer window

"...我的XAML设置如下..."
<ScrollViewer x:Name="myScrollView">
    <ItemsControl x:Name="myItemsControl">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <Grid>
                    <!-- data here -->
                </Grid>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</ScrollViewer>

0

这是一个旧的帖子,但我想提出一种建议:

/// <summary>
/// Scrolls to the desired item
/// </summary>
/// <param name="control">ItemsControl</param>
/// <param name="item">item</param>
public static void ScrollIntoView(this ItemsControl control, Object item)
{
    FrameworkElement framework = control.ItemContainerGenerator.ContainerFromItem(item) as FrameworkElement;
    if (framework == null) { return; }
    framework.BringIntoView();
}

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