在许多技术中,元素和组件被组织在树结构中,开发人员直接操作树中的对象节点,以影响应用程序的呈现或行为。 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 定义的 UI,树隐喻就足够相关,大多数 WPF 文档在引用此常规概念时将使用术语对象树。
逻辑树
在 WPF 中,通过设置返回这些元素的对象的属性,将内容添加到 UI 元素。 例如,通过操作Items属性将项添加到ListBox控件。 通过这样做,您将物品放入 Items 属性值中的 ItemCollection。 同样,若要向DockPanel添加对象,可以操作其 Children 属性值。 在这里,你要将对象添加到UIElementCollection。 有关代码示例,请参阅 “如何:动态添加元素”。
在可扩展应用程序标记语言(XAML)中,将列表项放在控件 ListBox 或其他 UI 元素中 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中的每个子对象元素都会通过解析器调用ItemCollection.Add
将其添加到ItemCollection中。 将 XAML 处理到对象树中的此示例似乎是一个示例,其中创建的对象树基本上是逻辑树。
但是,即使将 XAML 隐式语法项目排除在外,逻辑树也并不是运行时应用程序 UI 存在的整个对象图。其主要原因是视觉元素和模板。 例如,请考虑使用 Button. 逻辑树报告 Button 对象及其字符串 Content
。 但是,在运行时对象树中,这个按钮还有更多功能。 具体而言,按钮之所以能以特定方式显示在屏幕上,是因为应用了一个特定的 Button 控件模板。 来自已应用模板的视觉对象(如视觉按钮周围的深灰色模板定义 Border )不会在逻辑树中报告,即使你在运行时查看逻辑树(例如处理可见 UI 中的输入事件,然后读取逻辑树)。 若要查找模板视觉对象,需要检查可视化树。
有关 XAML 语法如何映射到所创建的对象图和 XAML 中的隐式语法的详细信息,请参阅 WPF 中的XAML 语法详细信息或 XAML。
逻辑树的用途
逻辑树存在,以便内容模型可以轻松循环访问其可能的子对象,以便内容模型可以扩展。 此外,逻辑树为某些通知提供框架,例如加载逻辑树中的所有对象时。 基本上,逻辑树是框架级别的运行时对象图的近似值,它不包括视觉对象,但足以满足许多针对你自己的运行时应用程序的构成的查询作。
此外,静态和动态资源引用是通过在逻辑树中向上查找初始请求对象的集合来解决的,然后继续沿着逻辑树往上,检查每个 FrameworkElement(或 FrameworkContentElement),看看是否有另一种 Resources
值,其中包含一个 ResourceDictionary,可能含有该键。 当逻辑树和可视化树都存在时,逻辑树用于资源查找。 有关资源字典和查找的详细信息,请参阅 XAML 资源。
逻辑树的构成
逻辑树是在 WPF 框架级别定义的,这意味着与逻辑树操作最相关的 WPF 基础元素是 FrameworkElement 或 FrameworkContentElement。 但是,当你实际使用 LogicalTreeHelper API 时,你会发现逻辑树有时会包含既不是 FrameworkElement 也不是 FrameworkContentElement 的节点。 例如,逻辑树报告 Text 字符串 TextBlock的值。
重写逻辑树
高级控件作者可以通过重写多个 API 来替代逻辑树,这些 API 定义常规对象或内容模型在逻辑树中添加或删除对象的方式。 有关如何重写逻辑树的示例,请参阅 “重写逻辑树”。
属性值继承
属性值继承通过混合树运行。 启用属性继承的 Inherits 属性所包含的实际元数据是 WPF 框架级的 FrameworkPropertyMetadata 类。 因此,保留原始值的父对象和继承该值的子对象必须是 FrameworkElement 或 FrameworkContentElement,并且它们都必须是某些逻辑树的一部分。 但是,对于支持属性继承的现有 WPF 属性,属性值继承可以通过不在逻辑树中的干预对象永久化。 这主要与模板元素使用在模板实例上或页面级更高层次的组合中设置的任何继承属性值相关,因此在逻辑树中具有更高层次。 为了使属性值继承能够一致地跨此类边界工作,继承属性必须注册为附加属性,并且如果要定义具有属性继承行为的自定义依赖属性,则应遵循此模式。 帮助程序类实用工具方法无法完全预测用于属性继承的确切树,即使在运行时也是如此。 有关详细信息,请参阅 属性值继承。
可视化树
除了逻辑树的概念外,WPF 中还有可视化树的概念。 可视化树描述由基类表示 Visual 的视觉对象结构。 为控件编写模板时,需要定义或重新定义适用于该控件的可视化树。 由于性能和优化原因,可视化树还对想要对绘图进行较低级别的控制的开发人员感兴趣。 作为常规 WPF 应用程序编程的一部分,显示可视化树时,路由事件的事件路由主要沿着可视化树传播,而不是逻辑树。 除非你是控件开发者,否则路由事件行为的微妙之处可能不会立即明显。 通过可视化树来路由事件,使在视觉层面实现合成的控件能够处理事件或创建事件设定器。
树、内容元素和内容主机
内容元素(派生自 ContentElement的类)不是可视化树的一部分;它们不继承, Visual 也没有可视表示形式。 为了在 UI 中显示,ContentElement 必须托管在内容主机中,并且该内容主机既是 Visual,又是逻辑树的参与者。 通常,此类对象是一个 FrameworkElement。 你可以将内容主机概念化为内容“浏览器”,并选择如何在主机控制的屏幕区域中显示该内容。 当内容被托管时,它可以参与通常与视觉树关联的某些树结构处理。 通常,主机类包括实现代码, FrameworkElement 这些实现代码通过内容逻辑树的子节点将任何托管 ContentElement 内容添加到事件路由中,即使托管内容不是真实可视化树的一部分。 这是必要的,以便ContentElement能够发起一个路由事件,该事件可以路由到除其自身以外的任何元素。
树遍历
该 LogicalTreeHelper 类提供逻辑树遍历的 GetChildren、 GetParent和 FindLogicalNode 方法。 在大多数情况下,你不必遍历现有控件的逻辑树,因为这些控件几乎总是将其逻辑子元素公开为支持集合访问的专用集合属性,例如 Add
,索引器等。 树遍历主要是一种用于选择不从预期控件模式(例如 ItemsControl 或 Panel)派生的控件作者的场景,这些控件模式已定义集合属性,这些作者打算提供自己的集合属性支持。
可视化树还支持用于遍历的帮助程序类 VisualTreeHelper。 可视化树没有通过控件特定的属性方便地公开,因此如果在您的编程场景中需要遍历可视化树,建议使用类VisualTreeHelper。 有关详细信息,请参阅 WPF 图形呈现概述。
注释
有时需要检查已应用的模板的可视化树。 使用此技术时,应小心。 即使你正在遍历用于定义模板的控件的可视化树,控件的使用者也可以始终通过对实例设置 Template 属性来更改模板,甚至最终用户也可以通过更改系统主题来影响应用的模板。
路由事件的路径像“树”结构
如前所述,任何给定的路由事件都会在一条已预先确定的单一路径上沿树行进,这棵树是可视树和逻辑树表示的混合体。 事件路由可以沿着树向上或向下传递,具体取决于它是隧道传递还是冒泡传递的路由事件。 事件路由概念没有直接支持的辅助类,该类可在不依赖于引发事件的情况下“遍历”事件路由。 有一个表示路由的类, EventRoute但该类的方法通常仅供内部使用。
资源字典和树
页中定义的所有 Resources
字典资源查找通常从逻辑树逐层进行。 不在逻辑树中的对象可以引用键状资源,但资源查找序列从该对象连接到逻辑树的点开始。 在 WPF 中,只有逻辑树节点可以拥有一个包含 ResourceDictionary 的 Resources
属性,因此没有必要遍历可视化树来从 ResourceDictionary 中查找带键的资源。
但是,资源查找还可以扩展到直接逻辑树之外。 对于应用程序标记,资源查找可以继续转到应用程序级资源字典,然后转到主题支持和系统值,这些值被引用为静态属性或键。 如果资源引用是动态的,主题本身还可以引用主题逻辑树之外的系统值。 有关资源字典和查找逻辑的详细信息,请参阅 XAML 资源。