为了账号安全,请及时绑定邮箱和手机立即绑定

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

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

C#
白衣非少年 2023-07-09 15:05:05
给定ListView已使用 a 分组的项目的绑定PropertyGroupDescription,是否可以以编程方式滚动以便将组放置在列表的顶部?我知道我可以滚动到组中的第一个项目,因为该项目属于绑定到的集合ListView。但是,我无法找到任何描述如何滚动到组标题(样式为GroupStyle)的资源。为了给出所需功能的示例,让我们看一下Visual Studio Code中的设置页面。该页面包含一个面板,允许用户滚动浏览所有应用程序的设置(在各自的组下组织)以及左侧的树结构,以便更快地导航到主面板中的特定组。在所附的屏幕截图中,我单击左侧树中的“格式化”选项,主面板自动滚动,以便相应的组标题位于主面板的顶部。如何在 WPF 中重新创建它(如果可能的话)?Visual Studio Code中主设置面板的“无限”滚动是否可以用另一个 WPF 控件来模仿?
查看完整描述

1 回答

?
Cats萌萌

TA贡献1805条经验 获得超9个赞

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

1. 编辑以解决使用PropertyGroupDescription

假设:

  • 在 a 内部存在一个CollectionViewSource定义 ResourceDictionary并命名为CollectionViewSource

  • 设置数据项具有属性SettingsCategoryName(例如格式)。

  • SettingsCategoryNameSelectedItem绑定 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并让它们发出命令以将节名称传递给自定义.ListViewScrollToSectionRoutedCommandTreeViewItemsButtonCommandParameterListView

备注
由于使用PropertyGroupDescription结果会产生混合数据类型的项目源(GroupItemData对于组标头以及实际数据项目),因此托管的 UI 虚拟化ItemsControl已禁用且不可能(。在这种情况下,附加属性ScrollViewer.CanContentScroll会自动设置为False(强制)。对于大列表来说,这可能是一个巨大的缺点,也是采用替代方法的原因。

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

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

2.1 设置

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

实施概述:

  • 您需要两个ItemsControl元素:

    • 带有节根节点(例如“文本编辑器”)

    • 以及该部分的设置类别标题子节点(叶节点)(例如“字体”、“格式”)

    • TreeView左侧导航窗格有 两层

    • 一个ListView用于实际设置及其类别标题。

  • 然后设计数据类型来表示设置、设置标头和节根节点

    • 让它们都实现一个IData具有共享属性的共同点(例如标头)

    • 让设置头数据类型实现一个额外的 IHeaderData

    • 让设置数据类型实现一个额外的ISettingData

    • 让父节节点数据类型(根节点)用于实现具有子节点类型的TreeView附加节点ISectionDataIHeaderData

  • 创建项目源集合(所有类型IEnumerable<IData>

    • TreeView一个用于(仅保存类别)的每个父节节点,aSectionCollection类型ISectionData

    • 每个类别一个,一个CategoryCollection类型IHeaderData

    • 单个用于设置数据和共享类别(标题数据),aSettingCollection类型IData

  • 逐节填充已排序的源集合

    • 将类型的节数据实例添加到的ISectionData源集合中SectionCollectionTreeView

    • 将类型的共享类别数据头实例添加IHeaderData到两个源集合中CategoryCollection,并且SettingCollection

    • 将 type 的设置实例添加ISettingData到唯一SettingCollection

    • 对当前部分的所有类别重复最后两个步骤

    • 将 分配给根节点CategoryCollection的子集合ISectionData

    • 对所有部分重复这些步骤(及其类别和相应的设置)

  • 将 绑定SectionCollection到 TreeView

  • 将 绑定SettingsCollectionLIstView

  • HierarchicalDataTemplateTreeView数据创建一个ISectionData类型为根的数据

  • 创建两个DataTemplate用于ListView

    • 一个目标IHeaderData

    • 一个目标ISettingData

逻辑:

  • 当选择 IHeaderData其中的一项时TreeView

    • ListView使用获取此数据项的项目容器var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)

    • 将容器滚动到视图中container.BringIntoView()(实现视图外的虚拟化项目)

    • 将容器滚动到视图顶部

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



查看完整回答
反对 回复 2023-07-09
  • 1 回答
  • 0 关注
  • 131 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信