本章内容
l 逻辑树与可视树
l 依赖属性
l 路由事件
我们即将完成本书的第一部分,在开始一些真正有趣的话题之前,回顾一下之前介绍的一些主要概念是很有用的,这也是.NET程序员们所不熟悉的部分。本章中的一些主题是WPF陡峭学习曲线中最主要的东西。熟悉了这些概念,你将能够很自信地学习本书的剩余部分(或其他任何WPF文档)。
本章中的一部分概念是全新的(例如逻辑与可视树),而另一部分是那些常见概念的扩展(如属性和事件)。通过学习每一个概念时,你将看到如何将它们应用到一个简单的用户界面——About对话框上,很多程序都需要这个界面。
3.1 逻辑树与可视树
XAML天生就是用来呈现用户界面的,这是由于它具有层次化的特性。在WPF中,用户界面由一个对象树构建而成,这棵树叫作逻辑树。
代码清单3-1定义了假想的About对话框的雏形,使用Window作为逻辑树的根节点。Window拥有一个StackPanel子元素(将在第6章讲解),它包括一些简单的控件和另一个StackPanel,而这个StackPanel中还有一些Button控件。
代码清单3-1 以XAML形式表示的一个简单的About对话框
图3-1展示了呈现出来的对话框(通过把代码清单3-1的内容粘贴到像XamlPad这样的工具中,很容易做出一个对话框),图3-2展示了这个对话框的逻辑树。
注意,即使是WPF用户界面的逻辑树也并不是用XAML创建。代码清单3-1完全可以用过程式代码来实现,而逻辑树是一致的。
逻辑树的概念很直观,但是为什么要关注它呢?因为几乎WPF的每一方面(属性、事件、资源等)都有与逻辑树相关联的行为。例如,属性值有时会沿着树自动传递给子元素,而触发的事件可以自底向上或自顶向下遍历树,这两种行为将在本章后面讲解。
与逻辑树类似的一个概念是可视树。可视树基本上是逻辑树的扩展,在可视树中,节点都被打散,分放到核心可视组件中。可视树提供了一些详细的可视化实现,而不是把每个元素当作一个“黑盒”。例如,虽然ListBox从逻辑上讲是一个单独的控件,但它的默认可视呈现是由更多的原始WPF元素组成的:一个Border对象、两个ScrollBar及其他一些元素。
并非所有的逻辑树节点都会出现在可视树中,只有从System.Windows.Media.Visual或System.Windows.Media.Visual3D派生的元素才会被包含进去。其他元素(和一些简单的字符串内容,如代码清单3-1中的内容)不会包含在内,因为它们自己并没有与生俱来的呈现行为。
图3-3是代码清单3-1在Windows Vista中使用Aero主题运行后生成的默认可视树,该图表提供了一些内部的不可见UI组件,例如ListBox的两个ScrollBar元素和每个Label的Border元素。这也告诉我们,Button、Label和ListBoxItem这3个元素,除了Button使用了一个模糊的ButtonChrome元素而不是Border元素外,其他的组成元素都是相同的。(由于这些控件有不同的默认属性值,它们会有一些其他的外观差异,例如,Button有一个默认的边界宽度Margin为10,这是在上下左右四个方向上都有的,而Label的默认Margin是0。)
提示 在XamlPad的工具栏中,有一个按钮可以显示出任何一个呈现出来的XAML的可视树(和属性值)。当XAML放入一个Window对象时(如图3-1中的情况),无法看到它的可视树,但只要将该Window元素改为Page元素(并删除SizeToContent属性),就能享受这一功能所带来的好处。
由于这一功能可以让你从深层次观察WPF元素的组成,所以可视树可能变得相当复杂。幸运的是,虽然可视树是WPF架构的核心组成部分,但通常不用去管它们,除非你正对控件进行完全的重塑(在第10章中讲解)或者做一些底层绘制(在11.1节中讲解)。根据一个按钮的可视树来写代码,例如,你想破坏WPF的核心原则之一——可视部分和逻辑部分分开。当使用第10章中的技术来改变像Button这样的控件风格时,整个可视树会被一些截然不同的东西所取代。
因此,使用System.Windows.LogicalTreeHelper和System.Windows.Media.VisualTreeHelper这两个有些对称的类可以方便地遍历逻辑树和可视树。代码清单3-2包含了代码清单3-1的一个代码隐藏文件,当它在调试器下运行时,输出的是基于深度优先的About对话框的逻辑树和可视树。(还要向代码清单3-1中添加x:Class="AboutDialog"及对应的xmlns:x标签,以便把它与过程式代码关联起来。)
注意 不要根据具体的可视树写代码!
逻辑树是静态的,不会受到程序员的干扰(例如动态添加/删除元素),但只要用户切换不同的Windows主题,可视树就会改变。
代码清单3-2 遍历和打印逻辑树和可视树
递归调用每个逻辑子节点
有时,叶子节点不是DependencyObject,如string
打印对象,使用前置空格表示深度
递归调用每个可视子节点
打印对象,使用前置空格表示深度
当使用depth=0和当前的Window实例调用这些方法时,其结果就是一个基于文本的树,其中的节点与图3-2和图3-3中的完全相同。虽然在Window的构造函数中就可以遍历逻辑树,但可视树直到Window完成至少一次布局之后才会有节点,否则是空的。这也是为什么PrintVisualTree是在OnContentRendered中调用的,因为OnContentRendered是在布局完成之后才被调用的。
有些时候,可以用元素自己的实例方法在两种树中进行操作。例如,Visual类包含了3个protected的成员(VisualParent、VisualChildrenCount和GetVisualChild)用于验证它的可视父节点和孩子节点。FrameworkElement是一个通用的控件基类(例如Button和Label就是从FrameworkElement派生而来),它有一个公共的Parent属性用于呈现逻辑父节点。某些FrameworkElement的子类会以不同的方式提供它们的逻辑子元素。例如,有些类提供了Children集合,其他类(如Button和Label)则提供了Content属性,这表示元素只能够有一个逻辑子节点。
提示 像图3-3中呈现的可视树通常会被简化为元素树,因为它们既包含了逻辑树中的元素,也包含了可视树中的某些元素。术语“可视树”是用来描述任何一棵包含可视元素(这些元素不是逻辑元素)的子树。例如,大多数人会说Window元素的默认可视树是由一个Border、一个AdornerDecorator、两个AdornerLayer和一个ContentPresenter组成的。在图3-3中,最上层的StackPanel是不作为ContentPresenter的可视子元素的,但事实是,VisualTreeHelper会把StackPanel作为一个可视子元素。
来自WPF揭秘