Поделиться через


Деревья в WPF

Во многих технологиях элементы и компоненты организованы в структуре дерева, где разработчики напрямую управляют узлами объектов в дереве, чтобы повлиять на отрисовку или поведение приложения. Windows Presentation Foundation (WPF) также использует несколько метафор дерева для определения связей между элементами программы. В большинстве случаев разработчики WPF могут создавать приложение в коде или определять части приложения в XAML, думая концептуально о метафоре дерева объектов, но будет вызывать конкретный API или использовать конкретную разметку для этого, а не некоторые общие API обработки дерева объектов, например в XML DOM. WPF предоставляет два вспомогательных класса, которые предоставляют представление метафоры дерева, LogicalTreeHelper и VisualTreeHelper. Термины визуального дерева и логического дерева также используются в документации WPF, так как эти же деревья полезны для понимания поведения определенных ключевых функций WPF. В этом разделе определяется представление визуального дерева и логического дерева, описывается, как такие деревья связаны с общей концепцией дерева объектов и вводятся LogicalTreeHelper и VisualTreeHelper.

Деревья в WPF

Самая полная структура дерева в WPF — это дерево объектов. Если вы определяете страницу приложения с помощью XAML, а затем загружаете XAML, структура дерева создается на основе отношений вложенности элементов в разметке. Если вы определяете приложение или часть приложения в коде, то структура дерева создается на основе назначения значений свойств для свойств, реализующих модель содержимого для данного объекта. В WPF существуют два способа, с помощью которых полное дерево объектов концептуально может быть передано в его общедоступный API: как логическое дерево и как визуальное дерево. Различия между логическим деревом и визуальным деревом не всегда важны, но иногда могут вызывать проблемы с определенными подсистемами WPF и влиять на выбор в разметке или коде.

Даже если вы не всегда управляете логическим деревом или визуальным деревом напрямую, понимание концепций взаимодействия деревьев полезно для понимания WPF как технологии. Представление WPF в виде некой древовидной структуры является ключевым для понимания того, как работают наследование свойств и маршрутизация событий в WPF.

Заметка

Поскольку дерево объектов является более концепцией, чем фактическим API, другой способ рассматривать концепцию как граф объектов. На практике во время выполнения между объектами существуют такие отношения, где метафора дерева разрушится. Тем не менее, особенно с определяемым XAML пользовательским интерфейсом, метафора дерева достаточно актуальна, что большинство документации WPF будет использовать дерево объектов терминов при ссылке на эту общую концепцию.

Логическое дерево

В WPF вы добавляете содержимое в элементы пользовательского интерфейса, задав свойства объектов, которые поддерживают эти элементы. Например, вы добавляете элементы в элемент управления ListBox путем управления его свойством Items. При этом элементы помещаются в ItemCollection, которое является значением свойства Items. Аналогичным образом, чтобы добавить объекты в DockPanel, вы изменяете его значение свойства Children. Здесь вы добавляете объекты в UIElementCollection. Пример кода см. в разделе Как динамически добавить элемент.

В языке разметки расширяемых приложений (XAML) при размещение элементов списка в ListBox или элементах управления или других элементов пользовательского интерфейса в DockPanelтакже используются свойства Items и Children, явным образом или неявно, как показано в следующем примере.

<DockPanel
  Name="ParentElement"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  >
  <!--implicit: <DockPanel.Children>-->
  <ListBox DockPanel.Dock="Top">
    <!--implicit: <ListBox.Items>-->
    <ListBoxItem>
      <TextBlock>Dog</TextBlock>
    </ListBoxItem>
    <ListBoxItem>
      <TextBlock>Cat</TextBlock>
    </ListBoxItem>
    <ListBoxItem>
      <TextBlock>Fish</TextBlock>
    </ListBoxItem>
  <!--implicit: </ListBox.Items>-->
  </ListBox>
  <Button Height="20" Width="100" DockPanel.Dock="Top">Buy a Pet</Button>
  <!--implicit: </DockPanel.Children>-->
</DockPanel>

Если бы вы обрабатывали этот КОД XAML как XML в объектной модели документа, и если вы включили теги, закомментированные как неявные (которые были бы законными), то результирующее дерево XML DOM включало бы элементы для <ListBox.Items> и других неявных элементов. Но XAML не работает таким образом: при чтении разметки и записи в объекты результирующий граф объектов в результате буквально не включает ListBox.Items. Однако оно имеет свойство ListBox с именем Items, которое содержит ItemCollection, а ItemCollection инициализировано, но пусто при обработке XAML ListBox. Затем каждый дочерний элемент объекта, являющийся содержимым для ListBox, добавляется в ItemCollection через вызовы синтаксического анализа к ItemCollection.Add. Этот пример обработки XAML в дереве объектов до сих пор является примером, когда созданное дерево объектов в основном является логическим деревом.

