首页 > 解决方案 > 是否可以以编程方式滚动 WPF ListView 以便将所需的分组标题放置在其顶部?

问题描述

给定ListView已使用 a 分组的项目的绑定PropertyGroupDescription,是否可以以编程方式滚动以将组放置在列表的顶部?我知道我可以滚动到组中的第一个项目,因为该项目属于ListView绑定到的集合。但是,我找不到任何描述如何滚动到组标题(样式为GroupStyle)的资源。

为了给出所需功能的示例,让我们看一下Visual Studio Code中的设置页面。此页面包含一个面板,允许用户滚动浏览所有应用程序的设置(组织在各自的组下)以及左侧的树结构,以便更快地导航到主面板中的特定组。在随附的屏幕截图中,我单击左侧树中的Formatting选项,主面板自动滚动,相应的组标题位于主面板的顶部。

如何在 WPF 中重新创建它(如果可能的话)?Visual Studio Code中主设置面板的“无限”滚动可以用另一个 WPF 控件来模仿吗?

在此处输入图像描述

标签: c#wpfxamlwpf-listview

解决方案


左边的树(TOC)有根节点(例如'TextEditor'的部分)。每个部分都包含设置类别(例如“格式化”)。右侧(设置视图)的ListView项目具有组标题,其类别名称与目录名称匹配(例如格式化)。

1.编辑解决使用PropertyGroupDescription

假设:

  • 在一个名为CollectionViewSource的内部存在一个CollectionViewSource定义 。ResourceDictionary
  • 设置数据项有一个属性SettingsCategoryName(例如格式)。
  • SettingsCategoryNameof SelectedItemthe TreeView绑定到一个属性SelectedSettingsCategoryName

查看.xaml:

<ResourceDictionary>
  <CollectionViewSource x:Key="CollectionViewSource" Source="{Binding Settings}">
      <CollectionViewSource.GroupDescriptions>
        <PropertyGroupDescription PropertyName="SettingsCategoryName"/>
      </CollectionViewSource.GroupDescriptions>
  </CollectionViewSource>
</ResourceDictionary>

<ListView x:Name="ListView" ItemsSource="{Binding Source={StaticResource CollectionViewSource}}">
  <ListView.GroupStyle>
    <GroupStyle>
      <GroupStyle.HeaderTemplate>
        <DataTemplate>
          <TextBlock FontWeight="Bold"
                     FontSize="14"
                     Text="{Binding Name}" />
        </DataTemplate>
      </GroupStyle.HeaderTemplate>
    </GroupStyle>
  </ListView.GroupStyle>
</ListView>

View.xaml.cs:
找到选定的类别并将其滚动到视口的顶部。

// Scroll the selected section to top when the selected item has changed
private void ScrollToSection()
{
  CollectionViewSource viewSource = FindResource("CollectionViewSource") as CollectionViewSource;
  CollectionViewGroup selectedGroupItemData = viewSource
    .View
    .Groups
    .OfType<CollectionViewGroup>()
    .FirstOrDefault(group => group.Name.Equals(this.SelectedSettingsCategoryName));

  GroupItem selectedroupItemContainer = this.ListView.ItemContainerGenerator.ContainerFromItem(selectedGroupItemData) as GroupItem;

  ScrollViewer scrollViewer;
  if (!TryFindCildElement(this.ListView, out scrollViewer))
  {
    return;
  }

  // Subscribe to scrollChanged event 
  // because the scroll executed by `BringIntoView` is deferred.
  scrollViewer.ScrollChanged += ScrollSelectedGroupToTop;

  selectedGroupItemContainer?.BringIntoView();
}

private void ScrollSelectedGroupToTop(object sender, ScrollChangedEventArgs e)
{
  ScrollViewer scrollViewer;
  if (!TryFindCildElement(this.ListView, out scrollViewer))
  {
    return;
  }

  scrollViewer.ScrollChanged -= ScrollGroupToTop;
  var viewSource = FindResource("CollectionViewSource") as CollectionViewSource;

  CollectionViewGroup selectedGroupItemData = viewSource
    .View
    .Groups
    .OfType<CollectionViewGroup>()
    .FirstOrDefault(group => group.Name.Equals(this.SelectedSettingsCategoryName));

  var groupIndex = viewSource
    .View
    .Groups.IndexOf(selectedGroupItemData);

  var absoluteVerticalScrollOffset = viewSource
    .View
    .Groups
    .OfType<CollectionViewGroup>()
    .TakeWhile((group, index) => index < groupIndex)
    .Sum(group =>
      (this.ListView.ItemContainerGenerator.ContainerFromItem(group) as GroupItem)?.ActualHeight 
     ?? 0
    );

  scrollViewer.ScrollToVerticalOffset(absoluteVerticalScrollOffset);
}

