是否可以以编程方式滚动 WPF ListView,以便将所需的分组 header 放置在其顶部?
Is it possible to programmatically scroll a WPF ListView so that a desired grouping header is placed at its top?
如果 ListView
绑定到已使用 PropertyGroupDescription
分组的项目,是否可以通过编程方式滚动以便将组置于列表顶部?我知道我可以滚动到组中的第一个项目,因为该项目属于 ListView
绑定到的 collection。但是,我找不到任何描述如何滚动到组 header(样式为 GroupStyle
)的资源。
为了举例说明所需的功能,让我们看一下 Visual Studio 代码 中的设置页面。此页面包含一个面板,允许用户滚动浏览所有应用程序的设置(组织在各自的组下)以及左侧的树结构,以便更快地导航到主面板中的特定组。在所附的屏幕截图中,我单击了左侧树中的 Formatting 选项,主面板自动滚动,因此相应的组 header 位于顶部主面板。
如何在 WPF 中重新创建它(如果可能的话)? Visual Studio 代码 中主设置面板的"infinite" 滚动是否可以用另一个 WPF 控件模仿?
左边的树(TOC)有根节点(部分例如 'TextEditor')。每个部分都包含设置类别(例如 'Formatting')。右侧的 ListView
(设置视图)中的项目的组 header 的类别名称与 TOC 的类别名称相匹配(例如格式)。
1。编辑以解决 PropertyGroupDescription
的使用
假设:
- 存在一个
CollectionViewSource
定义在一个
ResourceDictionary
并命名为 CollectionViewSource.
- 设置数据项有属性
SettingsCategoryName
(例如格式化)。
SettingsCategoryName
的SelectedItem
的
TreeView
绑定到 属性 SelectedSettingsCategoryName
View.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
派生类型中。然后将 CommandBindings
添加到处理路由命令的新自定义 ListView
,例如ScrollToSectionRoutedCommand
。将 TreeViewItems
模板化为 Button
并让他们发出命令以将部分名称作为 CommandParameter
传递给自定义 ListView
.
备注
由于使用 PropertyGroupDescription
导致混合数据类型的项目源(GroupItemData
组 headers 以及实际数据项) UI 的虚拟化托管 ItemsControl
已禁用且不可能(请参阅 Microsoft Docs:
Optimizing performance: Controls)。在这种情况下,附加的 属性 ScrollViewer.CanContentScroll
会自动设置为 False
(强制)。对于大列表,这可能是一个巨大的缺点,也是采用替代方法的原因。
2。替代解决方案(具有 UI 虚拟化支持)
实际设置结构的设计有多种可能的变化。它可以是一棵树,其中每个类别 header 节点都有自己的 child 节点,代表类别的设置或平面列表结构,其中类别 header 和设置都是兄弟。为了示例的简单起见,我选择了第二个选项:平面列表数据结构。
2.1 设置
基本思路:
TreeView
使用具有两个级别的 HierarchicalDataTemplate
进行模板化。 TreeView
(叶子)和 ListView
的第二层共享 header 项的相同实例(IHeaederData
。见后)。因此,TreeView
的所选 header 项目引用了 ListView
中的 完全相同的 项目 header - 无需搜索。
实施概述:
- 您需要两个
ItemsControl
元素:
- 一个
TreeView
用于左侧的两级导航窗格
- 带有节根节点(例如'Text Editor')
- 和该部分的设置类别header child节点(叶节点)(例如'Font'、'Formatting')
- 一个
ListView
用于实际设置及其类别 headers。
- 然后设计数据类型来表示一个设置,一个设置header和一个节根节点
- 让他们都实现一个具有共享属性的公共
IData
(例如 header)
- 让设置header数据类型实现一个额外的
IHeaderData
- 让设置数据类型实现额外的
ISettingData
- 让
TreeView
的 parent 部分节点数据类型(根节点)实现一个额外的 ISectionData
,它具有 children 类型 IHeaderData
- 创建项目源 collections(所有类型
IEnumerable<IData>
)
TreeView
(仅包含类别)的每个 parent 节节点一个,SectionCollection
类型 ISectionData
- 每个类别一个,
CategoryCollection
类型 IHeaderData
- 一个用于设置数据和共享类别(header 数据),一个
SettingCollection
类型 IData
- 逐段填充已排序的源 collections
- 将
ISectionData
类型的节数据实例添加到 TreeView
的源 collection SectionCollection
- 将类型为
IHeaderData
的共享类别数据 header 实例添加到源 collections CategoryCollection
和 SettingCollection
- 仅将
ISettingData
类型的设置实例添加到 SettingCollection
,每个类别的设置一个
- 对当前部分的所有类别重复最后两个步骤
- 将
CategoryCollection
赋值给ISectionData
根节点的childcollection
- 对所有部分重复这些步骤(及其类别和相应的设置)
- 将
SectionCollection
绑定到 TreeView
- 将
SettingsCollection
绑定到 LIstView
- 为
TreeView
数据创建一个 HierarchicalDataTemplate
,其中 ISectionData
类型是根
- 为
ListView
创建两个 DataTemplate
- 针对
IHeaderData
- 一个针对
ISettingData
逻辑:
- 当
TreeView
的 IHeaderData
项被选中时
- 使用
var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)
获取此数据项的ListView
项容器
- 将容器滚动到视图中
container.BringIntoView()
(实现看不见的虚拟化物品)
- 将容器滚动到视图顶部
因为 TreeView
和 ListView
共享相同的类别 header 数据 (IHeaderData
),所以所选项目很容易跟踪和查找。您不必搜索设置组。您可以使用引用直接跳转到该组。
这意味着数据的结构是解决方案的关键。
如果 ListView
绑定到已使用 PropertyGroupDescription
分组的项目,是否可以通过编程方式滚动以便将组置于列表顶部?我知道我可以滚动到组中的第一个项目,因为该项目属于 ListView
绑定到的 collection。但是,我找不到任何描述如何滚动到组 header(样式为 GroupStyle
)的资源。
为了举例说明所需的功能,让我们看一下 Visual Studio 代码 中的设置页面。此页面包含一个面板,允许用户滚动浏览所有应用程序的设置(组织在各自的组下)以及左侧的树结构,以便更快地导航到主面板中的特定组。在所附的屏幕截图中,我单击了左侧树中的 Formatting 选项,主面板自动滚动,因此相应的组 header 位于顶部主面板。
如何在 WPF 中重新创建它(如果可能的话)? Visual Studio 代码 中主设置面板的"infinite" 滚动是否可以用另一个 WPF 控件模仿?
左边的树(TOC)有根节点(部分例如 'TextEditor')。每个部分都包含设置类别(例如 'Formatting')。右侧的 ListView
(设置视图)中的项目的组 header 的类别名称与 TOC 的类别名称相匹配(例如格式)。
1。编辑以解决 PropertyGroupDescription
的使用
假设:
- 存在一个
CollectionViewSource
定义在一个ResourceDictionary
并命名为 CollectionViewSource. - 设置数据项有属性
SettingsCategoryName
(例如格式化)。 SettingsCategoryName
的SelectedItem
的TreeView
绑定到 属性SelectedSettingsCategoryName
View.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
派生类型中。然后将 CommandBindings
添加到处理路由命令的新自定义 ListView
,例如ScrollToSectionRoutedCommand
。将 TreeViewItems
模板化为 Button
并让他们发出命令以将部分名称作为 CommandParameter
传递给自定义 ListView
.
备注
由于使用 PropertyGroupDescription
导致混合数据类型的项目源(GroupItemData
组 headers 以及实际数据项) UI 的虚拟化托管 ItemsControl
已禁用且不可能(请参阅 Microsoft Docs:
Optimizing performance: Controls)。在这种情况下,附加的 属性 ScrollViewer.CanContentScroll
会自动设置为 False
(强制)。对于大列表,这可能是一个巨大的缺点,也是采用替代方法的原因。
2。替代解决方案(具有 UI 虚拟化支持)
实际设置结构的设计有多种可能的变化。它可以是一棵树,其中每个类别 header 节点都有自己的 child 节点,代表类别的设置或平面列表结构,其中类别 header 和设置都是兄弟。为了示例的简单起见,我选择了第二个选项:平面列表数据结构。
2.1 设置
基本思路:
TreeView
使用具有两个级别的 HierarchicalDataTemplate
进行模板化。 TreeView
(叶子)和 ListView
的第二层共享 header 项的相同实例(IHeaederData
。见后)。因此,TreeView
的所选 header 项目引用了 ListView
中的 完全相同的 项目 header - 无需搜索。
实施概述:
- 您需要两个
ItemsControl
元素:- 一个
TreeView
用于左侧的两级导航窗格- 带有节根节点(例如'Text Editor')
- 和该部分的设置类别header child节点(叶节点)(例如'Font'、'Formatting')
- 一个
ListView
用于实际设置及其类别 headers。
- 一个
- 然后设计数据类型来表示一个设置,一个设置header和一个节根节点
- 让他们都实现一个具有共享属性的公共
IData
(例如 header) - 让设置header数据类型实现一个额外的
IHeaderData
- 让设置数据类型实现额外的
ISettingData
- 让
TreeView
的 parent 部分节点数据类型(根节点)实现一个额外的ISectionData
,它具有 children 类型IHeaderData
- 让他们都实现一个具有共享属性的公共
- 创建项目源 collections(所有类型
IEnumerable<IData>
)TreeView
(仅包含类别)的每个 parent 节节点一个,SectionCollection
类型ISectionData
- 每个类别一个,
CategoryCollection
类型IHeaderData
- 一个用于设置数据和共享类别(header 数据),一个
SettingCollection
类型IData
- 逐段填充已排序的源 collections
- 将
ISectionData
类型的节数据实例添加到TreeView
的源 collection - 将类型为
IHeaderData
的共享类别数据 header 实例添加到源 collectionsCategoryCollection
和SettingCollection
- 仅将
ISettingData
类型的设置实例添加到SettingCollection
,每个类别的设置一个
- 对当前部分的所有类别重复最后两个步骤
- 将
CategoryCollection
赋值给ISectionData
根节点的childcollection - 对所有部分重复这些步骤(及其类别和相应的设置)
SectionCollection
- 将
- 将
SectionCollection
绑定到TreeView
- 将
SettingsCollection
绑定到LIstView
- 为
TreeView
数据创建一个HierarchicalDataTemplate
,其中ISectionData
类型是根 - 为
ListView
创建两个DataTemplate
- 针对
IHeaderData
- 一个针对
ISettingData
- 针对
逻辑:
- 当
TreeView
的IHeaderData
项被选中时- 使用
var container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)
获取此数据项的 - 将容器滚动到视图中
container.BringIntoView()
(实现看不见的虚拟化物品) - 将容器滚动到视图顶部
ListView
项容器 - 使用
因为 TreeView
和 ListView
共享相同的类别 header 数据 (IHeaderData
),所以所选项目很容易跟踪和查找。您不必搜索设置组。您可以使用引用直接跳转到该组。
这意味着数据的结构是解决方案的关键。