Однако логическое дерево — это не весь граф объектов, который существует для пользовательского интерфейса приложения во время выполнения, даже с учетом неявных элементов синтаксиса XAML. Основной причиной этого являются визуальные элементы и шаблоны. Например, рассмотрим Button. Логическое дерево сообщает объект Button, а также строку Content. Но в дереве объектов, доступном во время выполнения, эта кнопка имеет дополнительные особенности. В частности, кнопка отображается на экране именно так из-за применения определенного контрольного шаблона Button. Визуальные элементы из примененного шаблона (например, определённый шаблоном тёмно-серый цвет (Border) вокруг визуальной кнопки) не отображаются в логическом дереве, даже если вы просматриваете логическое дерево во время выполнения (например, при обработке входного события из видимого пользовательского интерфейса и последующем чтении логического дерева). Чтобы найти визуальные элементы шаблона, вместо этого необходимо проверить визуальное дерево.

Дополнительные сведения о том, как синтаксис XAML сопоставляется с созданной диаграммой объектов, а также о неявном синтаксисе в XAML, см. в разделе XAML Syntax In Detail или в разделе XAML в WPF.

Назначение логического дерева

Логическое дерево существует для того, чтобы модели контента могли легко итерировать свои возможные дочерние объекты, а также быть расширяемыми. Кроме того, логическое дерево предоставляет платформу для определенных уведомлений, например при загрузке всех объектов в логическом дереве. По сути, логическое дерево представляет собой приближение графа объектов времени выполнения на уровне фреймворка, которое исключает визуальные элементы, но подходит для многих операций запроса в отношении композиции вашего приложения времени выполнения.

Кроме того, ссылки на статические и динамические ресурсы разрешаются путем поиска вверх по логической структуре дерева для коллекций Resources на исходном объекте запроса, а затем продолжая поиск вверх по логическому дереву и проверяя каждое FrameworkElement (или FrameworkContentElement) на наличие другого значения Resources, которое содержит ResourceDictionary, возможно, содержащего этот ключ. Логическое дерево используется для поиска ресурсов, когда присутствует как логическое дерево, так и визуальное дерево. Дополнительные сведения о словарях ресурсов и поиске см. в ресурсах XAML.

Композиция логического дерева

Логическое дерево определяется на уровне платформы WPF, что означает, что базовый элемент WPF, наиболее подходящий для логических операций дерева, — FrameworkElement или FrameworkContentElement. Если вы действительно используете API LogicalTreeHelper, как вы можете видеть, логическое дерево иногда содержит узлы, которые не являются ни FrameworkElement, ни FrameworkContentElement. Например, логическое дерево сообщает значение Text для TextBlock, которое является строкой.

Переопределение логического дерева

Авторы расширенных элементов управления могут переопределить логическое дерево, переопределив несколько API, определяющих, как общий объект или модель содержимого добавляет или удаляет объекты в логическом дереве. См. раздел Переопределение логического деревадля примера переопределения логического дерева.

Наследование значений свойства

Наследование значений свойств выполняется через гибридное дерево. Фактические метаданные, содержащие свойство Inherits, которое включает наследование свойств, является классом FrameworkPropertyMetadata платформы WPF. Поэтому родительский объект, содержащий исходное значение, и дочерний объект, наследующий это значение, должны быть FrameworkElement или FrameworkContentElement, и они должны быть частью логического дерева. Однако для существующих свойств WPF, поддерживающих наследование свойств, наследование значений свойств может передаваться через промежуточный объект, который не входит в логическое дерево. В первую очередь это важно для того, чтобы элементы шаблона использовали все унаследованные значения свойств, заданные в экземпляре, к которому применяется шаблон, или на более высоких уровнях композиции страницы и, соответственно, выше в логическом дереве. Чтобы наследование значений свойств работало последовательно в такой границе, наследующее свойство должно быть зарегистрировано как присоединенное свойство, и следует следовать этому шаблону, если вы планируете определить настраиваемое свойство зависимостей с поведением наследования свойств. Точное дерево, используемое для наследования свойств, не может быть полностью предвидено вспомогательным методом служебной программы класса, даже во время выполнения. Дополнительные сведения см. в разделе Наследование значений свойств.

Визуальное дерево