// Generic method to find any `DependencyObject` in the visual tree of a parent element
private bool TryFindCildElement<TElement>(DependencyObject parent, out TElement resultElement) where TElement : DependencyObject
{
  resultElement = null;
  for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
  {
    DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

    if (childElement is Popup popup)
    {
      childElement = popup.Child;
    }

    if (childElement is TElement)
    {
      resultElement = childElement as TElement;
      return true;
    }

    if (TryFindCildElement(childElement, out resultElement))
    {
      return true;
    }
  }

  return false;
}

您可以将此方法移动到ListView派生类型中。然后将 a 添加到处理路由命令CommandBindings的新自定义中,例如. 将 a模板化为a并让他们发出命令以将部分名称传递给 custom 。ListViewScrollToSectionRoutedCommandTreeViewItemsButtonCommandParameterListView

备注
由于使用PropertyGroupDescription导致混合数据类型的项目源(GroupItemData对于组标头和实际数据项目),因此宿主的 UI 虚拟化ItemsControl被禁用并且不可能(请参阅 Microsoft Docs: 优化性能:控件)。在这种情况下,附加属性ScrollViewer.CanContentScroll会自动设置为False(强制)。对于大列表,这可能是一个巨大的缺点,也是采用替代方法的理由。

2. 替代方案(支持 UI 虚拟化)

当涉及到实际设置结构的设计时,有几种可能的变化。它可以是一棵树,其中每个类别标题节点都有自己的子节点,这些子节点表示类别的设置,也可以是一个平面列表结构,其中类别标题和设置都是同级的。为了示例的简单起见,我选择了第二个选项:平面列表数据结构。

2.1 设置

基本思想:使用具有两个级别
的a 进行TreeView模板化。HierarchicalDataTemplate第二级的TreeView(叶子)和ListView页眉项目共享相同的实例(IHeaederData见后文)。因此,选定的标题项目TreeView引用完全相同的项目标题ListView- 无需搜索。

实施概述:

  • 你需要两个ItemsControl元素:
    • 一个TreeView用于左侧导航窗格,有两个级别
      • 带有部分根节点(例如“文本编辑器”)
      • 以及该部分的设置类别标题子节点(叶节点)(例如“字体”、“格式”)
    • 一个ListView用于实际设置及其类别标题。
  • 然后设计数据类型来表示一个设置、一个设置头和一个节根节点
    • 让他们都实现一个IData具有共享属性的公共(例如标题)
    • 让设置头数据类型实现一个额外的 IHeaderData
    • 让设置数据类型实现一个额外的ISettingData
    • 让实现的父节节点数据类型(根节点)TreeView附加ISectionData一个具有子类型的IHeaderData
  • 创建项目源集合(所有类型IEnumerable<IData>
    • TreeView一个用于(仅包含类别)的每个父节节点,SectionCollection类型为ISectionData
    • 每个类别一个,一个CategoryCollection类型IHeaderData
    • 一个用于设置数据和共享类别(标题数据),一个SettingCollection类型IData
  • 逐节填充排序的源集合
    • 将类型的部分数据实例添加ISectionData到的源集合SectionCollectionTreeView
    • 将类型的共享类别数据头实例添加IHeaderData到源集合CategoryCollectionSettingCollection
    • 添加一个类型的设置实例ISettingData,一个用于类别的每个设置,到SettingCollection唯一的
    • 对当前部分的所有类别重复最后两个步骤
    • 分配给CategoryCollection根节点的ISectionData子集合
    • 对所有部分重复这些步骤(及其类别和相应的设置)
  • 绑定SectionCollectionTreeView
  • 绑定SettingsCollectionLIstView
  • 为类型为根HierarchicalDataTemplateTreeView数据创建一个ISectionData
  • 创建DataTemplate两个ListView
    • 一个针对IHeaderData
    • 一个针对ISettingData

逻辑:

  • When a IHeaderDataitem of the TreeViewis selected then
    • 使用获取ListView此数据项的项容器var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)
    • 将容器滚动到视图container.BringIntoView()中(实现视图外的虚拟化项目)
    • 将容器滚动到视图顶部

因为TreeViewListView共享相同的类别标题数据 ( IHeaderData),所选项目易于跟踪和查找。您不必搜索设置组。您可以使用引用直接跳转到组。这意味着数据的结构是解决方案的关键。


推荐阅读