Можно ли программно прокрутить WPF ListView так, чтобы желаемый заголовок группировки был помещен в его верхнюю часть?
Учитывая ListView
привязаны к элементам, которые были сгруппированы с использованием PropertyGroupDescription
Можно ли программно прокрутить так, чтобы группа была в верхней части списка? Я знаю, что могу прокрутить до первого элемента в группе, так как этот элемент принадлежит коллекции ListView
связан с. Однако я не смог найти никаких ресурсов, описывающих, как прокрутить заголовок группы (в стиле GroupStyle
).
Чтобы привести пример желаемой функциональности, давайте посмотрим на страницу настроек в Visual Studio Code. Эта страница состоит из панели, которая позволяет пользователю прокручивать все настройки приложения (организованные по соответствующим группам), а также древовидную структуру слева для более быстрой навигации к определенной группе на главной панели. На прикрепленном снимке экрана я щелкнул параметр " Форматирование" в дереве слева, и главная панель автоматически прокрутилась так, чтобы соответствующий заголовок группы был помещен в верхней части главной панели.
Как это можно воссоздать в WPF (если это вообще возможно)? Может ли "бесконечная" прокрутка панели основных настроек в коде Visual Studio имитироваться другим элементом управления WPF?
1 ответ
Дерево слева (оглавление) имеет корневые узлы (разделы, например, "TextEditor"). Каждый раздел содержит категории настроек (например, "Форматирование"). ListView
справа (вид настроек) есть элементы, которые имеют заголовок группы с именами категорий, совпадающими с названиями оглавления (например, Форматирование).
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
для заголовков групп и, кроме того, фактических элементов данных) виртуализация пользовательского интерфейса хостинга ItemsControl
отключен и невозможен (см. Документы Microsoft: Оптимизация производительности: Элементы управления). В этом сценарии прикрепленное свойство ScrollViewer.CanContentScroll
автоматически устанавливается на False
(Принудительная). Для большого списка это может быть огромным недостатком и причиной для альтернативного подхода.
2. Альтернативное решение (с поддержкой виртуализации пользовательского интерфейса)
Есть несколько возможных вариантов, когда речь идет о дизайне структуры фактических настроек. Это может быть дерево, в котором каждый узел заголовка категории имеет свои собственные дочерние узлы, которые представляют настройки категории, или структуру плоского списка, где заголовки и настройки категорий - это все одноуровневые элементы. Для простоты примера я выбрал второй вариант: структура данных с плоским списком.
2.1 Настройка
Основная идея:
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
в исходную коллекциюSectionCollection
изTreeView
- добавить экземпляр типа заголовка данных общей категории
IHeaderData
в обе исходные коллекцииCategoryCollection
а такжеSettingCollection
- добавить настройки экземпляры типа
ISettingData
по одному на каждую настройку категории,SettingCollection
только - повторите два последних шага для всех категорий текущего раздела
- назначить
CategoryCollection
к детской коллекцииISectionData
корневой узел - повторите шаги для всех разделов (с его категориями и соответствующими настройками)
- добавить экземпляр данных раздела типа
- Привязать
SectionCollection
кTreeView
- Привязать
SettingsCollection
кLIstView
- Создать
HierarchicalDataTemplate
дляTreeView
данные гдеISectionData
тип является корнем - Создать два
DataTemplate
дляListView
- тот, который нацелен
IHeaderData
- тот, который нацелен
ISettingData
- тот, который нацелен
Логика:
- Когда
IHeaderData
предмет изTreeView
затем выбирается- получить
ListView
контейнер элемента этого элемента данных, используяvar container = ItemsContainerGenerator.GetContainerFromItem(selectedTreeViewCategoryItem)
- Прокрутите контейнер в поле зрения
container.BringIntoView()
(для реализации виртуализированных предметов, которые находятся вне поля зрения) - Прокрутите контейнер до верхней части представления.
- получить
Потому что TreeView
а также ListView
использовать одни и те же данные заголовка категории (IHeaderData
) выбранные элементы легко отслеживать и находить. Вам не нужно искать группу настроек. Вы можете напрямую перейти к группе, используя ссылку. Это означает, что структура данных является ключом решения.