Помимо концепции логического дерева, существует также концепция визуального дерева в WPF. Визуальное дерево описывает структуру визуальных объектов, представленных базовым классом Visual. При написании шаблона для элемента управления вы определяете или переопределяете визуальное дерево, которое применяется к этому элементу управления. Визуальное дерево также интересно разработчикам, которым требуется более низкий уровень контроля над рисованием по соображениям производительности и оптимизации. Одна из экспозиций визуального дерева в рамках обычного программирования приложений WPF заключается в том, что маршруты событий для перенаправленного события в основном перемещаются по визуальному дереву, а не по логическому дереву. Эта тонкость поведения маршрутизируемого события может сразу не быть очевидна, если вы не являетесь автором элемента управления. События маршрутизации через визуальное дерево позволяют элементам управления, реализующим композицию на визуальном уровне, обрабатывать события или создавать методы задания событий.

Деревья, элементы содержимого и хосты содержимого

Элементы содержимого (классы, производные от ContentElement) не являются частью визуального дерева; они не наследуются от Visual и не имеют визуального представления. Для отображения в пользовательском интерфейсе, ContentElement должен быть размещен в узле содержимого, который одновременно является и Visual, и участником логического дерева. Обычно такой объект является FrameworkElement. Вы можете представить себе, что узел содержимого отчасти похож на "браузер" для содержимого и выбирает способ отображения содержимого в области экрана, которым управляет узел. При размещении содержимого его можно задействовать в определённых процессах, которые обычно относятся к визуальному дереву. Как правило, класс узла FrameworkElement включает код реализации, который добавляет любой размещенный ContentElement в маршрут событий через подмносы логического дерева контента, даже если размещенное содержимое не является частью истинного визуального дерева. Это необходимо, чтобы ContentElement могли создавать перенаправленное событие, которое направляется к любому элементу, отличному от самого себя.

Обход дерева

Класс LogicalTreeHelper предоставляет методы GetChildren, GetParentи FindLogicalNode для обхода логического дерева. В большинстве случаев не нужно проходить по логическому дереву существующих элементов управления, так как эти элементы управления почти всегда предоставляют свои логические дочерние элементы в качестве выделенного свойства коллекции, которое поддерживает доступ к коллекции, например Add, индексатор и т. д. Обход дерева главным образом используется авторами элементов управления, которые выбирают не наследовать от предполагаемых шаблонов элементов управления, таких как ItemsControl или Panel, где свойства коллекции уже определены, и которые намерены обеспечить поддержку для своих свойств коллекции.

Визуальное дерево также поддерживает вспомогательный класс для обхода визуального дерева VisualTreeHelper. Визуальное дерево не предоставляется столь удобно через свойства, специфичные для элемента управления, поэтому рекомендуется использовать класс VisualTreeHelper для обхода визуального дерева, если это необходимо в вашем сценарии программирования. Дополнительные сведения см. в обзоре отрисовки графики WPF.

Заметка

Иногда необходимо изучить визуальное дерево примененного шаблона. При использовании этого метода следует быть осторожным. Даже если вы просматриваете визуальное дерево для элемента управления, где определяется шаблон, потребители элемента управления всегда могут изменить шаблон, задав свойство Template для экземпляров, и даже конечный пользователь может повлиять на примененный шаблон, изменив системную тему.

Маршруты для маршрутизируемых событий в виде «дерева»

Как упоминалось ранее, маршрут любого заданного маршрутного события проходит по одному и предопределенному пути дерева, который является гибридом визуального и логического представления дерева. Маршрут событий может перемещаться вверх или вниз по дереву в зависимости от того, является ли он туннелирующим или всплывающим событием. Концепция маршрута событий не имеет непосредственно поддерживающего помощника, который можно использовать для "прохождения по маршруту событий" независимо от поднятия события, которое фактически маршрутизируется. Существует класс, представляющий маршрут, EventRoute, но методы этого класса обычно предназначены только для внутреннего использования.

Словари ресурсов и деревья

Поиск в словаре ресурсов для всех Resources, определенных на странице, в основном совершается путем обхода логического дерева. Объекты, которые не находятся в логическом дереве, могут ссылаться на ключевые ресурсы, но последовательность поиска ресурсов начинается в точке, где этот объект подключен к логическому дереву. В WPF только логические узлы дерева могут иметь свойство Resources, содержащее ResourceDictionary, поэтому обход визуального дерева в поисках ключевых ресурсов из ResourceDictionaryне имеет смысла.

Однако поиск ресурсов также может расшириться за рамки немедленного логического дерева. Для разметки приложения поиск ресурсов может продолжаться в словарях ресурсов на уровне приложения, затем к поддержке тем и системным значениям, которые упоминаются как статические свойства или ключи. Сами темы также могут ссылаться на системные значения за пределами логического дерева темы, если ссылки на ресурсы являются динамическими. Дополнительные сведения о словарях ресурсов и логике подстановки см. в разделе Ресурсы XAML.

См. также