猿问

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

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

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

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

白衣非少年
浏览 190回答 1
1回答

Cats萌萌

左侧的树(目录)具有根节点(例如“TextEditor”部分)。每个部分都包含设置类别(例如“格式”)。右侧ListView(设置视图)的项目具有组标题,其类别名称与目录名称相匹配(例如,格式)。1. 编辑以解决使用PropertyGroupDescription假设:在 a 内部存在一个CollectionViewSource定义 ResourceDictionary并命名为CollectionViewSource。设置数据项具有属性SettingsCategoryName(例如格式)。的SettingsCategoryName的SelectedItem绑定 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 changedprivate 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 elementprivate 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将 绑定SettingsCollection到LIstViewHierarchicalDataTemplate为TreeView数据创建一个ISectionData类型为根的数据创建两个DataTemplate用于ListView一个目标IHeaderData一个目标ISettingData逻辑:当选择 IHeaderData其中的一项时TreeViewListView使用获取此数据项的项目容器var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)将容器滚动到视图中container.BringIntoView()(实现视图外的虚拟化项目)将容器滚动到视图顶部因为TreeView和ListView共享相同的类别标题数据 ( IHeaderData),所以所选项目很容易跟踪和查找。您不必搜索设置组。您可以使用参考直接跳转到该组。这意味着数据的结构是解决方案的关键。
随时随地看视频慕课网APP
我要回答