WPF 中的树
在许多技术中,元素和组件都按树结构的形式组织。在这种结构中,开发人员可以直接操作树中的对象节点来影响应用程序的绘制或行为。 Windows Presentation Foundation (WPF) 也使用了若干树结构形式来定义程序元素之间的关系。 多数情况下,在概念层面考虑对象树形式时,WPF 开发人员会用代码创建应用程序,或用 XAML 定义应用程序的组成部分,但他们会调用具体的 API 或使用特定的标记来执行此操作,而不是像在 XML DOM 中那样,使用某些常规对象树操作 API。 WPF 公开提供树形式视图的两个帮助程序类:LogicalTreeHelper 和 VisualTreeHelper。 WPF 文档中还使用了“可视化树”和“逻辑树”两个术语,它们有助于理解某些关键 WPF 功能的行为。 本主题定义可视化树和逻辑树的含义,讨论这些树与总体对象树概念之间的关系,并介绍 LogicalTreeHelper 和 VisualTreeHelper。
WPF 中的树
WPF 中,最完整的树结构是对象树。 如果在 XAML 中定义一个应用程序页,然后加载 XAML,将根据标记中元素之间的嵌套关系来创建树结构。 如果使用代码定义应用程序或应用程序的一部分,则将根据为属性(属性实现给定对象的内容模型)分配属性值的方式来创建树结构。 在 WPF 中,完整的对象树可通过两种方式进行概念化并报告给其公共 API:作为逻辑树和作为可视化树。 逻辑树与可视化树之间的区别不一定重要,但在某些 WPF 子系统中它们偶尔可能会导致问题,并影响你对标记或代码的选择。
尽管你并不会总是直接操作逻辑树或可视化树,但理解它们之间的关系有助于你从技术角度了解 WPF。 若要理解 WPF 中属性继承和事件路由的工作原理,将 WPF 视为某种树形式也相当重要。
注意
因为对象树更像是概念,而不像是实际 API,所以还可以将此概念视为对象图。 实际上,在运行时,对象之间的某些关系不能由树形式表示。 尽管如此,树形式的相关性还是很强,尤其是对于 XAML 定义的 UI。因此,大多数 WPF 文档在引用这个常见概念时,仍使用术语“对象树”。
逻辑树
在 WPF 中,通过为支持 UI 元素的对象设置属性,可以向这些 UI 元素添加内容。 例如,通过操作 ListBox 控件的 Items 属性,可以将项添加到该控件。 通过这种方法,可以将项放入用作 Items 属性值的 ItemCollection 中。 同样,通过操作 DockPanel 的 Children 属性值,可以将对象添加到该控件中。 这里,你将对象添加到 UIElementCollection 中。 有关代码示例,请参阅如何:动态添加元素。
在 Extensible Application Markup Language (XAML) 中,当在 ListBox 中放置列表项或在 DockPanel 中放置控件或其他 UI 元素时,还会显式或隐式使用 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
。 不过,它确实有一个名为 Items
的 ListBox 属性,其中包含一个 ItemCollection,并且在处理 ListBox XAML 时,ItemCollection 会进行初始化,但是为空。 然后,作为 ListBox 的内容存在的每个子对象元素,都将通过对 ItemCollection.Add
的分析程序调用,添加到 ItemCollection 中。 此示例将 XAML 处理成对象树,目前这似乎表明所创建的对象树基本上是逻辑树。
不过,即使不考虑 XAML 隐式语法项,该逻辑树也不是应用程序 UI 在运行时存在的整个对象图。主要原因是视觉对象和模板。 例如,考虑 Button。 逻辑树报告 Button 对象及其字符串 Content
。 但在运行时对象树中,此按钮还有更多内容。 具体而言,该按钮在屏幕上仅显示为现在这样,是因为应用了特定的 Button 控件模板。 逻辑树不会报告来自所应用模板的视觉对象,例如可视化按钮周围由模板定义的深灰色的 Border。即使在运行时查看逻辑树(例如,处理来自可见 UI 的输入事件,然后读取逻辑树),也是如此。 若要查找模板视觉对象,需要改为检查可视化树。
有关 XAML 语法如何映射到所创建的对象图,以及 XAML 中隐式语法的详细信息,请参阅 XAML 语法详述或 WPF 中的 XAML。
逻辑树用途
借助逻辑树,内容模型可以方便地循环访问其可能的子对象,从而实现扩展。 此外,逻辑树还为某些通知提供框架,例如在加载逻辑树中的所有对象时。 基本上,逻辑树是框架级别的近似运行时对象图(排除了视觉对象),但其足以用于对你自己的运行时应用程序组合执行多种查询操作。
此外,静态和动态资源引用具有相同的解析过程:针对最初发出请求的对象,沿逻辑树向上查找 Resources 集合,然后沿逻辑树继续向上,检查每一个 FrameworkElement 或 FrameworkContentElement,以查找另一个包含 ResourceDictionary(可能包含该键)的 Resources
值。 当同时存在逻辑树和可视化树时,将使用逻辑树进行资源查找。 有关资源字典和查找的详细信息,请参见 XAML 资源。
逻辑树的构成
逻辑树在 WPF 框架级别定义。这意味着,与逻辑树操作关系最密切的 WPF 基元素是 FrameworkElement 或 FrameworkContentElement。 但是你会发现,如果实际使用 LogicalTreeHelper API,则逻辑树有时会包含既不是 FrameworkElement,也不是 FrameworkContentElement 的节点。 例如,逻辑树会报告 TextBlock 的 Text 值,该值是一个字符串。
替代逻辑树
经验丰富的控件作者会通过替代若干 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 资源。