Можно ли программно прокрутить 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) выбранные элементы легко отслеживать и находить. Вам не нужно искать группу настроек. Вы можете напрямую перейти к группе, используя ссылку. Это означает, что структура данных является ключом решения.

Другие вопросы по тегам