Деревья в 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 инициализируется, но остается пустой, когда обрабатывается список ListBox в XAML. Теперь каждый дочерний элемент, который существует в роли содержимого ListBox, добавляется в ItemCollection, путем вызова средства синтаксического анализа к методу ItemCollection.Add. Этот пример обработки XAML в дереве объектов на первый взгляд похож на пример, в котором созданное дерево объектов, по сути, является логическим деревом.

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

Дополнительные сведения о сопоставлении синтаксиса XAML с созданным графом объекта и неявным синтаксисом в XAML см. в разделе Подробное описание синтаксиса XAML или Общие сведения о языке XAML в WPF.

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

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

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

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

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

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

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

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

Наследование значения свойств действует через гибридное дерево. Класс WPF FrameworkPropertyMetadata уровня платформы представляет собой фактические метаданные, содержащие свойство Inherits, которое обеспечивает наследование свойств. Таким образом, и родительский объект, содержащий исходное значение, и дочерний объект, наследующий это значение, должны быть 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.

См. также