WPF 体系结构
本主题提供 Windows Presentation Foundation (WPF) 类层次结构的导览。 它涵盖了 WPF 的大部分主要子系统,并介绍了它们如何交互。 它还详细介绍了 WPF 架构师所做的一些选择。
System.Object
主要 WPF 编程模型通过托管代码公开。 在 WPF 的设计阶段早期,有许多关于系统的托管组件和非托管组件之间应划清界限的争论。 CLR 提供了许多功能,使开发更加高效和稳健(包括内存管理、错误处理、通用类型系统等),但这些功能也有代价。
下图演示了 WPF 的主要组件。 关系图(PresentationFramework、PresentationCore 和 milcore)的红色部分是 WPF 的主要代码部分。 其中,只有一个是非托管组件 - milcore。 Milcore 是使用非托管代码编写的,以便与 DirectX 紧密集成。 WPF 中的所有显示都通过 DirectX 引擎完成,可实现高效的硬件和软件呈现。 WPF 还需要对内存和执行进行精细控制。 milcore 中的合成引擎对性能极为敏感,因此需要放弃 CLR 的许多优势以提升性能。
本主题稍后将讨论 WPF 的托管部分和非托管部分之间的通信。 下面介绍了托管编程模型的其余部分。
System.Threading.DispatcherObject
WPF 中的大多数对象都派生自 DispatcherObject,它提供处理并发和线程处理的基本构造。 WPF 基于调度程序实现的消息传送系统。 这非常类似于熟悉的 Win32 消息泵;事实上,WPF 调度程序使用 User32 消息来执行跨线程调用。
在 WPF 中讨论并发时,确实有两个核心概念需要了解 - 调度程序和线程相关性。
在 WPF 的设计阶段,目标是转向单个执行线程,但采用非线程绑定的模型。 当组件使用执行线程的标识来存储某种状态时,会发生线程相关性。 最常见的形式是使用线程本地存储(TLS)来存储状态。 线程关联要求操作系统中的每个逻辑线程只能由一个物理线程拥有,这可能会导致内存密集。 最后,WPF 的线程模型与具有线程相关性的单线程执行的现有 User32 线程模型保持同步。 其主要原因是互操作性 – OLE 2.0、剪贴板和 Internet Explorer 等系统都需要单线程关联(STA)执行。
如果对象具有 STA 线程处理,则需要一种方法在线程之间进行通信,并验证你是否位于正确的线程上。 调度员的角色在此体现。 调度程序是一个基本的消息调度系统,具有多个优先顺序的队列。 消息示例包括原始输入通知(鼠标移动)、框架函数(布局)或用户命令(执行此方法)。 通过从 DispatcherObject派生,可以创建一个具有 STA 行为的 CLR 对象,并在创建时获得指向调度程序的指针。
System.Windows.DependencyObject
在构建 WPF 时使用的主要架构理念之一是优先使用属性而不是方法或事件。 属性是声明性的,允许你更轻松地指定意向,而不是操作。 这还支持模型驱动或数据驱动系统,用于显示用户界面内容。 此理念具有创建可以绑定到的更多属性的预期效果,以便更好地控制应用程序的行为。
为了使更多的系统由属性驱动,需要一个比 CLR 提供的更丰富的属性系统。 此丰富性的一个简单示例是变更通知。 若要启用双向绑定,需要绑定的两端来支持更改通知。 若要使行为与属性值相关联,需要在属性值更改时收到通知。 Microsoft .NET Framework 有一个接口,INotifyPropertyChange,该接口允许对象发布更改通知,但这是可选的。
WPF 提供一个更丰富的属性系统,派生自 DependencyObject 类型。 属性系统确实是一个“依赖”属性系统,因为它跟踪属性表达式之间的依赖关系,并在依赖项更改时自动重新评估属性值。 例如,如果你有一个继承属性的元素(如 FontSize),当该元素的父元素的属性发生更改时,系统会自动更新。
WPF 属性系统的基础是属性表达式的概念。 在此 WPF 的第一个版本中,属性表达式系统已关闭,并且这些表达式都作为框架的一部分提供。 表达式是属性系统没有将数据绑定、样式或继承硬编码的重要原因,而这些功能是由框架中的后续层提供的。
属性系统还提供对属性值的稀疏存储。 由于对象可以具有数十个(如果不是数百个)属性,并且大多数值都处于其默认状态(由样式等继承的、设置的),因此对象的每个实例都不需要对其定义每个属性的完全权重。
属性系统的最终新功能是附加属性的概念。 WPF 元素是基于组合和组件重用的原则构建的。 通常,某些包含元素(如 Grid 布局元素)需要子元素的额外数据来调整其行为(例如行/列信息)。 允许任何对象为任何其他对象提供属性定义,而不是将所有这些属性与每个元素相关联。 这类似于 JavaScript 的“expando”功能。
System.Windows.Media.Visual
定义系统后,下一步是将像素绘制到屏幕。 Visual 类提供生成视觉对象的树,每个对象(可选)包含有关如何呈现这些指令(剪辑、转换等)的绘图指令和元数据。 Visual 设计为极其轻量且灵活,因此大多数功能都没有公共 API 公开,并且严重依赖于受保护的回调函数。
Visual 实际上是 WPF 合成系统的入口点。 Visual 是这两个子系统(托管 API 和非托管 milcore)之间的连接点。
WPF 通过遍历由 milcore 管理的非托管数据结构来显示数据。 这些结构称为组合节点,表示具有每个节点上的呈现指令的分层显示树。 下图右侧所示的此树只能通过消息传递协议进行访问。
在对 WPF 进行编程时,可以创建 Visual 元素和派生类型,这些元素通过此消息传送协议在内部与合成树通信。 WPF 中的每个 Visual 都可以创建一个、无或多个组合节点。
此处需要注意一个非常重要的体系结构细节 - 缓存了整个视觉对象树和绘图指令。 在图形术语中,WPF 使用保留的呈现系统。 这样,系统就可以以高刷新率重绘,而不会因为合成系统对用户代码的回调而被阻塞。 这有助于防止出现无响应的应用程序。
示意图中不易察觉的另一个重要细节是系统如何实际进行组合操作。
在 User32 和 GDI 中,系统使用即时模式裁剪系统。 当组件需要呈现时,系统将建立一个剪裁边界,在该边界之外,组件不允许触摸像素,然后要求组件在该框中绘制像素。 此系统在内存约束系统中非常有效,因为当某些内容发生更改时,只需触摸受影响的组件 -- 任何两个组件都不会影响单个像素的颜色。
WPF 使用“画家算法”绘制模型。 这意味着,要求每个组件从显示器的后面向前呈现,而不是剪裁每个组件。 这样,每个组件就可以覆盖并绘制在上一个组件的显示之上。 此模型的优点是,可以具有复杂的部分透明形状。 借助当今的新式图形硬件,此模型相对较快(这不是创建 User32/GDI 时的情况)。
如前所述,WPF 的核心理念是转向更声明性的“以属性为中心的”编程模型。 在视觉系统中,这在几个有趣的地方显现出来。
首先,如果你考虑了保留的模式图形系统,这实际上正从命令性的 DrawLine/DrawLine 类型模型转移到面向数据的模型 - 新的 Line()/new Line()。 这种转变到数据驱动渲染允许使用属性来表达绘图指令的复杂操作。 从 Drawing 派生的类型实际上是用于呈现的对象模型。
其次,如果你评估动画系统,你将看到它几乎是完全声明性的。 你可以将动画表示为动画对象的一组属性,而不是要求开发人员计算下一个位置或下一种颜色。 然后,这些动画可以表达开发人员或设计器的意图(在 5 秒内将此按钮从此处移动到那里),并且系统可以确定实现此目的的最有效方法。
System.Windows.UIElement
UIElement 定义核心子系统,包括布局、输入和事件。
布局是 WPF 中的核心概念。 在许多系统中,有一组固定的布局模型(HTML 支持三个布局模型;流、绝对和表)或没有布局模型(User32 实际上仅支持绝对定位)。 WPF 首先假设开发人员和设计器想要一个灵活的可扩展布局模型,该模型可以由属性值而不是命令性逻辑驱动。 在 UIElement 级别,引入了布局的基本协议,即一个具有 Measure 和 Arrange 阶段的两个阶段模型。
Measure 允许组件确定要占用多少空间。 这是与 Arrange 阶段分开的,因为在很多情况下,父元素会要求子元素进行多次测量,以确定其最佳的位置和大小。 父元素要求子元素进行测量,这一事实展示了 WPF 的另一项关键理念 —— 根据内容调整大小。 WPF 中的所有控件都支持根据内容自动调整大小。 这使得本地化更加容易,并且在调整大小时可以动态地布局元素。 Arrange 阶段允许父级定位并确定每个子级的最终大小。
很多时间通常都花在讨论 WPF 的输出端 - Visual 和相关对象。 然而,输入端也有巨大的创新。 WPF 输入模型中最根本的更改,可能是使输入事件能够以一致的模型路由通过系统。
输入源自内核模式设备驱动程序上的信号,并通过涉及 Windows 内核和 User32 的复杂过程路由到正确的进程和线程。 将对应于输入的 User32 消息路由到 WPF 后,它将转换为 WPF 原始输入消息并发送到调度程序。 WPF 允许将原始输入事件转换为多个实际事件,使“MouseEnter”等功能能够在保证传递的低级别系统实现。
每个输入事件将转换为至少两个事件 - 一个“预览”事件和实际事件。 WPF 中的所有事件都有通过元素树进行路由的概念。 事件在从目标节点向树的根节点传递时被称作“冒泡”,而从根节点向目标节点传递时被称作“隧道”。 输入预览事件隧道,使树中的任何元素都有机会筛选或对事件执行操作。 然后,常规(非预览)事件从目标向上冒泡到根节点。
隧道和气泡阶段之间的这种拆分使得键盘加速器等功能的实现在复合世界中以一致的方式工作。 在 User32 中,你将实现键盘快捷键,方法是创建一个全局表,其中包含要支持的所有快捷键(Ctrl+N 映射到“新建”)。 在应用程序的调度程序中,你将调用 TranslateAccelerator,这将监测 User32 中的输入消息,并确定是否有任何匹配已注册的加速器。 在 WPF 中,这不起作用,因为系统完全“可组合”–任何元素都可以处理和使用任何键盘加速器。 有了这两个阶段模型进行输入,组件就可以实现自己的“TranslateAccelerator”。
为了进一步推进,UIElement 还引入了命令绑定的概念。 WPF 命令系统允许开发人员通过命令入口点(实现 ICommand)来定义功能。 命令绑定使元素能够定义输入手势(Ctrl+N)和命令(新建)之间的映射。 输入手势和命令定义都是可扩展的,可以在使用时连接在一起。 例如,这使得最终用户能够自定义要在应用程序中使用的密钥绑定,这一点很简单。
至此,在主题中,WPF 的“核心”功能 - 在 PresentationCore 程序集中实现的功能一直是焦点。 在生成 WPF 时,基础部分(如具有 度量值 和 排列)和框架片段(如实现特定布局(如 Grid)之间的布局的协定是所需的结果。 目标是在堆栈中提供一个低扩展点,以便外部开发人员在需要时创建自己的框架。
System.Windows.FrameworkElement
可以通过两种不同的方式查看 FrameworkElement。 它在 WPF 的较低层中引入的子系统上引入了一组策略和自定义项。 它还引入了一组新的子系统。
FrameworkElement 引入的主要策略围绕应用程序布局。 FrameworkElement 基于 UIElement 引入的基本布局协定,并添加了布局“槽”的概念,使布局作者更容易拥有一组一致的属性驱动布局语义。 属性,如HorizontalAlignment、VerticalAlignment、MinWidth和Margin,仅举几例,使所有从FrameworkElement派生的组件在布局容器中表现得一致。
FrameworkElement 还简化了对 WPF 核心层中许多功能的 API 公开。 例如,FrameworkElement 通过 BeginStoryboard 方法提供对动画的直接访问。 Storyboard 提供了针对一组属性编写多个动画脚本的方法。
FrameworkElement 引入的两项最关键的事情是数据绑定和样式。
WPF 中的数据绑定子系统对于使用 Windows 窗体或 ASP.NET 创建应用程序用户界面(UI)的任何人都应该相对熟悉。 在这些系统中,有一种简单的方式来表达你希望给定元素中的一个或多个属性绑定到一段数据。 WPF 完全支持属性绑定、转换和列表绑定。
WPF 中数据绑定最有趣的功能之一是引入数据模板。 数据模板允许以声明方式指定应如何可视化数据。 与其创建可以绑定数据的自定义用户界面,不如改变思路,让数据决定将要创建的显示方式。
样式实际上是一种轻量级的数据绑定形式。 使用样式设置,可以将一组属性从共享定义绑定到元素的一个或多个实例。 样式通过显式引用(通过设置 Style 属性)或隐式将样式与元素的 CLR 类型相关联来应用于元素。
System.Windows.Controls.Control
控件最重要的功能是模板化。 如果将 WPF 的合成系统视为保留模式呈现系统,模板化允许控件以参数化、声明性的方式描述其呈现。 ControlTemplate 实际上只是一个用于创建一组子元素的脚本,并绑定控件提供的属性。
Control 提供了一组预设属性,如Foreground、Background、Padding,仅举几例,方便模板作者使用这些属性来自定义控件的显示。 控件的实现提供数据模型和交互模型。 交互模型定义一组命令(如窗口关闭)和对输入手势的绑定(如单击窗口右上角的红色 X)。 数据模型提供了一组属性,用于自定义交互模型或显示(由模板决定)。
在数据模型(属性)、交互模型(命令和事件)和显示模型(模板)之间进行拆分可以完全自定义控件的外观和行为。
控件数据模型的共同方面是内容模型。 如果查看类似于 Button的控件,则会看到它具有一个名为“Content”Object类型的属性。 在 Windows 窗体和 ASP.NET 中,此属性通常是一个字符串,但会限制可以在按钮中放置的内容类型。 按钮的内容可以是简单字符串、复杂数据对象或整个元素树。 对于数据对象,数据模板用于构造显示。
总结
WPF 旨在允许你创建动态数据驱动的呈现系统。 系统的每个部分都旨在通过驱动行为的属性集创建对象。 数据绑定是系统的基本部分,并且在每个层集成。
传统应用程序创建界面,然后绑定某些数据。 在 WPF 中,控件的所有内容(显示的各个方面)都是由某种类型的数据绑定生成的。 按钮内的文本通过在按钮中创建组合控件并将其与按钮的内容属性绑定来进行显示。
开始开发基于 WPF 的应用程序时,你会觉得非常熟悉。 你可以设置属性、使用对象和进行数据绑定,其方式与使用 Windows 窗体或 ASP.NET 的方式大致相同。 通过更深入地调查 WPF 的体系结构,你会发现创建更丰富的应用程序的可能性,这些应用程序从根本上将数据视为应用程序的核心驱动程序。