WPF 开发教程

buleskyxx 贡献于2013-02-20

作者 微软用户  创建于2010-03-21 06:44:00   修改者微软用户  修改于2010-03-21 09:29:00字数153414

文档摘要:  本主题提供 Windows Presentation Foundation (WPF) 类层次结构的指导教程,涵盖了 WPF 的大部分主要子系统,并描述它们是如何交互的。本主题还详细介绍了 WPF 架构师所做的一些选择。  WPF 主要编程模型是通过托管代码公开的。在 WPF 的早期设计阶段,曾有过大量关于如何界定系统的托管组件和非托管组件的争论。CLR 提供一系列的功能,可以令开发效率更高并且更加可靠(包括内存管理、错误处理和通用类型系统等),但这是需要付出代价的。
关键词:

微软WPF应用 ------WPF开发教程 目录 WPF基础入门 3 1. WPF基础之体系结构 3 2. WPF基础之XAML 9 3. WPF基础之基元素 23 4. WPF基础之属性系统 26 5. WPF基础之路由事件 33 6. WPF基础之布局系统 46 7. WPF基础之样式设置和模板化 51 8. 详谈WPF开发中的数据虚拟化 64 XAML语法 74 1. XAML语法术语 74 2. 代码隐藏和XAML 82 3. XAML和自定义类 83 4. 标记扩展和XAML 87 5. XAML命名空间和命名空间映射 90 6. WPF名称范围 92 WPF控件开发 95 1. WPF控件开发之控件概述 95 2. 使用XAML创建按钮 103 3. WPF控件库之Button 114 4. WPF控件库之Menu 115 5. WPF控件库之Lable 119 6. WPF控件库之Toolbar 121 7. WPF控件开发之自定义控件 124 8. WPF控件开发之装饰器 140 WPF数据绑定 143 1. 数据绑定概述 143 2. WPF数据绑定之绑定源 164 3. WPF数据绑定之数据模板 166 4. WPF数据绑定之绑定声明 181 5. 实例一:绑定到ADO.NET数据源 184 6. 实例二:绑定到LINQ查询的结果 186 WPF图形和多媒体开发 187 第 页 共 291 页 微软WPF应用 1. WPF 图形动画和媒体概述 187 2. WPF的图形呈现 191 3. WPF的图像处理 205 4. WPF的三维图形应用 219 5. WPF的三维变换应用 229 6. WPF的动画开发 238 7. WPF的多媒体开发 250 第 页 共 291 页 微软WPF应用 WPF基础入门 1. 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 第 页 共 291 页 微软WPF应用 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 的第一版中,属性表达式系统是关闭的,表达式都是作为框架的一部分提供的。表达式致使属性系统不具有硬编码的数据绑定、样式调整或继承,而是由框架内后面的层来提供这些功能。 第 页 共 291 页 微软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 中,系统是在一个即时模式剪辑系统上工作。当需要呈现一个组件时,系统会建立一个剪辑边界,不允许组件接触该边界之外的像素,然后会要求此组件在该框中绘制像素。此系统在内存受限的系统上工作良好,因为当某些内容更改时,只需要处理受影响的组件即可 – 不会有两个组件对一个像素的颜色更改起作用。 第 页 共 291 页 微软WPF应用 WPF 使用“绘画器的算法”绘制模型。这意味着并不是剪辑每个组件,而是要求从显示内容的背面至正面来呈现每个组件。这允许每个组件在先前的组件的显示内容上绘制。此模型的优点是您可以生成部分透明的复杂形状。与现今的现代图形硬件比较,此模型相对要快(创建 User32/ GDI 的情况除外)。 如上面所述,WPF 的一个核心原理是移动到一个更具声明性且“以属性为核心”的编程模型。在可视化系统中,这会表现为需要关注的两种情况。 首先,如果您考虑保留的模式图形系统,则实际上是从命令性 DrawLine/DrawLine 类型模型移动到面向数据的模型 new Line()/new Line()。通过这一向数据驱动的呈现移动,可以在使用属性表达的绘制指令上进行复杂的操作。从 Drawing 派生的类型实际上是用于呈现的对象模型。 第二,如果评估动画系统,您将看到它几乎是完全声明性的。无需要求开发人员计算下一位置或下一颜色,您可以将动画表示为动画对象上的一组属性。于是,这些动画可以表示开发人员或设计人员的意图(在 5 秒内将此按钮从一个位置移动到另一个位置),系统就可以确定完成此任务的最有效方式。 System.Windows.UIElement UIElement 定义核心子系统,包括 Layout、Input 和 Event。 Layout 是 WPF 中的一个核心概念。在许多系统中,可能有一组固定的布局模型(HTML 支持三种布局模型:流、绝对和表),也可能没有布局模型(User32 实际仅支持绝对定位)。WPF 先假设开发人员和设计人员希望有一个灵活的可扩展布局模型,该模型可能是由属性值而不是命令性逻辑驱动的。在 UIElement 级别,会引入布局的基本协定 - 具有 Measure 和 Arrange 处理过程的两阶段模型。 Measure 允许组件确定它要采用的大小。此阶段独立于 Arrange,因为在许多情形下,父元素会要求子元素测量若干次以确定其最佳位置和大小。父元素要求子元素测量这一事实体现了 WPF 的另一关键原则 – 内容大小。WPF 中的所有控件支持调整到内容原始大小的功能。这使本地化更加容易,并允许在调整大小时对元素进行动态布局。Arrange 阶段允许父元素定位并确定每个子元素的最终大小。 通常会花费大量的时间来讨论 WPF 的输出端(Visual 及其相关对象)。然而,在输入端也有许多创新。WPF 输入模型的最基本更改也许是一致模型,输入事件通过系统借助此模型进行路由。 第 页 共 291 页 微软WPF应用 输入是作为内核模式设备驱动程序上的信号发出的,并通过涉及 Windows 内核和 User32 的复杂进程路由到正确的进程和线程。与输入相对应的 User32 消息一旦路由到 WPF,它就会转换为 WPF 原始输入消息,并发送到调度程序。WPF 允许原始输入事件转换为多个实际事件,允许在保证传递到位的情况下在较低的系统级别实现类似“MouseEnter”的功能。 每个输入事件至少会转换为两个事件 – “预览”事件和实际事件。WPF 中的所有事件都具有通过元素树路由的概念。如果事件从目标向上遍历树直到根,则被称为“冒泡”,如果从根开始向下遍历到目标,它们被称为“隧道”。输入预览事件隧道,使树中的任何元素都有机会筛选事件或对事件采取操作。然后,常规(非预览)事件将从目标向上冒泡到根。 分割隧道和冒泡阶段使快捷键等功能在复合世界中表现一致。在 User32 中,您可以通过使用一个全局表来实现快捷键,该表中包含您希望支持的所有快捷键(Ctrl+N 映射为“新建”)。在应用程序的调度程序中,您可以调用 TranslateAccelerator,它会探查 User32 中的输入消息,并确定是否有任何消息与已注册的快捷键匹配。在 WPF 中,上述内容不会起作用,因为系统是完全“可组合”的 – 任何元素都可以处理和使用任何快捷键。将这个两阶段模型用于输入,将允许组件实现其自己的TranslateAccelerator"。 为了进一步深化此功能,UIElement 还引入了 CommandBindings 的概念。WPF 命令系统允许开发人员以命令终结点(一种用于实现 ICommand 的功能)的方式定义功能。命令绑定使元素可以定义输入笔势 (Ctrl+N) 和命令(“新建”)之间的映射。输入笔势和命令定义都是可扩展的,并且可以在使用时联系到一起。这使得一些操作(例如,允许最终用户自定义其要在应用程序内使用的键绑定)显得无关紧要。 至此,本主题已重点讨论了 WPF 的“核心”功能 - PresentationCore 程序集中实现的功能。当生成 WPF 时,基础部分(例如带有 Measure 和 Arrange 的布局的协定)和框架部分(例如 Grid 的特定布局的实现)之间的明确划分是希望的结果。目标就是提供在堆栈中处于较低位置的可扩展性点,这将允许外部开发人员可以在需要时创建自己的框架。 System.Windows.FrameworkElement 可以按两种不同的方式来看待 FrameworkElement。它对在 WPF 的较低层中的子系统引入一组策略和自定义项。它还引入了一组新的子系统。 FrameworkElement 引入的主要策略是关于应用程序布局。FrameworkElement 在 UIElement 引入的基本布局协定之上生成,并增加了布局“插槽”的概念,使布局制作者可以方便地拥有一组面向属性的一致的布局语义。HorizontalAlignment、VerticalAlignment、MinWidth 和 Margin 等属性使得从 FrameworkElement 派生的所有组件在布局容器内具有一致的行为。 第 页 共 291 页 微软WPF应用 利用 FrameworkElement,WPF 的核心层中具有的许多功能可以更方便地进行 API 公开。例如,FrameworkElement 通过 BeginStoryboard 方法提供对动画的直接访问。Storyboard 提供一种针对一组属性为多个动画编写脚本的方式。 FrameworkElement 引入的两个最关键的内容是数据绑定和样式。 曾经使用 Windows 窗体或 ASP.NET 创建应用程序用户界面 (UI) 的用户应当对 WPF 中的数据绑定子系统较为熟悉。在上述每个系统中,可通过一种简单的方式来表达您希望将给定元素中的一个或多个属性绑定到一个数据片断。WPF 对属性绑定、变换和列表绑定提供全面支持。 WPF 中数据绑定的最值得关注的功能之一是引入了数据模板。利用数据模板,您可以声明性地指定某个数据片断的可视化方式。您可以将问题换个方向,让数据来确定将要创建的显示内容,而无需创建可绑定到数据的自定义用户界面。 样式实际上是轻量级的数据绑定。使用样式,您可以将共享定义的一组属性绑定到元素的一个或多个实例。通过显式引用(通过设置 Style 属性)或通过将样式与元素的 CLR 类型隐式关联,便可以将样式应用到元素。 System.Windows.Controls.Control 控件的最重要的功能是模板化。如果您将 WPF 的组合系统视为一个保留模式呈现系统,则控件可通过模板化以一种参数化的声明性方式描述其呈现。ControlTemplate 实际上不过是一个用于创建一组子元素的脚本,同时绑定到由控件提供的属性。 Control 提供一组常用属性,如 Foreground、Background、Padding 等,模板创作者可以使用这些常用属性来自定义控件的显示。控件的实现提供了数据模型和交互模型。交互模型定义了一组命令(如窗口的“关闭”),以及到输入笔势的绑定(如单击窗口上角的红叉)。数据模型提供了一组属性,用于自定义交互模型或自定义显示(由模板确定)。 数据模型(属性)、交互模型(命令和事件)及显示模型(模板)之间的划分,使用户可以对控件的外观和行为进行完全自定义。 最常见的控件数据模型是内容模型。如果查看 Button 之类的控件,您会看到它具有一个类型为 Object 的名为“Content”的属性。在 Windows 窗体和 ASP.NET 中,此属性通常为一个字符串 – 不过,这会限制您可以在按钮中输入的内容类型。按钮的内容可以是简单的字符串、复杂的数据对象或整个元素树。如果是数据对象,可以使用数据模板构造显示内容。 摘要 WPF 旨在帮助您创建动态的数据驱动的演示系统。系统的每一部分均可通过驱动行为的属性集来创建对象。数据绑定是系统的基础部分,在每一层中均进行了集成。 第 页 共 291 页 微软WPF应用 传统的应用程序创建一个显示内容,然后绑定到某些数据。在 WPF 中,关于控件的所有内容、显示内容的所有方面都是由某种类型的数据绑定生成的。通过在按钮内部创建复合控件并将其显示绑定到按钮的内容属性,就会显示按钮内的文本。 当开始开发基于 WPF 的应用程序时,您应对其感到非常熟悉。在其中设置属性、使用对象和数据绑定的方式与您使用 Windows 窗体或 ASP.NET 是极其相似的。如果对 WPF 体系结构有更深的了解,您就能够创建更丰富的应用程序,这些应用程序在根本上会将数据视为应用程序的核心驱动力。 2. WPF基础之XAML 本主题介绍可扩展应用程序标记语言 (XAML) 语言的功能,并演示如何使用 XAML 编写 Windows Presentation Foundation (WPF) 应用程序。本主题专门介绍了 Windows Presentation Foundation (WPF) 实现的 XAML。XAML 本身是比 Windows Presentation Foundation (WPF) 更广泛的一个语言概念。 具有流控制支持的声明性语言 XAML 简化了为 .NET Framework 编程模型创建 UI 的过程。您可以在声明性 XAML 标记中创建可见的 UI 元素,然后使用代码隐藏文件(通过分部类定义与标记相连接)将 UI 定义与运行时逻辑相分离。在 XAML 中混合代码和标记的功能很重要,因为 XML 本身是声明性的,不会为流控制真正建议一个模型。基于 XML 的声明性语言非常直观,可以为用户(尤其是具有 Web 设计和技术背景的人员)创建从原型到生产的各种界面。与其他大多数标记语言不同,XAML 直接呈现托管对象的实例化。这种常规设计原则简化了使用 XAML 创建的对象的代码和调试访问。 XAML 文件是指通常使用 .xaml 扩展名的 XML 文件。 下面的 XAML 示例演示了小标记在创建作为 UI 一部分的按钮时的必要性。创建的按钮通过主题样式获得默认的可视化表示形式,通过其类设计获得默认的行为。 XAML 对象元素 XAML 有一组规则,这些规则将对象元素映射为类或结构,将属性 (Attribute) 映射为属性 (Property) 或事件,并将 XML 命名空间映射为 CLR 命名空间。XAML 元素映射为被引用程序集中定义的 Microsoft .NET 类型,而属性 (Attribute) 则映射为这些类型的成员。 第 页 共 291 页 微软WPF应用 上面的示例指定了两个对象元素:<STACKPANEL>(具有一个结束标记)和<BUTTON>同样具有多个属性;下一节将介绍属性)。字符串 StackPanel 和 Button 都将映射为某个类的名称,该类由 WPF 定义并且是 WPF 程序集的一部分。在指定对象元素标记时,可以为 XAML 处理创建一条指令,以便在加载 XAML 页时创建指定类的一个新实例。每个实例都是通过调用基础类或结构的默认构造函数并对结果进行存储而创建的。为了可用作 XAML 中的对象元素,该类或结构必须公开一个公共的默认(无参数)构造函数。 设置属性 XAML 中的属性是通过使用各种可能的语法在对象元素上设置属性来设置的。根据所设置的属性的特征,给定属性可使用的语法会有所不同。 通过设置属性值,可以为对象元素添加功能或特征。对象元素的基础对象实例的初始状态基于默认的构造函数行为。通常,您的应用程序将使用其他一些实例,而不是任何给定对象的完全默认的实例。 属性语法 在 XAML 中,属性 (Property) 常常可以表示为属性 (Attribute)。属性 (Attribute) 语法是最简单的属性 (Property) 设置语法,并将成为过去使用标记语言的开发人员可以使用的最直观的语法。例如,以下标记将创建一个具有红色文本和蓝色背景的按钮,还会创建指定为 Content 的显示文本。 属性元素语法 对于一个对象元素的某些属性 (Property),属性 (Attribute) 语法是不可能实现的,因为提供属性 (Property) 值所需的对象或信息不能充分地表示为简单的字符串。对于这些情况,可以使用另一个语法,即属性元素语法。属性元素语法用标记的内容设置包含元素的引用的属性。一般而言,内容就是作为属性值的类型的某个对象(值设置实例通常被指定为另一个对象元素)。属性元素本身的语法为 <类型名称.属性>。指定内容之后,必须用一个结束标记结束属性元素,就像其他任何元素(语法为 )一样。对于同时支持属性 (Attribute) 和属性 (Property) 元素语法的属性 (Property),尽管这两种语法的细微之处(如空白处理)略有不同,但它们的结果通常是一样的。如果可以使用属性 (Attribute) 语法,那么使用属性 (Attribute) 语法通常更为方便,且能够实现更为精简的标记,但这只是一个风格的问题,而不属于技术限制。下面的示例演示了在前面的属性 (Attribute) 语法示例中设置的相同属性 (Property),但这次对 Button 的所有属性 (Property) 使用了属性 (Property) 元素语法。 第 页 共 291 页 微软WPF应用 XAML 的属性 (Property) 元素语法表示了与标记的基本 XML 解释之间的巨大背离。对于 XML,<类型名称.属性> 代表了另一个元素,该元素仅表示一个子元素,而与 TypeName 父级之间没有必然的隐含关系。在 XAML 中,<类型名称.Property> 直接表示 Property 是类型名称 的属性(由属性元素内容设置),而绝不会是一个名称相似(碰巧名称中有一个点)但却截然不同的元素。 属性和类继承 作为 WPF 元素的XAML 属性 (Attribute) 而出现的属性 (Property) 通常从基类继承而来。例如,在上一个示例中,如果您要查看类定义、反射结果或文档,Background 属性并不是在 Button 类上直接声明的属性。相反,Background 是从基 Control 类继承而来。 WPF XAML 元素的类继承行为是与标记的基本 XML 解释之间的另一个巨大背离。使用类继承(尤其是中间基类为抽象类时)的另一个原因在于,通过 XML 编程常用的架构类型(如 DTD 或 XSD 格式)几乎不可能准确且完整地表示 XAML 元素及其允许属性集。另外,XAML 中的“X”表示“extensible”(可扩展),而可扩展性破坏了“什么是用于 WPF 的 XAML”的任何给定表示形式的完整性。 引用值和标记扩展 标记扩展是一个 XAML 概念。在属性语法中,花括号({ 和 })表示标记扩展用法。此用法指示 XAML 处理不要像通常那样将属性值视为一个字符串或者可直接转换为文本字符串的值。 当属性采用引用类型值时,这些属性常常需要属性元素语法(始终创建一个新实例)或通过标记扩展的对象引用。标记扩展用法有可能会返回现有实例,因此可以更加多样化,或者产生较少的对象系统开销。 当使用标记扩展提供属性值时,应改为由相关标记扩展的后备类中的逻辑提供属性值。WPF 应用程序编程中最常用的标记扩展是 Binding(用于数据绑定表达式)以及资源引用 StaticResource 和 DynamicResource。通过使用标记扩展,即使属性 (Property) 不支持对直接对象实例化使用属性 (Attribute) 语法,也可以使用属性 (Attribute) 语法为属性 (Property) 提供引用值;或者使特定行为能够符合必须用属性 (Property) 类型值填充 XAML 属性 (Property) 这一常规行为要求。 第 页 共 291 页 微软WPF应用 例如,下面的示例使用属性 (Attribute) 语法设置 Style 属性 (Property) 的值。Style 属性 (Property) 采用了 Style 类的一个实例,这是默认情况下不能在属性 (Attribute) 语法字符串中指定的引用类型。但在本例中,属性 (Attribute) 引用了特定的标记扩展 StaticResource。当处理该标记扩展时,它返回对以前在资源字典中作为键控资源进行实例化的某个样式的引用。 资源只是 WPF 或 XAML 启用的一种标记扩展用法。 支持 Typeconverter 的属性值 在“属性语法”一节中,曾提到属性值必须能够使用字符串进行设置。对字符串如何转换为其他对象类型或基元值的基本本机处理取决于 String 类型本身。但是很多 WPF 类型或这些类型的成员扩展了基本字符串属性处理行为,因此更复杂的对象类型的实例可通过字符串指定为属性值。在代码级别,此处理是通过指定处理字符串属性值的 CLR 类型转换器来完成的。常用于指示矩形区域尺寸(如 Margin)的 Thickness 结构类型是这样一个类型的示例:它具有针对采用该类型的所有属性 (Property) 公开的一个特殊的、支持类型转换器的属性 (Attribute) 语法,以便于在 XAML 标记中使用。下面的示例使用支持类型转换器的属性 (Attribute) 语法来为 Margin 提供值: 上面的属性 (Attribute) 语法示例与下面更为详细的语法示例等效,但在下面的示例中,Margin 是通过包含 Thickness 对象元素的属性 (Property) 元素语法设置的,而且 Thickness 的四个关键属性 (Property) 设置为新实例的属性 (Attribute): 第 页 共 291 页 微软WPF应用 是使用支持类型转换器的语法,还是使用更详细的等效语法,通常只是编码风格的选择问题,但支持转换器的语法有助于生成更简洁的标记。(但是,有一些对象只能采用类型转换器将属性设置为该类型,因为类型对象本身并没有默认的构造函数。例如,Cursor。) 集合类型和 XAML 集合属性 XAML 指定了一个语言功能,通过该功能,可以从标记中特意省略表示集合类型的对象元素。当 XAML 处理器处理采用了集合类型的属性时,将隐式创建相应集合类型的实例,即使标记中不存在该集合的对象元素也是如此。在集合类型的 SDK 参考页中,特意省略集合对象元素的这种语法在 XAML 语法部分中有时候称为“隐式集合语法”。 隐式集合语法适用于实现 IList 或 IDictionary 的类型,或者适用于数组。 您已经在 XAML 资源示例中看到了未调用的集合对象元素的隐式集合语法的示例: 除了根元素外,页面上作为另一个元素的子元素而嵌套的每个对象元素实际上都是下列一种或两种情况下的元素:父元素的隐式集合属性的一个成员,或者为父元素指定 XAML 内容属性值的元素(XAML 内容属性将在下一节进行讨论)。换言之,一个标记页上的父元素与子元素之间的关系实际上就是一个根对象,而根对象下面的每个对象元素要么是为父元素提供属性值的一个实例,要么是同样作为父元素的集合类型属性值的集合中的一项。在资源示例的案例中,Resources 属性采用 ResourceDictionary 类型的一个对象。下面的示例在语法上与显式指定的 ResourceDictionary 的对象元素等效。 第 页 共 291 页 微软WPF应用 Resources 集合是许多常见的 WPF 框架级元素上存在的集合属性的一个示例。在 XAML 中设置此属性需要使用属性元素语法。属性元素中的每个被包含的对象元素都成为集合(IDictionary 实现)中的一个项。虽然集合类型本身通常没有包含项的属性或索引器,但是该属性不能在标记中指定;它完全是隐含的。对于 ResourceDictionary,该属性是 Item 索引器。 XAML 内容属性 XAML 指定了一个语言功能,通过该功能,任何可以用作 XAML 对象元素的类都可以确切指定其属性之一作为该类实例的 XAML 内容属性。当 XAML 处理器处理具有 XAML 内容属性的对象元素时,该对象元素的任何 XML 子元素都被当作包含在一个表示该内容属性的隐式属性元素标记中来处理。在标记中,可以省略 XAML 内容属性的属性元素语法。在标记中指定的任何子元素都将成为 XAML 内容属性的值。 您已经看过了未调用的 XAML 内容属性的示例:本主题中的第一个示例。 这里,Button 是 StackPanel 的子元素。这是一个简单直观的标记,其中出于两个不同的原因省略了两个标记。 省略的 StackPanel.Children 属性元素: StackPanel 从 Panel 派生。Panel 将 Panel..::.Children 定义为其 XAML 内容属性。Panel 的所有派生类因而具有该 XAML 内容属性,而 Panel..::.Children 的属性元素可省略。 省略的 UIElementCollection 对象元素: Panel..::.Children 属性采用类型 UIElementCollection,该类型实现 IList。因此,根据为集合定义的 XAML 规则,可以省略 UIElementCollection 对象元素标记。在这种情况下,UIElementCollection 实际上不能实例化为一个对象元素。您甚至无法显式声明该集合对象。这是因为 UIElementCollection 不公开默认的构造函数。其他几个 WPF 集合类型也不公开对象元素用法的构造函数,因为 XAML 集合语法处理仍然允许它们在 XAML 中隐式工作。这就是 UIElementCollection 对象元素在示例中显示为已被注释的原因;如果未被注释,示例将不能编译。 第 页 共 291 页 微软WPF应用 内部文本和 XAML 内容属性 StackPanel / Button 示例还有另一种变体。 请注意为 Button 指定的显示文本如何发生变化。前面已在属性 (Attribute) 语法中指定了 Content 属性 (Property);这次显示字符串是 Button 对象元素中的内部文本。此语法可行,因为 Content 是 Button 基类 ContentControl 的 XAML 内容属性。元素中的字符串根据 Content 属性的属性类型(即 Object)进行计算。Object 不会尝试任何字符串类型转换,因此 Content 属性的值变成了文本字符串值。或者,Button 中的内容可以是任何单个 Object。Button 等控件通常为类定义 XAML 内容属性,因此 XAML 内容属性可用于 UI 和显示文本,或用于控件合成,或同时用于此两者。 对于流程文档模型和本地化而言,在元素中放置字符串作为内容以生成与其他常见标记语言类似的标记的功能特别重要。 XAML 内容属性值必须连续 XAML 内容属性的值必须完全在该对象元素的其他任何属性元素之前或之后指定。不管 XAML 内容属性的值指定为字符串还是指定为一个或多个对象都是如此。例如,下面的标记无法进行编译: 这在本质上是非法的,因为如果此语法是通过使用内容属性的属性元素语法而变为显式的,则内容属性将设置两次: 第 页 共 291 页 微软WPF应用   一个类似的非法示例是,如果内容属性是一个集合,则子元素是与属性元素交错的:   内容模型 从语法上讲,可能支持将类用作 XAML 元素,但只有放置到整体内容模型或元素树中的所需位置时,该元素才能在应用程序或页面上正常运行。例如,MenuItem 通常只应作为 MenuBase 派生类(如 Menu)的子级放置。特定元素的内容模型在可用作 XAML 元素的控件和其他 WPF 类的类页面上的备注中进行说明。对于具有更复杂内容模型的某些控件,内容模型作为单独的概念主题进行说明。 XAML 中的大小写和空白 XAML 区分大小写。按名称与程序集中的基础类型进行比较或者与类型的成员进行比较时,必须使用正确的大小写指定所有对象元素、属性 (Property) 元素和属性 (Attribute) 名称。属性的值并不总是区分大小写。值是否区分大小写将取决于与采用该值的属性关联的类型转换器行为,或取决于属性值类型。例如,采用 Boolean 类型的属性可以采用 true 或 True 作为等效值,但只是因为 Boolean 的默认字符串类型转换已经允许这些值作为等效值。 XAML 处理器和序列化程序将忽略或删除所有无意义的空白,并规范化任何有意义的空白。只有当您在 XAML 内容属性中指定字符串时,才会体现此行为的重要性。简言之,XAML 将空格、换行符和制表符转化为空格,如果它们出现在一个连续字符串的任一端,则保留一个空格。 有关 XAML 语法的更多信息 隐式集合语法和 XAML 内容属性都是允许省略某些推断标记的 XAML 语言功能。这些功能的目的是在编写或检查标记时使页面上的元素的父子关系更明显。 如果您正在创建自定义类,并且正在考虑是否允许使用 XAML,XAML 语法术语主题也是一个很好的起点。 第 页 共 291 页 微软WPF应用 XAML 根元素和 xmlns 一个 XAML 文件只能有一个根元素,这样才能成为格式正确的 XML 文件和有效的 XAML 文件。通常,应选择属于应用程序模型一部分的元素(例如,为页面选择 Window 或 Page,为外部字典选择 ResourceDictionary,或为应用程序定义根选择 Application)。下面的示例演示 WPF 页面的典型 XAML 文件的根元素,其中的根元素为 Page。   根元素还包含属性 xmlns 和 xmlns:x。这些属性向 XAML 处理器指明哪些命名空间包含标记将要引用的元素的元素定义。xmlns 属性专门指示默认的 xmlns 命名空间。在默认的 xmlns 命名空间中,可以不使用前缀指定标记中的对象元素。对于大多数 WPF 应用程序方案以及 SDK 的 WPF 部分中给出的几乎所有示例,默认的 xmlns 命名空间均映射为 WPF 命名空间 http://schemas.microsoft.com/winfx/2006/xaml/presentation。xmlns:x 属性指示另外一个 xmlns 命名空间,该命名空间映射 XAML 语言命名空间 http://schemas.microsoft.com/winfx/2006/xaml。在具有此映射的文件的标记中引用时,XAML 规范定义的必需语言组件带有 x: 前缀。使用 xmlns 定义用法范围和映射的这种做法符合 XML 1.0 规范。请注意,xmlns 属性仅在每页的根元素上和应用程序定义上(如果在标记中提供了应用程序定义)才是严格必需的。xmlns 定义将应用于根的所有子元素。(此行为仍然符合 xmlns 的 XML 1.0 规范。)xmlns 属性还允许出现在根下面的其他元素上,并且将应用于定义元素的任何子元素。但是,此用法并不典型,因为频繁定义或重新定义 xmlns 命名空间可能会导致 XAML 标记样式难以阅读。 由于存在属于项目生成文件一部分的配置,因此可以知道 WPF 程序集包含的某些类型支持 WPF 到默认 xmlns 的映射。程序集还映射到目标文件中。因此,为了引用来自 WPF 程序集的 XAML 元素,只需映射 xmlns 即可。对于您自己的自定义程序集,或者除 WPF 之外的程序集,可以将该程序集指定为 xmlns 映射的一部分。通常,可选择其他前缀,但是也可以选择其他 xmlns 作为默认值,然后将 WPF 映射到前缀。 x: 前缀 在前面的根元素示例中,前缀 x: 用于映射 XAML xmlns http://schemas.microsoft.com/winfx/2006/xaml。在此 SDK 的项目模板、示例以及文档中,此 x: 前缀将用于映射 XAML xmlns。x: 前缀/XAML xmlns 包含多个将在 XAML 中频繁用到的编程构造。下面列出了将用到的最常见 x: 前缀/XAML xmlns 编程构造: 第 页 共 291 页 微软WPF应用 x:Key:为 ResourceDictionary 中的每个资源设置一个唯一的键。在应用程序标记中看到的所有 x: 用法中,x:Key 可能占到 90%。 x:Class:向为 XAML 页提供代码隐藏的类指定 CLR 命名空间和类名。必须具有这样一个类才能支持代码隐藏,也正是由于这个原因,即使没有资源,您也几乎总是会看到映射的 x:。 x:Name:处理对象元素后,为运行时代码中存在的实例指定运行时对象名称。在不支持等效的 WPF 框架级Name 属性的情况下命名元素时,可以使用 x:Name。某些动画方案中会发生这种情况。 x:Static:启用一个获取静态值的值引用,该静态值只能是一个 XAML 可设置属性。 x:Type:根据类型名称构造一个 Type 引用。它用于指定采用 Type 的属性 (Attribute),如 Style..::.TargetType,不过在许多情况下属性 (Property) 本身具有字符串到 Type 的转换功能,因此使用 x:Type 是可选的。 x: 前缀/XAML xmlns 中还有其他一些不太常见的编程构造。 事件和 XAML 代码隐藏 大多数 WPF 应用程序都是既包括标记,又包括代码隐藏。在一个项目中,XAML 被编写为 .xaml 文件,而使用 CLR 语言(如 Microsoft Visual Basic .NET 或 C#)编写代码隐藏文件。编译 XAML 文件时,每个 XAML 页的 XAML 代码隐藏文件的位置是通过指定一个命名空间和类作为 XAML 页的根元素的 x:Class 属性来确定的。 在目前已介绍的示例中,您已看到几个按钮,但还没有一个按钮具有任何关联的逻辑行为。为对象元素添加行为的主要应用程序级机制是使用元素类的现有事件,并为在运行时引发该事件时调用的该事件编写特定的处理程序。事件名称以及要使用的处理程序的名称在标记中指定,而实现处理程序的代码在代码隐藏中定义。   第 页 共 291 页 微软WPF应用   请注意,代码隐藏文件使用命名空间 MyNamespace 并将 MyPageCode 声明为该命名空间内的一个分部类。这相当于在标记根中提供的 MyNamespace.MyPageCode 的 x:Class 属性值。编译器将通过从根元素类型派生一个类,自动为编译的任何 XAML 页创建一个分部类。当您提供也会定义同一分部类的代码隐藏时,将在与编译的应用程序相同的命名空间和类中组合生成的代码。 如果您不想创建单独的代码隐藏文件,还可以将代码内联到 XAML 文件中。但是,内联代码是一种缺少多样性的方法,有很多的限制。 事件属性语法 当您在标记中通过事件指定行为时,通常使用属性语法来附加处理程序。在其中指定事件属性的对象元素则变成侦听事件以及调用处理程序的实例。您要处理的具体事件的名称是属性名。属性值是您要定义的处理程序的方法名。然后您必须在代码隐藏中提供处理程序实现,并使处理程序基于该事件的委托。您使用编程语言(如 Microsoft Visual Basic .NET 或 C#)在代码隐藏中编写处理程序。 引发事件时,每个 WPF 事件都将报告事件数据。事件处理程序可以访问这些事件数据。在前面的示例中,处理程序通过事件数据获取所报告的事件源,然后在该事件源上设置属性。 路由事件 路由事件是一个特殊的事件功能,该功能是 WPF 特有的并且是它的基础。路由事件允许一个元素处理另一个元素引发的事件,只要这些元素通过元素树关系连接起来。当使用 XAML 属性指定事件处理时,可以在任何元素(包括未在类成员表中列出该特定事件的元素)上侦听和处理路由事件。这是通过使用所属类名限定事件名属性来实现的。例如,在当前所讨论的 StackPanel / Button 示例中,父 StackPanel 可以通过在 StackPanel 对象元素上指定属性 Button.Click,并使用处理程序名作为属性值,为子元素按钮的 Click 事件注册一个处理程序。 x:Name 默认情况下,通过处理对象元素而创建的对象实例没有可供您在代码中使用的唯一标识符或固有的对象引用。当您在代码中调用构造函数时,几乎总是使用构造函数结果为构造的实例设置一个变量,以便以后在代码中引用该实例。为了对通过标记定义创建的对象进行标准化访问,XAML 定义了 x:Name 属性。您可以在任何对象元素上设置 x:Name 属性的值。在代码隐藏文件中,您选择的标识符等效于引用所构造的实例的实例变量。在任何方面,命名元素都像它们是对象实例一样工作(此名称只是引用该实例),并且代码隐藏文件可以引用该命名元素来处理应用程序内的运行时交互。 第 页 共 291 页 微软WPF应用 WPF 框架级 XAML 元素继承 Name 属性 (Property),该属性等效于 XAML 定义的 x:Name 属性 (Attribute)。其他某些类也为 x:Name(通常也定义为 Name 属性)提供属性级等效项。一般而言,如果您在所选元素的成员表中找不到 Name 属性,可以改用 x:Name。 下面的示例在 StackPanel 元素上设置 Name。然后,该 StackPanel 中的 Button 上的处理程序通过由 Name 设置的实例引用 buttonContainer 来引用 StackPanel。     就像变量一样,实例的名称受范围概念的控制,因此可以在可预测的某个范围内强制名称唯一。定义页面的主要标记表示一个唯一的名称范围,而该名称范围的边界就是该页面的根元素。但是,其他标记源(如样式或样式中的模板)可以在运行时与页面交互,这种标记源常常具有其自己的名称范围,这些名称范围不一定与页面的名称范围相连接。 附加属性和附加事件 XAML 指定了一个语言功能,该功能允许在任何元素上指定某些属性或事件,而不管要为其设置属性或事件的元素的成员表中是否存在该属性或元素。该功能的属性版本称为附加属性,事件版本称为附加事件。从概念上讲,您可以将附加属性和附加事件认为是可以在任何元素/类上设置的全局成员,而不管其类层次结构如何。 通常通过属性 (Attribute) 语法来使用 XAML 中的附加属性 (Property)。在属性 (Attribute) 语法中,您可以按照所有者类型.属性名 的形式指定附加属性 (Property)。表面上,这与属性元素用法类似,但在这种情况下,您指定的所有者类型 始终是一种与要为其设置附加属性的对象元素不同的类型。所有者类型 这种类型提供 XAML 处理器获取或设置附加属性值所需要的访问器方法。使用附加属性的最常见方案是使子元素能够向其父元素报告属性值。 第 页 共 291 页 微软WPF应用 下面的示例演示了 DockPanel..::.Dock 附加属性。DockPanel 类为 DockPanel..::.Dock 定义访问器,因此拥有附加属性。DockPanel 类还包括一个逻辑,该逻辑迭代其子元素并具体检查每个元素是否具有 DockPanel..::.Dock 设置值。如果找到一个值,将在布局过程中使用该值定位子元素。使用 DockPanel..::.Dock 附加属性和这种定位功能事实上是 DockPanel 类的激动人心的一面。   在 Windows Presentation Foundation (WPF) 中,所有附加属性还作为依赖项属性来实现。 附加事件使用类似的所有者类型.事件名 属性语法形式。就像非附加事件一样,XAML 中的附加事件的属性值指定在元素上处理事件时调用的处理程序方法的名称。 使用附加事件的一种方案适用于可在任何元素(如鼠标按钮)上处理的设备输入事件。例如,Mouse..::.MouseDown 就是这样一个附加事件。但是,大多数 WPF 框架级元素可以使用此事件,而无需使用附加事件。这是因为基元素类 UIElement 可为 Mouse..::.MouseDown 附加事件创建一个别名,并在 UIElement 成员表中公开该别名(为 MouseDown)。因此,通常不需要在 XAML 页或 Windows Presentation Foundation (WPF) 应用程序编程中指定附加事件语法。例外情况包括,您使用的是自定义元素,或者使用并非从 UIElement 派生但仍然具有可视化表示形式的对象元素(这些情况很少见)。在 WPF 中,所有附加事件还作为路由事件来实现。ContentElement 也为输入事件公开别名,供流程文档模型使用。 XAML 页面根元素剖析 下表显示了一个典型的 XAML 页面根元素分解结构,并显示了本主题中介绍的根元素的具体属性: 第 页 共 291 页 微软WPF应用   基类和 XAML 基础 XAML 及其架构是一个类集合,这些类对应于 CLR 对象以及要在 XAML 中使用的标记元素。但是,并不是所有的类都能映射到元素。抽象类(如 ButtonBase)和某些非抽象基类在 CLR 对象模型中用于继承,并且不支持对应的 XAML 标记。基类(包括抽象类)对于 XAML 开发仍然很重要,因为每个具体的 XAML 元素都从其层次结构中的某个基类继承成员。通常,这些成员包括可以设置为元素属性 (Attribute) 的属性 (Property),或者可以处理的事件。FrameworkElement 是 WPF 在 WPF 框架级的具体 UI 基类。设计 UI 时,您将使用各种形状、面板、修饰器或控件类,它们全部从 FrameworkElement 派生而来。有一个相关的基类 FrameworkContentElement,它使用可在 FrameworkElement 中特意镜像 API 的 API,支持适合流布局表示形式的面向文档的元素。元素级的属性 (Attribute) 和 CLR 对象模型的组合提供了一组通用的属性 (Property),可以在大多数具体的 XAML 元素上设置这些属性 (Property),而不管确切的元素类型及其基础类是什么。 XAML 安全性 XAML 是一种直接表示对象实例化和执行的标记语言。因此,使用 XAML 创建的元素能够像等效的生成代码那样与系统资源进行交互(如网络访问、文件系统 IO)。 WPF 支持 .NET 安全框架代码访问安全性 (CAS)。这意味着在 Internet 区域中运行的 WPF 内容具有更少的执行权限。“松散 XAML”(由 XAML 查看器在加载时解释的未编译 XAML 的页面)和 XAML 浏览器应用程序 (XBAP) 通常在此 Internet 区域中运行,并且使用相同的权限集。但是,加载到完全受信任的应用程序中的 XAML 与宿主应用程序具有相同的系统资源访问权限。有关更多信息,请参见 Windows Presentation Foundation 部分信任安全性。 从代码中加载 XAML XAML 可用于定义整个 UI,但有时也适合只使用 XAML 定义 UI 的一部分。利用此功能可以实现部分自定义、在本地存储信息、使用 XAML 提供业务对象或者各种可能的方案。这些方案的关键是 XamlReader 类及其 Load 方法。输入是一个 XAML 文件,而输出是一个对象,它表示从该标记中创建的对象的整个运行时树。然后您可以插入该对象,作为应用程序中已存在的另一个对象的属性。只要该属性在具有最终显示功能并且将通知执行引擎已在应用程序中添加新内容的内容模型中是一个合适的属性,您就可以通过以 XAML 形式加载来轻松地修改正在运行的应用程序的内容。请注意,通常只在完全受信任的应用程序中使用此功能,因为将文件加载到正在运行的应用程序中会带来明显的安全隐患。 第 页 共 291 页 微软WPF应用 接下来的内容 本主题简单介绍了 XAML 语法概念和术语。 如果您尚未了解这些内容,请尝试阅读 Windows Presentation Foundation 入门教程。当您真正创建本教程中介绍的标记应用程序时,其中的练习将帮助您进一步了解本主题中介绍的许多概念。 WPF 使用一个特定的应用程序模型,该模型基于 Application 类。 生成 WPF 应用程序 (WPF) 为您详细介绍了如何通过命令行以及使用 Microsoft Visual Studio 生成包含 XAML 的应用程序。 依赖项属性概述详细介绍了 Windows Presentation Foundation (WPF) 中属性的多样性,并介绍了依赖项属性的概念。 最后,SDK 中还包含一个称为 XAMLPad 的 XAML 编辑工具。您可以使用此工具实时体验 XAML。 3. WPF基础之基元素 Windows Presentation Foundation (WPF) 中的大部分类都从四个类派生而来,这四个类在 SDK 文档中常常被称为基元素类。这些类包括 UIElement、FrameworkElement、ContentElement 和 FrameworkContentElement。DependencyObject 也是一个相关类,因为它是 UIElement 和 ContentElement 的通用基类。 WPF 类中的基元素 API UIElement 和 ContentElement 都是从 DependencyObject 派生而来,但途径略有不同。此级别上的拆分涉及到 UIElement 或 ContentElement 如何在用户界面上使用,以及它们在应用程序起到什么作用。UIElement 在其类层次结构中也有 Visual,该类为 Windows Presentation Foundation (WPF) 公开较低级别的图形支持。Visual 通过定义独立的矩形屏幕区域来提供呈现框架。实际上,UIElement 适用于支持大型数据模型的元素,这些元素用于在可以称为矩形屏幕区域的区域内进行呈现和布局,在该区域内,内容模型特意设置得更加开放,以允许不同的元素进行组合。ContentElement 不是从 Visual 派生的;它的模型由其他对象(例如,阅读器或查看器,用来解释元素并生成完整的 Visual 供 Windows Presentation Foundation (WPF) 使用)来使用 ContentElement。某些 UIElement 类可用作内容宿主:它们为一个或多个 ContentElement 类(如 DocumentViewer)提供宿主和呈现。ContentElement 用作以下元素的基类:所具有的对象模型较小,并且多用于寻址可能宿主在 UIElement 中的文本、信息或文档内容。 第 页 共 291 页 微软WPF应用 框架级和核心级 UIElement 用作 FrameworkElement 的基类,ContentElement 用作 FrameworkContentElement 的基类。对于此下一级类,原因是要支持与 WPF 框架级相分离的 WPF 核心级,这种分离还存在于 API 如何在 PresentationCore 和 PresentationFramework 程序集之间进行划分。WPF 框架级为基本应用程序需要提供了一个更完整的解决方案,包括用于表示的布局管理器的实现。WPF 核心级提供了一种方法,以充分利用 WPF,而又不至于产生附加程序集开销。对于大多数典型的应用程序开发方案而言,这些级别之间的区别很少有影响,而且一般情况下应将 WPF API 视为一个整体,而无需担心 WPF 框架级与 WPF 核心级之间有何区别。如果您的应用程序设计选择替换大量 WPF 框架级功能,例如,如果您的整体解决方案已经有其自己的用户界面 (UI) 组合和布局实现,则可能需要了解级别之间的差异。 选择从哪个元素派生 创建用于扩展 WPF 的自定义类的最实用方法是从某个 WPF 类中派生,这样您可以通过现有的类层次结构获得尽可能多的所需功能。本节列出了三个最重要的元素类附带的功能,以帮助您决定要从哪个类进行派生。 如果您要实现控件(这的确是从 WPF 类派生的更常见的原因之一),您可能需要从以下类中派生:实际控件、控件系列基类或至少是 Control 基类。 如果您不是创建控件,并且需要从层次结构中较高的类进行派生,则可以参考下列各节的内容,了解每个基元素类定义了哪些特征。 如果您创建从 DependencyObject 派生的类,则将继承以下功能: GetValue 和 SetValue 支持以及一般的属性系统支持。 使用依赖项属性以及作为依赖项属性实现的附加属性的能力。 如果您创建从 UIElement 派生的类,则除了能够继承 DependencyObject 提供的功能外,还将继承以下功能: 对动画属性值的基本支持。 对基本输入事件和命令的支持。 可以重写以便为布局系统提供信息的虚方法。 第 页 共 291 页 微软WPF应用 如果您创建从 FrameworkElement 派生的类,则除了能够继承 UIElement 提供的功能外,还将继承以下功能: 对样式设置和演示图板的支持。 对数据绑定的支持。 对动态资源引用的支持。 对属性值继承以及元数据中有助于向框架服务报告属性的相关情况(如数据绑定、样式或布局的框架实现)的其他标志的支持。 逻辑树的概念。 对布局系统的实际 WPF 框架级实现的支持,包括 OnPropertyChanged 重写(该重写可以检测到影响布局的属性更改)。 如果您创建从 ContentElement 派生的类,则除了能够继承 DependencyObject 提供的功能外,还将继承以下功能: 对动画的支持。 对基本输入事件和命令的支持。 如果您创建从 FrameworkContentElement 派生的类,则除了能够继承 ContentElement 提供的功能外,还将获得以下功能: 对样式设置和演示图板的支持。 对数据绑定的支持。 对动态资源引用的支持。 对属性值继承以及元数据中有助于向框架服务报告属性情况(如数据绑定、样式或布局的框架实现)的其他标志的支持。 您不会继承对布局系统修改(如 ArrangeOverride)的访问权限。布局系统实现只在 FrameworkElement 上提供。但是,您会继承 OnPropertyChanged 重写(可以检测影响布局的属性更改并将这些更改报告给任何内容宿主)。 记录了各种类的内容模型。如果您要找到一个合适的类以便从该类进行派生,其内容模型是一个应该考虑的可能因素。 其他基类 DispatcherObject DispatcherObject 为 WPF 线程模型提供支持,并允许为 WPF 应用程序创建的所有对象与 Dispatcher 相关联。即使您不从 UIElement, DependencyObject 或 Visual 派生,也应考虑从 DispatcherObject 派生,以获得此线程模型支持。 第 页 共 291 页 微软WPF应用 Visual Visual 实现二维对象在近似矩形的区域中通常需要具有可视化表示的概念。Visual 的实际呈现发生在其他类中(不是独立的),但是 Visual 类提供了一个由各种级别的呈现处理使用的已知类型。Visual 实现命中测试,但它不公开报告命中测试结果的事件(这些都位于 UIElement 中)。 Freezable Freezable 通过在出于性能原因需要不可变对象时提供为对象生成副本的途径,来模拟可变对象的不变性。Freezable 类型为某些图形元素(如几何形状、画笔以及动画)提供了一个通用的基础。值得注意的是,Freezable 不是一个 Visual;当应用 Freezable 以填充另一个对象的属性值时,它包含的属性将变成子属性,而这些子属性可能会影响呈现。 Animatable Animatable 是一个 Freezable 派生类,它特别添加了动画控件层和某些实用工具成员,从而使当前动画的属性可以与未动画的属性区分开。 Control Control 是称为控件或组件(取决于技术)的对象类型的理想基类。一般而言,WPF 控件类是直接表示 UI 控件或积极参与控件组合的类。Control 实现的主要功能是控件模板化。 4. WPF基础之属性系统 Windows Presentation Foundation (WPF) 提供了一组服务,这些服务可用于扩展公共语言运行库 (CLR) 属性的功能。这些服务通常统称为 WPF 属性系统。由 WPF 属性系统支持的属性称为依赖项属性。本概述介绍 WPF 属性系统以及依赖项属性的功能,这包括如何在可扩展应用程序标记语言 (XAML) 中和代码中使用现有的依赖项属性。本概述还介绍了依赖项属性所特有的方面(如依赖项属性元数据),并说明了如何在自定义类中创建自己的依赖项属性。 先决条件 本主题假设您在 CLR 和面向对象的编程方面有一些基础知识。若要采用本主题中的示例,还应当了解 XAML 并知道如何编写 WPF 应用程序。 依赖项属性和 CLR 属性 在 WPF 中,属性通常公开为公共语言运行库 (CLR) 属性。在基本级别,您可以在根本不知道这些属性实现为依赖项属性的情况下直接与它们交互。但是,您应当熟悉 WPF 属性系统的部分或全部功能,才能利用这些功能。 第 页 共 291 页 微软WPF应用 依赖项属性的用途在于提供一种方法来基于其他输入的值计算属性值。这些其他输入可以包括系统属性(如主题和用户首选项)、实时属性确定机制(如数据绑定和动画/演示图板)、重用模板(如资源和样式)或者通过与元素树中其他元素的父子关系来公开的值。另外,可以通过实现依赖项属性来提供独立验证、默认值、监视其他属性的更改的回调以及可以基于可能的运行时信息来强制指定属性值的系统。派生类还可以通过重写依赖项属性元数据(而不是重写现有属性的实际实现或者创建新属性)来更改现有属性的某些具体特征。 在 SDK 参考中,可以根据某个属性的托管引用页上是否存在“依赖项属性信息”部分来确定该属性是否为依赖项属性。“依赖项属性信息”部分包括一个指向该依赖项属性的 DependencyProperty 标识符字段的链接,还包括一个为该属性设置的元数据选项的列表、每个类的重写信息以及其他详细信息。 依赖项属性支持 CLR 属性 依赖项属性和 WPF 属性系统通过提供一个支持属性的类型来扩展属性功能,这是使用私有字段支持该属性的标准模式的替代实现方法。该类型的名称是 DependencyProperty。定义 WPF 属性系统的另一个重要类型是 DependencyObject。DependencyObject 定义可以注册和拥有依赖项属性的基类。 下面汇集了在本软件开发工具包 (SDK) 文档中,在讨论依赖项属性时所使用的术语: 依赖项属性:一个由 DependencyProperty 支持的属性。 依赖项属性标识符:一个 DependencyProperty 实例,在注册依赖项属性时作为返回值获得,之后将存储为一个类成员。在与 WPF 属性系统交互的许多 API 中,此标识符用作一个参数。 CLR“包装”:属性的实际 get 和 set 实现。这些实现通过在 GetValue 和 SetValue 调用中使用依赖项属性标识符来合并此标识符,从而使用 WPF 属性系统为属性提供支持。 下面的示例定义 IsSpinning 依赖项属性,并说明 DependencyProperty 标识符与它所支持的属性之间的关系。 第 页 共 291 页 微软WPF应用   属性以及支持它的 DependencyProperty 字段的命名约定非常重要。字段总是与属性同名,但其后面追加了 Property 后缀。 设置属性值 可以在代码或 XAML 中设置属性。 在 XAML 中设置属性值 下面的 XAML 示例将按钮的背景色指定为红色。该示例演示了这样一种情况:在所生成的代码中,XAML 加载器将 XAML 属性的简单字符串值的类型转换为 WPF 类型(一种 Color,通过 SolidColorBrush)。   XAML 支持各种设置属性的语法格式。要对特定的属性使用哪种语法取决于该属性所使用的值类型以及其他因素(例如,是否存在类型转换器)。 作为非属性语法的示例,下面的 XAML 示例显示了另一种按钮背景。这一次不是设置简单的纯色,而是将背景设置为图像,用一个元素表示该图像并将该图像的源指定为嵌套元素的属性。这是属性元素语法的示例。   在代码中设置属性 在代码中设置依赖项属性值通常只是调用由 CLR“包装”公开的 set 实现。   第 页 共 291 页 微软WPF应用 获取属性值实质上也是在调用 get“包装”实现:   您还可以直接调用属性系统 API GetValue 和 SetValue。如果您使用的是现有属性,则上述操作通常不是必需的(使用包装会更方便,并能够更好地向开发人员工具公开属性)。但是在某些情况下适合直接调用 API。 还可以在 XAML 中设置属性,然后通过代码隐藏在代码中访问这些属性。 由依赖项属性提供的属性功能 依赖项属性提供用来扩展属性功能的功能,这与字段支持的属性相反。每个这样的功能通常都表示或支持整套 WPF 功能中的特定功能: 资源 数据绑定 样式 动画 元数据重写 属性值继承 WPF 设计器集成 资源 依赖项属性值可以通过引用资源来设置。资源通常指定为页面根元素或应用程序的子元素,通过这些位置可以最方便地访问资源。下面的示例演示如何定义 SolidColorBrush 资源。   在定义了某个资源之后,可以引用该资源并使用它来提供属性值:   这个特定的资源称为 DynamicResource 标记扩展(在 XAML 中,可以使用静态或动态资源引用)。若要使用动态资源引用,必须设置为依赖项属性,因此它是由 WPF 属性系统明确启用的动态资源引用用法。 第 页 共 291 页 微软WPF应用 说明: 资源被视为本地值,这意味着,如果您设置另一个本地值,该资源引用将被消除。   数据绑定 依赖项属性可以通过数据绑定来引用值。数据绑定通过特定的标记扩展语法(在 XAML 中)或 Binding 对象(在代码中)来工作。使用数据绑定,最终属性值的确定将延迟到运行时,在运行时,将从数据源获取属性值。 下面的示例在 XAML 中使用一个绑定,为 Button 设置 Content 属性。该绑定使用一个继承的数据上下文和一个 XmlDataProvider 数据源(未显示出来)。绑定本身通过数据源中的 XPath 指定所需的源属性。   绑定被视为本地值,这意味着,如果您设置另一个本地值,该绑定将被消除。 依赖项属性或 DependencyObject 类本身并不支持 INotifyPropertyChanged,以便为数据绑定操作生成有关 DependencyObject 源属性值变化的通知。 样式 样式和模板是使用依赖项属性的两个主要激发方案。在设置定义应用程序用户界面 (UI) 的属性时,样式尤其有用。样式在 XAML 中通常定义为资源。样式与属性系统交互,因为它们通常包含特定属性的“setter”,以及基于另一个属性的实时值更改属性值的“trigger”。 下面的示例创建一个非常简单的样式(该样式将在 Resources 字典中定义,未显示出来),然后将该样式直接应用于 Button 的 Style 属性。样式中的 setter 将带样式的 Button 的 Background 属性设置为 green。     动画 可以对依赖项属性进行动画处理。在应用和运行动画时,经过动画处理的值的操作优先级将高于该属性以其他方式具有的任何值(如本地值)。 第 页 共 291 页 微软WPF应用 下面的示例对 Button 属性的 Background 进行动画处理(在技术上,Background 是通过使用属性元素语法将空白 SolidColorBrush 指定为 Background 来进行动画处理的,之后,该 SolidColorBrush 的 Color 属性就是直接进行动画处理的属性)。   元数据重写 在从最初注册依赖项属性的类派生时,可以通过重写依赖项属性的元数据来更改该属性的某些行为。对元数据的重写依赖于 DependencyProperty 标识符。重写元数据不需要重新实现属性。元数据的变化是由属性系统在本机处理的;对于所有从基类继承的属性,每个类都有可能基于每个类型保留元数据。 下面的示例重写依赖项属性 DefaultStyleKey 的元数据。重写这个特定的依赖项属性的元数据是某个实现模式的一部分,该模式创建可以使用主题中的默认样式的控件。   属性值继承 元素可以从其在树中的父级继承依赖项属性的值。 说明: 属性值继承行为并未针对所有的依赖项属性在全局启用,因为继承的计算时间确实会对性能产生一定的影响。属性值继承通常只有在特定方案指出适合使用属性值继承时才对属性启用。可以通过在 SDK 参考中查看某个依赖项属性的“依赖项属性信息”部分,来确定该依赖项属性是否继承属性值。 第 页 共 291 页 微软WPF应用 下面的示例演示一个绑定,并设置指定绑定(在前面的绑定示例中未显示出来)的源的 DataContext 属性。DataContext 属性的值继承,因此子元素中的任何后续绑定都不必遵守在父级 StackPanel 元素中指定为 DataContext 的源。   WPF 设计器集成 如果自定义控件具有实现为依赖项属性的属性,则它将收到相应的 Visual Studio Windows Presentation Foundation (WPF) 设计器支持。一个示例就是能够在“属性”窗口中编辑直接依赖项属性和附加依赖项属性。 依赖项属性值优先级 当您获取依赖项属性的值时,可能会获得通过其他参与 WPF 属性系统且基于属性的任一输入而在该属性上设置的值。由于存在依赖项属性值优先级,使得属性获取值的方式的各种方案得以按可预测的方式交互。 请看下面的示例。该示例包括一个应用于所有按钮及其 Background 属性的样式,但是之后还指定了一个具有在本地设置的 Background 值的按钮。 说明: SDK 文档在讨论依赖项属性时有时会使用“本地值”或“本地设置的值”等术语。本地设置的值是指在代码中直接为对象实例设置的属性 (Property) 值,或者在 XAML 中设置为元素属性 (Attribute) 的属性 (Property) 值。  实际上,对于第一个按钮,该属性设置了两次,但是仅应用了一个值,即,具有最高优先级的值。本地设置的值具有最高优先级(对于正在运行的动画除外,但是在本示例中没有应用动画),因此,对于第一个按钮的背景将使用本地设置的值,而不使用样式 setter 值。第二个按钮没有本地值(而且没有其他比样式 setter 优先级更高的值),因此该按钮中的背景将来自样式 setter。   第 页 共 291 页 微软WPF应用 为什么存在依赖项属性优先级? 通常,您不会希望总是应用样式,而且不希望样式遮盖单个元素的哪怕一个本地设置值(否则,通常将很难使用样式或元素)。因此,来自样式的值的操作优先级将低于本地设置的值。 说明: 在 WPF 元素定义了许多非依赖项属性的属性。一般说来,只有在需要支持至少一个由属性系统启用的方案(数据绑定、样式、动画、默认值支持、继承、附加属性或失效)时,才将属性实现为依赖项属性。 了解有关依赖项属性的更多信息 附加属性是一种类型的属性,它支持 XAML 中的专用语法。附加属性通常与公共语言运行库 (CLR) 属性不具有 1:1 对应关系,而且不一定是依赖项属性。附加属性的典型用途是使子元素可以向其父元素报告属性值,即使父元素和子元素的类成员列表中均没有该属性也是如此。一个主要方案是,使子元素可以将其在 UI 中的表示方式通知给父级。 组件开发人员或应用程序开发人员可能希望创建自己的依赖项属性,以便实现数据绑定或样式支持之类的功能,或者实现对失效和强制指定值的支持。 通常,依赖项属性应当被视为公共属性,这些公共属性可以由任何具有实例访问权限的调用方访问,或至少可被这样的调用方发现。 5. WPF基础之路由事件 本主题描述 Windows Presentation Foundation (WPF) 中路由事件的概念。本主题定义路由事件术语,描述路由事件如何通过元素树来路由,概述如何处理路由事件,并介绍如何创建您自己的自定义路由事件。 先决条件 本主题假设您对如下内容有基本的了解:公共语言运行库 (CLR)、面向对象的编程以及如何用树的概念来说明 WPF 元素之间的关系。为了按照本主题中的示例操作,您还应当了解可扩展应用程序标记语言 (XAML) 并知道如何编写非常基本的 WPF 应用程序或页。 什么是路由事件? 可以从功能或实现的角度来考虑路由事件。此处对这两种定义均进行了说明,因为用户当中有的认为前者更有用,而有的则认为后者更有用。 功能定义:路由事件是一种可以针对元素树中的多个侦听器(而不是仅针对引发该事件的对象)调用处理程序的事件。 实现定义:路由事件是一个 CLR 事件,可以由 RoutedEvent 类的实例提供支持并由 Windows Presentation Foundation (WPF) 事件系统来处理。 第 页 共 291 页 微软WPF应用 典型的 WPF 应用程序中包含许多元素。无论这些元素是在代码中创建的还是在 XAML 中声明的,它们都由共同所在的元素树关联起来。根据事件的定义,事件路由可以按两种方向之一传播,但是通常会在元素树中从源元素向上“冒泡”,直到它到达元素树的根(通常是页面或窗口)。如果您以前用过 DHTML 对象模型,则可能会熟悉这个冒泡概念。 请考虑下面的简单元素树:   此元素树生成类似如下的内容: 在这个简化的元素树中,Click 事件的源是某个 Button 元素,而所单击的 Button 是有机会处理该事件的第一个元素。但是,如果附加到 Button 的任何处理程序均未作用于该事件,则该事件将向上冒泡到元素树中的 Button 父级(即 StackPanel)。该事件可能会冒泡到 Border,然后会到达元素树的页面根(未显示出来)。 换言之,此 Click 事件的事件路由为: Button-->StackPanel-->Border-->... 路由事件的顶级方案 下面简要概述了需运用路由事件的方案,以及为什么典型的 CLR 事件不适合这些方案: 控件的撰写和封装:WPF 中的各个控件都有一个丰富的内容模型。例如,可以将图像放在 Button 的内部,这会有效地扩展按钮的可视化树。但是,所添加的图像不得中断命中测试行为(该行为会使按钮响应对图像内容的 Click),即使用户所单击的像素在技术上属于该图像也是如此 单一处理程序附加点:在 Windows 窗体中,必须多次附加同一个处理程序,才能处理可能是从多个元素引发的事件。路由事件使您可以只附加该处理程序一次(像上例中那样),并在必要时使用处理程序逻辑来确定该事件源自何处。例如,这可以是前面显示的 XAML 的处理程序: 第 页 共 291 页 微软WPF应用   类处理:路由事件允许使用由类定义的静态处理程序。这个类处理程序能够抢在任何附加的实例处理程序之前来处理事件。 引用事件,而不反射:某些代码和标记技术需要能标识特定事件的方法。路由事件创建 RoutedEvent 字段作为标识符,以此提供不需要静态反射或运行时反射的可靠的事件标识技术。 路由事件的实现方式 路由事件是一个 CLR 事件,它由 RoutedEvent 类提供支持并用 WPF 事件系统注册。从注册中获取的 RoutedEvent 实例通常保留为某种类的 public static readonly 字段成员,该类进行了注册并因此“拥有”路由事件。与同名 CLR 事件(有时称为“包装”事件)的连接是通过重写 CLR 事件的 add 和 remove 实现来完成的。通常,add 和 remove 保留为隐式默认值,该默认值使用特定于语言的相应事件语法来添加和移除该事件的处理程序。路由事件的支持和连接机制在概念上与以下机制相似:依赖项属性是一个 CLR 属性,该属性由 DependencyProperty 类提供支持并用 WPF 属性系统注册。 下面的示例演示自定义 Tap 路由事件的声明,其中包括注册和公开 RoutedEvent 标识符字段以及对 Tap CLR 事件进行 add 和 remove 实现。   第 页 共 291 页 微软WPF应用   路由事件处理程序和 XAML 若要使用 XAML 为某个事件添加处理程序,请将该事件的名称声明为用作事件侦听器的元素上的属性。该属性的值是所实现的处理程序方法的名称,该方法必须存在于代码隐藏文件的分部类中。   用来添加标准 CLR 事件处理程序的 XAML 语法与用来添加路由事件处理程序的语法相同,因为您实际上是在向下面具有路由事件实现的 CLR 事件包装中添加处理程序。 路由策略 路由事件使用以下三个路由策略之一: 冒泡:针对事件源调用事件处理程序。路由事件随后会路由到后续的父元素,直到到达元素树的根。大多数路由事件都使用冒泡路由策略。冒泡路由事件通常用来报告来自不同控件或其他 UI 元素的输入或状态变化。 直接:只有源元素本身才有机会调用处理程序以进行响应。这与 Windows 窗体用于事件的“路由”相似。但是,与标准 CLR 事件不同的是,直接路由事件支持类处理(类处理将在下一节中介绍)而且可以由 EventSetter 和 EventTrigger 使用。 隧道:最初将在元素树的根处调用事件处理程序。随后,路由事件将朝着路由事件的源节点元素(即引发路由事件的元素)方向,沿路由线路传播到后续的子元素。在合成控件的过程中通常会使用或处理隧道路由事件,这样,就可以有意地禁止显示复合部件中的事件,或者将其替换为特定于整个控件的事件。在 WPF 中提供的输入事件通常是以隧道/冒泡对实现的。隧道事件有时又称作 Preview 事件,这是由隧道/冒泡对所使用的命名约定决定的。 第 页 共 291 页 微软WPF应用 为什么使用路由事件? 作为应用程序开发人员,您不需要始终了解或关注正在处理的事件是否作为路由事件实现。路由事件具有特殊的行为,但是,如果您在引发该行为的元素上处理事件,则该行为通常会不可见。 如果您使用以下任一建议方案,路由事件的功能将得到充分发挥:在公用根处定义公用处理程序、合成自己的控件或者定义您自己的自定义控件类。 路由事件侦听器和路由事件源不必在其层次结构中共享公用事件。任何 UIElement 或 ContentElement 可以是任一路由事件的事件侦听器。因此,您可以使用在整个工作 API 集内可用的全套路由事件作为概念“接口”,应用程序中的不同元素凭借这个接口来交换事件信息。路由事件的这个“接口”概念特别适用于输入事件。 路由事件还可以用来通过元素树进行通信,因为事件的事件数据会永存到路由中的每个元素中。一个元素可以更改事件数据中的某项内容,该更改将对于路由中的下一个元素可用。 之所以将任何给定的 WPF 事件作为路由事件实现(而不是作为标准 CLR 事件实现),除了路由方面的原因,还有两个其他原因。如果您要实现自己的事件,则可能也需要考虑这两个因素: 某些 WPF 样式和模板功能(如 EventSetter 和 EventTrigger)要求所引用的事件是路由事件。前面提到的事件标识符方案就是这样的。 路由事件支持类处理机制,类可以凭借该机制来指定静态方法,这些静态方法能够在任何已注册的实例程序访问路由事件之前,处理这些路由事件。这在控件设计中非常有用,因为您的类可以强制执行事件驱动的类行为,以防它们在处理实例上的事件时被意外禁止。 本主题将用单独的章节来讨论每个考虑因素。 为路由事件添加和实现事件处理程序 若要在 XAML 中添加事件处理程序,只需将相应的事件名称作为一个属性添加到某个元素中,并将该属性的值设置为用来实现相应委托的事件处理程序的名称,如下面的示例中所示。   b1SetColor 是所实现的处理程序的名称,该处理程序中包含用来处理 Click 事件的代码。b1SetColor 必须具有与 RoutedEventHandler 委托相同的签名,该委托是 Click 事件的事件处理程序委托。所有路由事件处理程序委托的第一个参数都指定要向其中添加事件处理程序的元素,第二个参数指定事件的数据。 第 页 共 291 页 微软WPF应用   RoutedEventHandler 是基本的路由事件处理程序委托。对于针对某些控件或方案而专门处理的路由事件,要用于路由事件处理程序的委托还可能会变得更加专用化,因此它们可以传输专用的事件数据。例如,在常见的输入方案中,可以处理 DragEnter 路由事件。您的处理程序应当实现 DragEventHandler 委托。借助更具体的委托,可以对处理程序中的 DragEventArgs 进行处理,并读取 Data 属性,该属性中包含拖动操作的剪贴板内容。 在用代码创建的应用程序中为路由事件添加处理程序非常简单。路由事件处理程序始终可以通过帮助器方法 AddHandler 来添加,现有支持为 add 调用的也是此方法。但是,现有的 WPF 路由事件通常借助于支持机制来实现 add 和 remove 逻辑,这些逻辑允许使用特定于语言的事件语法来添加路由事件的处理程序,特定于语言的事件语法比帮助器方法更直观。下面是帮助器方法的示例用法:   下一个示例演示 C# 运算符语法(visual Basic 的运算符语法稍有不同,因为它以不同的方法来处理取消引用):   如果使用的是 Visual Basic,则还可以使用 Handles 关键字,将处理程序作为处理程序声明的一部分来添加。 第 页 共 291 页 微软WPF应用 “已处理”概念 所有的路由事件都共享一个公用的事件数据基类 RoutedEventArgs。RoutedEventArgs 定义了一个采用布尔值的 Handled 属性。Handled 属性的目的在于,允许路由中的任何事件处理程序通过将 Handled 的值设置为 true 来将路由事件标记为“已处理”。处理程序在路由路径上的某个元素处对共享事件数据进行处理之后,这些数据将再次报告给路由路径上的每个侦听器。 Handled 的值影响路由事件在沿路由线路向远处传播时的报告或处理方式。在路由事件的事件数据中,如果 Handled 为 true,则通常不再为该特定事件实例调用负责在其他元素上侦听该路由事件的处理程序。这条规则对以下两类处理程序均适用:在 XAML 中附加的处理程序;由语言特定的事件处理程序附加语法(如 += 或 Handles)添加的处理程序。对于最常见的处理程序方案,如果将 Handled 设置为 true,以此将事件标记为“已处理”,则将“停止”隧道路由或冒泡路由,同时,类处理程序在某个路由点处处理的所有事件的路由也将“停止”。 但是,侦听器仍可以凭借“handledEventsToo”机制来运行处理程序,以便在事件数据中的 Handled 为 true 时响应路由事件。换言之,将事件数据标记为“已处理”并不会真的停止事件路由。您只能在代码或 EventSetter 中使用 handledEventsToo 机制: 在代码中,不使用适用于一般 CLR 事件的特定于语言的事件语法,而是通过调用 WPF 方法 AddHandler(RoutedEvent, Delegate, Boolean) 来添加处理程序。使用此方法时,请将 handledEventsToo 的值指定为 true。 在 EventSetter 中,请将 HandledEventsToo 属性设置为 true。 除了 Handled 状态在路由事件中生成的行为以外,Handled 概念还暗示您应当如何设计自己的应用程序和编写事件处理程序代码。可以将 Handled 概念化为由路由事件公开的简单协议。此协议的具体使用方法由您来决定,但是需要按照如下方式来对 Handled 值的预期使用方式进行概念设计: 如果路由事件标记为“已处理”,则它不必由该路由中的其他元素再次处理。 如果路由事件未标记为“已处理”,则说明该路由中前面的其他侦听器已经选择了不注册处理程序,或者已经注册的处理程序选择不操作事件数据并将 Handled 设置为 true。(或者,当前的侦听器很可能就是路由中的第一个点。) 当前侦听器上的处理程序现在有三个可能的操作方案: 不执行任何操作;该事件保持未处理状态,该事件将路由到下一个侦听器。 执行代码以响应该事件,但是所执行的操作被视为不足以保证将事件标记为“已处理”。该事件将路由到下一个侦听器。 第 页 共 291 页 微软WPF应用 执行代码以响应该事件。在传递到处理程序的事件数据中将该事件标记为“已处理”,因为所执行的操作被视为不足以保证将该事件标记为“已处理”。该事件仍将路由到下一个侦听器,但是,由于其事件数据中存在 Handled=true,因此只有 handledEventsToo 侦听器才有机会调用进一步的处理程序。 这个概念设计是通过前面提到的路由行为来加强的:即使路由中前面的处理程序已经将 Handled 设置为 true,也会增加为所调用的路由事件附加处理程序的难度(尽管仍可以在代码或样式中实现这一目的)。 有关 Handled、路由事件的类处理的更多信息,以及针对何时适合将路由事件标记为 Handled 的建议,请参见将路由事件标记为已处理以及类处理。 在应用程序中,相当常见的做法是只针对引发冒泡路由事件的对象来处理该事件,而根本不考虑事件的路由特征。但是,在事件数据中将路由事件标记为“已处理”仍是一个不错的做法,因为这样可以防止元素树中位置更高的元素也对同一个路由事件附加了处理程序而出现意外的副作用。 类处理程序 如果您定义的类是以某种方式从 DependencyObject 派生的,那么对于作为类的已声明或已继承事件成员的路由事件,还可以定义和附加一个类处理程序。每当路由事件到达其路由中的元素实例时,都会先调用类处理程序,然后再调用附加到该类某个实例的任何实例侦听器处理程序。 有些 WPF 控件对某些路由事件具有固有的类处理。路由事件可能看起来从未引发过,但实际上正对其进行类处理,如果您使用某些技术的话,路由事件可能仍由实例处理程序进行处理。同样,许多基类和控件都公开可用来重写类处理行为的虚方法。 WPF 中的附加事件 XAML 语言还定义了一个名为“附加事件”的特殊类型的事件。使用附加事件,可以将特定事件的处理程序添加到任意元素中。正在处理该事件的元素不必定义或继承附加事件,可能引发这个特定事件的对象和用来处理实例的目标也都不必将该事件定义为类成员或将其作为类成员来“拥有”。 WPF 输入系统广泛地使用附加事件。但是,几乎所有的附加事件都是通过基本元素转发的。输入事件随后会显示为等效的、作为基本元素类成员的非附加路由事件。例如,通过针对该 UIElement 使用 MouseDown(而不是在 XAML 或代码中处理附加事件语法),可以针对任何给定的 UIElement 更方便地处理基础附加事件 Mouse..::.MouseDown。 XAML 中的限定事件名称 为子元素所引发的路由事件附加处理程序是另一个语法用法,它与类型名称.事件名称 附加事件语法相似,但它并非严格意义上的附加事件用法。可以向公用父级附加处理程序以利用事件路由,即使公用父级可能不将相关的路由事件作为其成员也是如此。请再次考虑下面的示例: 第 页 共 291 页 微软WPF应用   在这里,在其中添加处理程序的父元素侦听器是 StackPanel。但是,它正在为已经声明而且将由 Button 类(实际上是 ButtonBase,但是可以由 Button 通过继承来使用)引发的路由事件添加处理程序。Button“拥有”该事件,但是路由事件系统允许将任何路由事件的处理程序附加到任何 UIElement 或 ContentElement 实例侦听器,该侦听器可能会以其他方式为公共语言运行库 (CLR) 事件附加侦听器。对于这些限定的事件属性名来说,默认的 xmlns 命名空间通常是默认的 WPF xmlns 命名空间,但是您还可以为自定义路由事件指定带有前缀的命名空间。 WPF 输入事件 路由事件在 WPF 平台中的常见用法之一是用于事件输入。在 WPF 中,按照约定,隧道路由事件的名称以单词“Preview”开头。输入事件通常成对出现,一个是冒泡事件,另一个是隧道事件。例如,KeyDown 事件和 PreviewKeyDown 事件具有相同的签名,前者是冒泡输入事件,后者是隧道输入事件。偶尔,输入事件只有冒泡版本,或者有可能只有直接路由版本。在本文档中,当存在具有替换路由策略的类似路由事件时,路由事件主题交叉引用它们,而且托管引用页面中的各个节阐释每个路由事件的路由策略。 实现成对出现的 WPF 输入事件的目的在于,使来自输入的单个用户操作(如按鼠标按钮)按顺序引发该对中的两个路由事件。首先引发隧道事件并沿路由传播,然后引发冒泡事件并沿其路由传播。顾名思义,这两个事件会共享同一个事件数据实例,因为用来引发冒泡事件的实现类中的 RaiseEvent 方法调用会侦听隧道事件中的事件数据并在新引发的事件中重用它。具有隧道事件处理程序的侦听器首先获得将路由事件标记为“已处理”的机会(先是类处理程序,后是实例处理程序)。如果隧道路由中的某个元素将路由事件标记为“已处理”,则会针对冒泡事件发送已经处理的事件数据,而且将不调用为等效的冒泡输入事件附加的典型处理程序。已处理的冒泡事件看起来好像尚未引发过。此处理行为对于控件合成非常有用,因为此时您可能希望所有基于命中测试的输入事件或者所有基于焦点的输入事件都由最终的控件(而不是它的复合部件)报告。作为可支持控件类的代码的一部分,最后一个控件元素靠近合成链中的根,因此将有机会首先对隧道事件进行类处理,或许还有机会将该路由事件“替换”为更特定于控件的事件。 第 页 共 291 页 微软WPF应用 为了说明输入事件处理的工作方式,请考虑下面的输入事件示例。在下面的树插图中,leaf element #2 是先后发生的 PreviewMouseDown 事件和 MouseDown 事件的源。 输入事件的冒泡和隧道   事件的处理顺序如下所示: 针对根元素处理 PreviewMouseDown(隧道)。 针对中间元素 1 处理 PreviewMouseDown(隧道)。 针对源元素 2 处理 PreviewMouseDown(隧道)。 针对源元素 2 处理 MouseDown(冒泡)。 针对中间元素 1 处理 MouseDown(冒泡)。 针对根元素处理 MouseDown(冒泡)。 路由事件处理程序委托提供对以下两个对象的引用:引发该事件的对象以及在其中调用处理程序的对象。在其中调用处理程序的对象是由 sender 参数报告的对象。首先在其中引发事件的对象是由事件数据中的 Source 属性报告的。路由事件仍可以由同一个对象引发和处理,在这种情况下,sender 和 Source 是相同的(事件处理示例列表中的步骤 3 和 4 就是这样的情况)。 由于存在隧道和冒泡,因此父元素接收 Source 作为其子元素之一的输入事件。当有必要知道源元素是哪个元素时,可以通过访问 Source 属性来标识源元素。 通常,一旦将输入事件标记为 Handled,就将不进一步调用处理程序。通常,一旦调用了用来对输入事件的含义进行特定于应用程序的逻辑处理的处理程序,就应当将输入事件标记为“已处理”。 第 页 共 291 页 微软WPF应用 对于这个有关 Handled 状态的通用声明有一个例外,那就是,注册为有意忽略事件数据 Handled 状态的输入事件处理程序仍将在其路由中被调用。 通常,隧道事件和冒泡事件之间的共享事件数据模型以及先引发隧道事件后引发冒泡事件等概念并非对于所有的路由事件都适用。该行为的实现取决于 WPF 输入设备选择引发和连接输入事件对的具体方式。实现自己的输入事件是一个高级方案,但是您也可以选择针对自己的输入事件遵循该模型。 一些类选择对某些输入事件进行类处理,其目的通常是重新定义用户驱动的特定输入事件在该控件中的含义并引发新事件。 EventSetter 和 EventTrigger 在样式中,可以通过使用 EventSetter 在标记中包括某个预先声明的 XAML 事件处理语法。在应用样式时,所引用的处理程序会添加到带样式的实例中。只能针对路由事件声明 EventSetter。下面是一个示例。请注意,此处引用的 b1SetColor 方法位于代码隐藏文件中。   这样做的好处在于,样式有可能包含大量可应用于应用程序中任何按钮的其他信息,让 EventSetter 成为该样式的一部分甚至可以提高代码在标记级别的重用率。而且,EventSetter 还进一步从通用的应用程序和页面标记中提取处理程序方法的名称。 另一个将 WPF 的路由事件和动画功能结合在一起的专用语法是 EventTrigger。与 EventSetter 一样,只有路由事件可以用于 EventTrigger。通常将 EventTrigger 声明为样式的一部分,但是还可以在页面级元素上将 EventTrigger 声明为 Triggers 集合的一部分或者在 ControlTemplate 中对其进行声明。使用 EventTrigger,可以指定当路由事件到达其路由中的某个元素(这个元素针对该事件声明了 EventTrigger)时将运行的 Storyboard。与只是处理事件并且会导致它启动现有演示图板相比,EventTrigger 的好处在于,EventTrigger 对演示图板及其运行时行为提供更好的控制。 第 页 共 291 页 微软WPF应用 本主题主要从以下角度讨论路由事件:描述基本概念;就如何以及何时响应各种基元素和基控件中已经存在的路由事件提供指南。但是,您可以为自定义类创建自己的路由事件,还可以创建所有必要的支持(如专用的事件数据类和委托)。路由事件的所有者可以是任何类,但是路由事件只有在由 UIElement 或 ContentElement 派生类引发和处理之后才有用。 可扩展应用程序标记语言 (XAML) 定义了一个语言组件和称为“附加事件”的事件类型。附加事件的概念允许您针对特定事件为任意元素(而不是为实际定义或继承该事件的元素)添加处理程序。在这种情况下,对象既不会引发该事件,目标处理实例也不会定义或“拥有”该事件。 先决条件 本主题假定您已阅读路由事件概述和 XAML 概述。 附加事件语法 附加事件具有一种 XAML 语法和编码模式,后备代码必须使用该语法和编码模式才支持附加事件的使用。 在 XAML 语法中,不仅可以通过事件名称来指定附加事件,而且还可以通过用点 (.) 分隔的事件拥有类型加上事件名称来指定。因为事件名称是使用其拥有类型的名称限定的,所以附加事件语法允许将任何附加事件附加到可以实例化的任何元素上。 例如,下面是为自定义 NeedsCleaning 附加事件附加处理程序的 XAML 语法:   请注意 aqua: 前缀;该前缀在本例中是必需的,因为附加事件是来自自定义映射 xmlns 的自定义事件。 如何在 WPF 中实现附加事件 在 WPF 中,附加事件由 RoutedEvent 字段来支持,并在引发后通过元素树进行路由。通常,附加事件的源(引发该事件的对象)是系统或服务源,所以运行引发该事件的代码的对象并不是元素树的直接组成部分。 附加事件的方案 在 WPF 中,附加事件存在于具有服务级抽象的某些功能区域,例如,对于通过静态 Mouse 类或 Validation 类实现的事件。与服务交互或使用服务的类可以在附加事件语法中使用该事件,也可以选择将附加事件作为路由事件来实现(这是类如何集成服务功能的一部分)。 第 页 共 291 页 微软WPF应用 尽管 WPF 定义了许多附加事件,但您直接使用或处理附加事件的方案却很有限。一般情况下,附加事件用于实现体系结构目的,但随后即被转发给非附加(使用 CLR 事件“包装”提供支持)路由事件。 例如,通过针对该 UIElement 使用 MouseDown(而不是在 XAML 或代码中处理附加事件语法),可以针对任何给定的 UIElement 更方便地处理基础附加事件 Mouse..::.MouseDown。附加事件用于实现体系结构目的,因为它允许进一部扩展输入设备。假设的设备只需引发 Mouse..::.MouseDown 即可模拟鼠标输入,而不需要从 Mouse 派生。但是,此方案会涉及事件的代码处理,而附加事件的 XAML 处理则与此方案无关。 在 WPF 中处理附加事件 处理附加事件的过程以及您将要编写的处理程序代码与路由事件基本相同。 一般情况下,WPF 附加事件与 WPF 路由事件并没有太大的区别。不同之处在于如何确定事件的源,以及如何通过类将事件作为成员进行公开。(这还将影响 XAML 处理程序语法。) 但是,正如前文所述,现有的 WPF 附加事件并不是专门用于在 WPF 中进行处理。该事件的目的常常是实现合成元素,以便向合成中的父元素报告某个状态,在这种情况下,该事件常常在代码中引发,并且还依赖于相关父类中的类处理。例如,Selector 中的项应引发附加的 Selected 事件,该事件随后将由 Selector 类进行类处理,然后可能由 Selector 类转换为不同的路由事件 SelectionChanged。 将您自己的附加事件定义为路由事件 如果您从通用 WPF 基类派生,则可以通过在类中包括某些模式方法并使用基类中已经存在的实用工具方法来实现您自己的附加事件。 模式如下: 带有两个参数的方法 Add*Handler。第一个参数必须标识事件,而标识的事件必须与方法名称中带有 * 的名称相匹配。第二个参数是要添加的处理程序。该方法必须是公共且静态的,没有任何返回值。 带有两个参数的方法 Remove*Handler。第一个参数必须标识事件,而标识的事件必须与方法名称中带有 * 的名称相匹配。第二个参数是要移除的处理程序。该方法必须是公共且静态的,没有任何返回值。 当针对某个元素声明附加事件处理程序属性时,Add*Handler 访问器方法可以加快 XAML 处理。Add*Handler 和 Remove*Handler 方法还可实现对附加事件的事件处理程序存储区的代码访问。 第 页 共 291 页 微软WPF应用 这个普通模式对于框架中的实际实现还不够精确,因为任何给定的 XAML 读取器实现都可能采用不同的架构在支持语言和体系结构中标识基础事件。这是 WPF 将附加事件作为路由事件来实现的原因之一;用于事件的标识符 (RoutedEvent) 已经由 WPF 事件系统进行定义。而且,路由一个事件也是对附加事件的 XAML 语言级概念的自然实现扩展。 WPF 附加事件的 Add*Handler 实现包括将路由事件和处理程序用作参数来调用 AddHandler。 此实现策略和路由事件系统一般将附加事件的处理限制为 UIElement 派生类或 ContentElement 派生类,因为只有这些类才具有 AddHandler 实现。 例如,下面的代码按照 WPF 将附加事件作为路由事件进行声明的附加事件策略为所有者类 Aquarium 定义 NeedsCleaning 附加事件。   请注意,用来确定附加事件标识符字段的方法 RegisterRoutedEvent 实际上与用来注册非附加路由事件的方法相同。附加事件和路由事件都是在集中管理的内部存储区中进行注册的。此事件存储区实现则实现了路由事件概述中讨论的“事件作为界面”概念方面的注意事项。 引发 WPF 附加事件 通常不需要从代码中引发 WPF 定义的现有附加事件。这些事件采用一般的“服务”概念模型,而服务类(如 InputManager)则负责引发事件。 但是,如果您根据将 RoutedEvent 作为附加事件基础的 WPF 模型来定义自定义附加事件,则可以使用 RaiseEvent 从任何 UIElement 或 ContentElement 中引发附加事件。引发路由事件(不管是否附加)要求您在元素树中声明一个特定的元素作为事件源;该事件源被报告为 RaiseEvent 调用方。您的服务负责决定将哪个元素报告为元素树中的事件源。 6. WPF基础之布局系统 本主题描述 Windows Presentation Foundation (WPF) 布局系统。了解在构造外观醒目、性能优良的用户界面时如何以及何时进行布局计算是非常重要的。 布局系统 术语“布局”描述测量和排列 Panel 元素的 Children 集合的成员、然后在屏幕上绘制它们的过程。这是一个计算密集型过程,即 Children 集合越大,执行的计算次数就越多。根据拥有该集合的 Panel 元素所定义的布局行为,还可能会增加复杂性。如果不需要较为复杂的 Panel(如 Grid),则可以使用构造相对简单的布局(如 Canvas),这种布局可产生更佳的性能。 第 页 共 291 页 微软WPF应用 每当子 UIElement 改变其位置时,布局系统就可能触发一个新的处理过程。因此,了解哪些事件会调用布局系统就很重要,因为不必要的调用可能导致应用程序性能变差。 简单地说,布局是一个递归系统,实现在屏幕上对元素进行大小调整、定位和绘制。布局系统为 Children 集合的每个成员完成两个处理过程:测量处理过程和排列处理过程。每个子 Panel 均提供自己的 MeasureOverride 和 ArrangeOverride 方法,以实现自己特定的布局行为。不论何时调用布局系统,都会发生以下系列事件。 子 UIElement 通过首先测量它的核心属性来开始布局过程。 计算在 FrameworkElement 上定义的大小调整属性,例如 Width、Height 和 Margin。 应用 Panel 特定逻辑,例如 Dock 方向或堆栈 Orientation。 测量所有子级后排列内容。 Children 集合绘制到屏幕。 如果其他 Children 添加到集合、应用 LayoutTransform 或调用 UpdateLayout 方法,会再次调用此过程。 下面的小节将更详尽地定义此过程及其调用方式。 元素边界框 在 Windows Presentation Foundation (WPF) 中构思应用程序布局时,了解环绕所有元素的边界框非常重要。这有助于您理解布局系统的行为。布局系统使用的每个 FrameworkElement 可以被视为是嵌入到布局分区中的矩形。LayoutInformation 类会公开,可以返回元素布局分配的几何边界或槽。矩形的大小是由系统通过计算可用屏幕空间、任意约束的大小、布局特定属性(如边距和填充)及父 Panel 元素的个别行为来确定的。通过处理此数据,系统将能够计算给定的 Panel 的所有子级的位置。牢记在父元素上定义的哪些大小调整特性(如 Border)会影响其子级,这非常重要。 例如,请考虑下面的简单布局方案。   第 页 共 291 页 微软WPF应用 可以使用以下可扩展应用程序标记语言 (XAML) 来实现此布局。   此单独的 TextBlock 元素是在 Grid 内承载的,而文本仅填充在其所在列的左上角,为 TextBlock 分配的空间实际更大。可以使用 GetLayoutSlot 方法检索任意 FrameworkElement 的边界框。使用此方法,TextBlock 元素的边界框是叠加的,这可能是因为 TextBlock 是在 Grid(它是允许共享布局坐标的 Panel 元素)内承载的。   现在很明显该元素由白线环绕,分配给 TextBlock 元素的分区实际远远大于其填充的空间。由于还有其他元素添加到 Grid,此分配可能会收缩或扩展,这取决于所添加元素的类型和大小。 将会使用 GetLayoutSlot 方法(它是一项用于显示元素边界框的有用技术)返回 TextBlock 的布局槽并将其转换成 Path。   第 页 共 291 页 微软WPF应用   测量和排列子控件 当呈现 Window 对象的内容时,会自动调用布局系统。为了显示内容,窗口的 Content 必须定义根 Panel(用于定义框架,Children 是按框架在屏幕上组织的)。 布局的第一个处理过程是测量处理过程,将在此对 Children 集合的每个成员进行计算。此过程将以调用 Measure 方法开始。此方法将在父 Panel 元素的实现中调用,无需为要出现的布局显式调用该方法。 首先,将计算 UIElement 的本机大小属性,如 Clip 和 Visibility。这将生成一个名为 constraintSize 的传递给 MeasureCore 的值。 其次,会处理在 FrameworkElement 上定义的框架属性,这将影响 constraintSize 的值。这些属性旨在描述基础 UIElement 的大小调整特性,例如其 Height、Width、Margin 和 Style。上述每个属性均可能改变显示元素所必需的空间。然后,将用 constraintSize 作为一个参数调用 MeasureOverride。 说明: 在 Height、Width、ActualHeight 和 ActualWidth 的属性之间存在着差异。例如,ActualHeight 属性是基于其他高度输入和布局系统的计算值。该值是由布局系统本身基于实际呈现处理过程设置的,因此可能稍微小于属性(例如作为输入更改基础的 Height)的设置值。 由于 ActualHeight 是一个计算值,所以您应该知道,作为布局系统多个操作的结果,该值可能有多次或不断增加的报告的更改。布局系统可能正在计算子元素所需的测量空间、父元素的约束等。 测量处理过程的最终目标是让子级确定其 DesiredSize,这是在 MeasureCore 调用期间发生的。该值由 Measure 存储,以便在内容排列过程期间使用。 此排列过程将以调用 Arrange 方法开始。在排列处理过程期间,父 Panel 元素生成一个代表子级边界的矩形。该值会传递给 ArrangeCore 方法以便进行处理。 第 页 共 291 页 微软WPF应用 ArrangeCore 方法计算子级的 DesiredSize,计算可能影响该元素呈现大小的任何其他边距,并生成 arrangeSize(作为参数传递给 Panel 的 ArrangeOverride)。ArrangeOverride 生成子级的 finalSize,最后,ArrangeCore 方法执行偏移属性(例如边距和对齐方式)的最终计算,并将子级放在其布局槽内。子级无需(且通常不会)填充整个分配空间。然后,控件返回到父 Panel,至此布局过程完成。  面板元素和自定义布局行为 Windows Presentation Foundation (WPF) 包括 Panel 元素的派生套件,可以实现许多复杂的布局。常见方案(如堆栈元素)可以使用 StackPanel 元素方便地实现,而较为复杂和自由流动的布局可以使用 Canvas 来实现。 下表概括了可用的布局元素。 面板名称 说明 Canvas 定义一个区域,在此区域内,您可以使用相对于 Canvas 区域的坐标显式定位子元素。 DockPanel 定义一个区域,在此区域中,您可以使子元素互相水平或垂直排列。 Grid 定义由行和列组成的灵活网格区域。 StackPanel 将子元素排列成一行(可沿水平或垂直方向)。 VirtualizingPanel 为“虚拟化”其子数据集合的 Panel 元素提供一个框架。这是一个抽象类。 WrapPanel 从左至右按顺序位置定位子元素,在包含框的边缘处将内容断开至下一行。后续排序按照从上至下或从右至左的顺序进行,具体取决于 Orientation 属性的值。 对于其所需应用程序布局不可能使用任意预定义的 Panel 元素来实现的方案,您可以通过从 Panel 继承、并重写 MeasureOverride 和 ArrangeOverride 方法来实现自定义布局行为。 第 页 共 291 页 微软WPF应用 布局性能注意事项 布局是一个递归过程。Children 集合中的每个子元素会在每次调用系统期间得到处理。因此,应避免在不必要时触发系统。以下提示有助于实现更高的性能。 其值可能导致布局系统被初始化的相关性属性会标记为公共标志。AffectsMeasure 和 AffectsArrange 提供有关哪个属性值更改会强制执行布局系统的递归更新的有用提示。通常,任何可能影响元素边界框大小的属性应将 AffectsMeasure 标志设置为 true。 LayoutTransform 可能是影响用户界面 (UI) 内容的非常有用的方式。不过,如果转换的效果无需对其他元素的位置施加影响,则最好改为使用 RenderTransform,因为 RenderTransform 不会调用布局系统。LayoutTransform 会应用其转换,并强制对帐户执行递归布局更新,以获取受影响元素的新位置。 避免不必要地调用 UpdateLayout。此方法强制递归布局更新,但常常却是不必要的。除非您确认需要进行完整更新,否则请依赖布局系统来为您调用此方法。 当处理大型 Children 集合时,请考虑使用 VirtualizingStackPanel 而非常规 StackPanel。通过“虚拟化”子元素,VirtualizingStackPanel 仅在内存中保留当前位于父 ViewPort 内的对象。因此,在大多数情况下性能会得到极大改进。 7. WPF基础之样式设置和模板化 Windows Presentation Foundation (WPF) 样式设置和模板化是指一套功能(样式、模板、触发器和演示图板),应用程序、文档或用户界面 (UI) 的设计人员使用这些功能可以创建更好的视觉效果,也可以对其产品的统一外观进行标准化。尽管作者或设计人员可以对应用程序的外观逐个进行大量自定义操作,他们还是需要一个功能强大的样式设置和模板化模型,以便在应用程序内部和应用程序之间维护和共享外观。Windows Presentation Foundation (WPF) 就提供了这样的模型。 WPF 样式设置模型的另一个功能是实现表示形式与逻辑的分离。这意味着,在开发人员使用 C# 或 Visual Basic 进行逻辑编程时,设计人员只需使用 XAML 即可设计程序的外观。 本概述将讨论样式设置和模板化示例简介应用程序,该应用程序具有两个 TextBlock 元素,以及一个绑定到图像列表的 ListBox 控件: 第 页 共 291 页 微软WPF应用 本概述主要讨论该应用程序的样式设置和模板化方面,而不讨论任何数据绑定概念。 另外,了解资源也很重要,正是资源使得样式和模板得以重用。 样式基础知识 您可以将 Style 看作是将一组属性值应用到多个元素的捷径。例如,考虑下面的 TextBlock 元素及其默认外观:   通过直接对每个 TextBlock 元素设置 FontSize 和 FontFamily 等属性可以更改默认外观。但是,如果希望 TextBlock 元素可以共享某些属性,则可以在 XAML 文件的 Resources 节中创建一个 Style,如下所示:   第 页 共 291 页 微软WPF应用 将样式的 TargetType 设置为 TextBlock 类型时,该样式会应用于窗口中的所有 TextBlock 元素。 现在,TextBlock 元素的外观如下: 扩展样式 您可能希望两个 TextBlock 元素共享某些属性值(如 FontFamily 和居中的 HorizontalAlignment),同时还希望文本“我的图片”具有某些其他属性。在第一个样式的基础上创建一个新样式可以达到这一目的,如下所示:   请注意,已为上面的样式提供了 x:Key。若要应用该样式,请将 TextBlock 的 Style 属性设置为 x:Key 值,如下所示:  现在,此 TextBlock 样式的 HorizontalAlignment 值为 Center,FontFamily 值为 Comic Sans MS,FontSize 值为 26,Foreground 值设置为示例所示的 LinearGradientBrush。请注意,我们已重写了基础样式的 FontSize 值。如果有多个 Setter 对 Style 的同一属性进行设置,则最后声明的 Setter 优先。 第 页 共 291 页 微软WPF应用 下面演示 TextBlock 元素现在的外观: 此 TitleText 样式扩展了为 TextBlock 类型创建的样式。此外,使用 x:Key 值也可以扩展具有 x:Key 的样式。 TargetType 属性 (Property) 和 x:Key 属性 (Attribute) 的关系 如第一个示例所示,如果将 TargetType 属性设置为 TextBlock 而不为样式分配 x:Key,样式就会应用于所有 TextBlock 元素。这种情况下,x:Key 隐式设置为 {x:Type TextBlock}。这意味着,如果将 x:Key 值显式设置为 {x:Type TextBlock} 之外的任何值,Style 就不会自动应用于所有 TextBlock 元素。此时,必须通过使用 x:Key 值,将样式显式应用于 TextBlock 元素。如果样式位于资源部分,并且未设置样式的 TargetType 属性,则必须提供 x:Key。 除了提供 x:Key 的默认值之外,TargetType 属性还指定要应用 setter 属性的类型。如果未指定 TargetType,则必须通过语法 Property="ClassName.Property",用类名限定 Setter 对象的属性。例如,必须将 Property 设置为 "TextBlock.FontSize" 或 "Control.FontSize",而不要设置 Property="FontSize"。 此外,还应注意,很多 WPF 控件都是由其他 WPF 控件组合而成的。如果创建要应用于某个类型的所有控件的样式,可能会得到意想不到的结果。例如,如果针对 Window 中的 TextBlock 类型创建一个样式,则该样式会应用于窗口中的所有 TextBlock 控件,即使 TextBlock 是另一个控件(如 ListBox)的组成部分也不例外。 样式和资源 任何派生自 FrameworkElement 或 FrameworkContentElement 的元素都可以使用样式。声明样式的最常见方式是将样式作为 XAML 文件的 Resources 节中的资源,如前面的示例所示。由于样式是资源,因此同样遵循所有资源都适用的范围规则:样式的声明位置决定样式的应用范围。例如,如果在应用程序定义 XAML 文件的根元素中声明样式,则样式可在应用程序范围内使用。如果创建的是导航应用程序,并在该应用程序的某个 XAML 文件中声明样式,则该样式只能在该 XAML 文件中使用。 第 页 共 291 页 微软WPF应用 以编程方式设置样式 若要以编程方式向元素分配命名样式,请从资源集合中获取该样式,然后将其分配给元素的 Style 属性。请注意,资源集合中的项是 Object 类型,因此,将检索到的样式分配给 Style 属性之前,必须将该样式强制转换为 Style。例如,若要对名为 textblock1 的 TextBlock 设置定义的 TitleText 样式,请执行以下操作:   请注意,样式一旦应用,便会密封并且无法更改。如果要动态更改已应用的样式,必须创建一个新样式来替换现有样式。 您可以创建一个根据自定义逻辑选择要应用的样式的对象。 绑定、动态资源和事件处理程序 请注意,使用 Setter.Value 属性可以指定 绑定标记扩展或 DynamicResource 标记扩展。 到目前为止,我们只讨论了如何使用 setter 设置属性值。在样式中也可以指定事件处理程序。 数据模板 在本示例应用程序中,有一个绑定到照片列表的 ListBox 控件。   此 ListBox 当前的外观如下所示: 大多数控件都具有某种类型的内容,这些内容通常来自绑定到的数据。在本示例中,数据为照片列表。在 WPF 中,使用 DataTemplate 可以定义数据的可视表示形式。基本上,输入 DataTemplate 的内容决定了数据在呈现的应用程序中的外观。 第 页 共 291 页 微软WPF应用 在我们的示例应用程序中,每个自定义 Photo 对象都具有一个字符串类型的 Source 属性,该属性指定图像的文件路径。当前,照片对象显示为文件路径。 对于要显示为图像的照片,可以将 DataTemplate 作为资源创建:   请注意,DataType 属性与 Style 的 TargetType 属性非常相似。如果 DataTemplate 位于资源部分,并且将 DataType 属性指定为某个类型,也不为其分配 x:Key,则只要该类型出现,便会应用 DataTemplate。任何时候都可以为 DataTemplate 分配 x:Key,然后将其设置为 DataTemplate 类型的属性(如 ItemTemplate 属性或 ContentTemplate 属性)的 StaticResource。 实质上,上面示例的 DataTemplate 确定只要存在 Photo 对象,该对象就应作为 Image 显示在 Border 中。通过此 DataTemplate,应用程序现在的外观如下: 第 页 共 291 页 微软WPF应用 数据模板化模型还提供其他功能。例如,如果要使用 HeaderedItemsControl 类型(如 Menu 或 TreeView)显示包含其他集合的集合数据,则可以使用 HierarchicalDataTemplate。另一个数据模板化功能是 DataTemplateSelector,利用这一功能可以根据自定义逻辑选择要使用的 DataTemplate。有关更多信息,请参见数据模板概述,该概述对不同的数据模板化功能进行了更加深入的讨论。 控件模板 请注意,我们的照片显示为图片,我们要水平显示这些照片,而不是垂直显示;我们希望 ListBox 是水平的。 不使用 ControlTemplate 首先,要使 ListBox 水平,不一定要使用 ControlTemplate,明确这一点很重要。ListBox 具有 ItemsPanel 属性,利用该属性可以设置 ItemsPanelTemplate,即控制 ListBox 的项的布局的模板。一种方法是只创建 ListBox 样式,然后设置 ItemsPanel 属性,如下例所示:   例表明,除了替换 ControlTemplate 之外,可能还有其他方法,这取决于具体的情况。在本示例中,如果希望获得具有其他属性(如圆角)的水平 ListBox,则需要使用 ListBox 的 ControlTemplate。 在提供示例来演示具体操作之前,首先需要说明 ControlTemplate 的概念。 什么是 ControlTemplate? 大多数控件都具有外观和行为。以按钮为例:外观是可以按下的凸起区域,行为是在响应单击时所引发的 Click 事件。 有时,控件可以提供所需行为,但不具有所需外观。到目前为止,我们已经演示了可以使用样式 setter 来设置属性值,从而影响控件的外观。但是,若要更改控件的结构,或对组成控件的组件设置属性值,就需要使用 ControlTemplate。 在 WPF 中,控件的 ControlTemplate 定义控件的外观。通过为控件定义新的 ControlTemplate 可以更改控件的结构和外观。很多情况下,这种方法都足够灵活,您不需要自己编写自定义控件。如果没有为控件定义自己的 ControlTemplate,则可以获取与系统主题匹配的默认模板,该模板向 Button 控件提供默认外观。 第 页 共 291 页 微软WPF应用 请注意:一旦为控件创建 ControlTemplate,就会替换整个 ControlTemplate。例如,可以通过以下方式定义 Button ControlTemplate。 请注意,ContentPresenter 元素只标记 Button 的 Content 应出现在何处。后面有一节专门展开详细讨论。   应用此模板之后,Button 显示为 Ellipse: 请记住,当 Button 具有焦点或按下时,其外观都是将替换的按钮的默认外观的组成部分。因此,您可能希望定义按钮按下时的外观,这取决于您的具体需要。 如果要创建 ControlTemplate,使用 ControlTemplate 示例 是最好的入门方法。如果确实需要查看控件的组成部分,可以查看位于主题的主题文件,也可以使用 XAMLPad(随 Windows 软件开发工具包 (SDK) 安装的应用程序)的 Show Visual Tree 功能。 创建 ControlTemplate 现在,继续演示示例,我们创建一个 ControlTemplate,它定义一个水平的圆角 ListBox。若要替换控件的 ControlTemplate,请将 Template 属性设置为新的 ControlTemplate。 第 页 共 291 页 微软WPF应用   以这种方式设置 Template 属性,实际上与使用 Style 设置其他控件属性没有区别:您将 Style 用作一个工具来帮助设置 Template 属性。也就是说,设置 ControlTemplate 的另一种方法是直接设置控件的 Template 属性。如果以这种方式设置,则会在 Resources 节中创建一个 ControlTemplate,并为它提供 x:Key,然后将它作为静态资源使用。 如上例所示,ControlTemplate 类具有 TargetType 属性,该属性类似于 Style 类的 TargetType 属性。但要注意,与 Style 和 DataTemplate 不同,ControlTemplate 对象没有隐式键的概念。换言之,如果有一个独立 ControlTemplate,其 TargetType 属性设置为某个类型,则 ControlTemplate 不会自动应用于该类型。另请注意,如果模板定义包含 ContentPresenter,则 ControlTemplate 需要 TargetType 属性。 尝试使用 ControlTemplate。例如,用 WrapPanel 替换 StackPanel,将 ScrollViewer 的 HorizontalScrollBarVisibility 属性设置为 Disabled,然后将 ListBox 的宽度设置为 300。(只有第一行空间不足时,WrapPanel 才会将项放置到下一行。如果没有将 ScrollViewer 的 HorizontalScrollBarVisibility 属性设置为 Disabled,由于可以滚动到末尾,则第一行不会空间不足。因此,WrapPanel 不会对项进行换行。) IsItemsHost 属性 在此示例中,一个必需的重要属性是 IsItemsHost 属性。IsItemsHost 属性用于指示在 ItemsControl(如处理项列表的 ListBox 控件)的模板中,生成的元素应放在什么位置。如果将 StackPanel 的这一属性设置为 true,则添加到 ListBox 的所有项都将进入 StackPanel。请注意,此属性只对 Panel 类型有效。 ItemsPresenter 和 ContentPresenter 请注意,如果以这种方式在 ControlTemplate 中指定一个面板并将其标记为 IsItemsHost,控件的用户不使用 ControlTemplate 就无法替换 ItemsPanel。因此,除非您确信必须使用模板才能替换面板,否则不要采用这种方式。此外,您也可以使用 ItemsPresenter 元素来标记项的位置,然后通过设置 ItemsPanel 属性来指定 ItemsPanelTemplate。ItemsPanelTemplate 页提供了一个示例,为您演示如何操作。 第 页 共 291 页 微软WPF应用 如果要创建 ContentControl(如 Button)的模板,则对应元素为 ContentPresenter。同样,将此元素放置到 ContentControl 类型的 ControlTemplate 中,可以指示内容应在什么位置显示,如什么是 ControlTemplate? 一节中的示例所示。有关其他示例,请参见 Label ControlTemplate 示例和 ListBoxItem ControlTemplate 示例。 TemplateBinding 在上一示例中,需要注意的另一个重点是设置为 {TemplateBinding ListBox.Background} 的 Background 值。它只是指示 Border 的 Background 应与 ListBox 上设置的 Background 值同步。TemplatBinding 与 Binding 类似。实际上,TemplatBinding 比 Binding 更有效,但功能更弱;使用 TemplatBinding 等效于使用 Source 属性设置为 RelativeSource.TemplatedParent 的 Binding。 若要使控件用户能够控制某些属性的值,可以在 ControlTemplate 中使用 TemplateBinding。TemplateBinding 是一个由 TemplateBindingExtension 类表示的标记扩展。 您可能已经注意到,DataTemplate 和 ControlTemplate 的相似之处在于它们的内容变成了对象的外观。通过 ListBox ControlTemplate 定义,应用程序现在的外观如下: 第 页 共 291 页 微软WPF应用 触发器 Style、ControlTemplate 和 DataTemplate 都具有 Triggers 属性,该熟悉可以包含一组触发器。某个属性值更改时,或某个事件引发时,触发器会相应地设置属性或启动操作(如动画操作)。 属性触发器 为了演示如何使用触发器来设置属性,我们将每个 ListBoxItem 都设置为部分透明(除非它被选中)。 下面的样式将 ListBoxItem 的 Opacity 值设置为 0.5。但是,当 IsSelected 属性为 true 时,Opacity 设置为 1.0:   此示例使用 Trigger 来设置属性值,但请注意,Trigger 类还具有 EnterActions 和 ExitActions 属性,通过这两个属性,触发器可以执行操作。 请注意,我们还将 ListBoxItem 的 MaxHeight 属性设置为 75。在下面的屏幕快照中,选中的项是第三项: EventTrigger 和 Storyboard 我们刚刚演示了 Trigger 根据某个属性的值来设置属性值或启动操作。另一种类型的触发器是 EventTrigger,它根据事件的引发来启动一组操作。例如,下面的 EventTrigger 对象指定当鼠标指针进入 ListBoxItem 时,MaxHeight 属性在 0.2 秒时间内以动画方式增大为值 90。当鼠标离开该项时,该属性在 1 秒时间内还原为原始值。请注意为何无需为 MouseLeave 动画指定 To 值。这是因为动画能够跟踪原始值。 第 页 共 291 页 微软WPF应用   在下面的屏幕快照中,鼠标指向第三项: MultiTrigger、DataTrigger 和 MultiDataTrigger 除了 Trigger 和 EventTrigger 之外,还有其他类型的触发器。通过 MultiTrigger,可以根据多个条件来设置属性值。如果条件的属性是经过数据绑定的,则可以使用 DataTrigger 和 MultiDataTrigger。 共享的资源和主题 典型 Windows Presentation Foundation (WPF) 应用程序可能具有多个在整个应用程序范围内应用的用户界面 (UI) 资源。概括地说,这组资源可视为应用程序的主题。通过使用封装为 ResourceDictionary 类的资源字典,Windows Presentation Foundation (WPF) 支持将用户界面 (UI) 资源打包为主题。 第 页 共 291 页 微软WPF应用 Windows Presentation Foundation (WPF) 主题是使用样式设置和模板化机制定义的,Windows Presentation Foundation (WPF) 公开该机制,用于自定义任何元素的可视对象。 Windows Presentation Foundation (WPF) 主题资源存储在嵌入式资源字典中。这些资源字典必须嵌入到已签名的程序集中,它们既可以嵌入到代码自身所在的程序集中,也可以嵌入到并行程序集中。对于包含 Windows Presentation Foundation (WPF) 控件的程序集 PresentationFramework.dll,主题资源在一系列并行程序集中。 搜索元素样式时,主题是最后查找的位置。通常,搜索首先沿元素树向上查找相应资源,然后在应用程序资源集合中查找,最后查询系统。这为应用程序的作者提供了机会,让他们可以在到达主题之前,在树或应用程序级重新定义任何对象的样式。 您可以将各资源字典分别定义为单个文件,这样就可以在多个应用程序中重用某个主题。通过定义多个提供相同类型资源、但具有不同值的资源字典,也可以创建可交换的主题。在设计应用程序外观时,建议在应用程序级重新定义这些样式或其他资源。 若要在多个应用程序中共享一组资源(包括样式和模板),可以创建一个 XAML 文件并定义一个 ResourceDictionary。例如,请看下面的屏幕快照,它显示了使用 ControlTemplates 设置样式的示例的一部分: 第 页 共 291 页 微软WPF应用 如果查看示例中的 XAML 文件,您会注意到所有文件都包含以下内容:   这是共享的 shared.xaml,该文件定义一个 ResourceDictionary,该资源字典包含一组样式和画笔资源,使示例中的控件具有了一致的外观。 8. 详谈WPF开发中的数据虚拟化 UI虚拟化 当一个WPF的ItemControl被绑定到一个大型集合的数据源时,如果可以UI虚拟化,该控件将只为那些在可以看到的项创见可视化的容器(加上面和下面的少许)。这是一个完整集合中有代表性的一小部分。用户移动滚动条时,将为那些滚动到可视区域的项创建新的可视化容器,那些不再可见的项的容器将被销毁。当容器设置为循环使用时,它将再使用可视化容器代替不断的创建和销毁可视化容器,避免对象的实例化和垃圾回收器的过度工作。 数据虚拟化 数据虚拟化是指绑定到ItemControl的真实的数据对象的归档虚拟化的时间段。数据虚拟化不是由WPF提供的。作为对比,基本数据对象的小集合对内存的消耗不是很多;但是,大集合的内存消耗是非常严重的。另外,真实的检索数据(例如,从数据库)和实例化数据对象是很耗时的,尤其当是网络数据调用时。因此,我们希望使用数据虚拟化机制来限制检索的数据的数量和在内存中生成数据对象的数量。 解决方案 总览 这个解决方案是只在ItemControl绑定到IList接口的实现时起作用,而不是IEumerable的实现,它并不枚举整个列表,而只是读取需要显示的项。它使用Count属性判断集合的大小,推测并设置滚动的范围。然后使用列表索引重新确定要在屏幕上显示的项。因此,创建一个可以报告具有大量项的,并且可以只检索需要的项的IList。 IItemsProvider 为了利用这个解决方案,下面的数据源必须能提供集合中项的数量,并且能够提供完整集合的小块(或页)。这需要在IItemsProvider接口封装。 /// /// Represents a provider of collection details. /// /// The type of items in the collection. public interface IItemsProvider { /// /// Fetches the total number of items available. /// /// int FetchCount(); /// /// Fetches a range of items. /// /// The start index. /// The number of items to fetch. /// IList FetchRange(int startIndex, int count); } 第 页 共 291 页 微软WPF应用 如果下面的查询是一个数据库查询,它是一个利用大多数据库供应商都提供的COUNT()聚集函数和OFFSET与LIMIT表达式的一个IItemProvider接口的一个简单实现。 VirtualizingCollection 这是一个执行数据虚拟化的IList的实现。VirtualizingCollection(T)把整个集合分装到一定数量的页中。根据需要把页加载到内存中,在不需要时从释放。 下面讨论我们有兴趣的部分。详细信息请参考附件中的源代码项目。 IList实现的第一个方面是实现Count属性。它通常被ItemsControl用来确定集合的大小,并呈现适当的滚动条。 private int _count = -1; public virtual int Count { get { if (_count == -1) { LoadCount(); } return _count; } protected set { _count = value; } } protected virtual void LoadCount() { Count = FetchCount(); } protected int Fet 第 页 共 291 页 微软WPF应用 chCount() { return ItemsProvider.FetchCount(); } Count属性使用延迟和懒惰加载(lazy loading)模式。它使用特殊值-1作为未加载的标识。当第一次读取它时,它从ItemsProvider加载其实际的数量。 IList接口的实现的另一个重要方面是索引的实现。 private int _count = -1; public virtual int Count { get { if (_count == -1) { LoadCount(); } return _count; } protected set { _count = value; } } protected virtual void LoadCount() { Count = FetchCount(); } protected int FetchCount() { return ItemsProvider.FetchCount(); } 第 页 共 291 页 微软WPF应用 这个索引是这个解决方案的一个聪明的操作。首先,它必须确定请求的项在哪个页(pageIndex)中,在页中的位置(pageOffset),然后调用RequestPage()方法请求该页。 附加的步骤是然后根据pageOffset加载后一页或前一页。这基于一个假设,如果用户正在浏览第0页,那么他们有很高的机率接下来要滚动浏览第1页。提前把数据取来,就可以无延迟的显示。 然后调用CleanUpPages()清除(或卸载)所有不再使用的页。 最后,放置页不可用的一个防御性的检查, 当RequestPage没有同步操作时是必要的,例如在子类AsyncVirtualizingCollection中。 // ... private readonly Dictionary> _pages = new Dictionary>(); private readonly Dictionary _pageTouchTimes = new Dictionary(); protected virtual void RequestPage(int pageIndex) { if (!_pages.ContainsKey(pageIndex)) { _pages.Add(pageIndex, null); _pageTouchTimes.Add(pageIndex, DateTime.Now); LoadPage(pageIndex); } else { _pageTouchTimes[pageIndex] = DateTime.Now; } } protected virtual void PopulatePage(int pageIndex, IList page) { if (_pages.ContainsKey(pageIndex)) _pages[pageIndex] = page; } public void CleanUpPages() { List keys = new List(_pageTouchTimes.Keys); foreach (int key in keys) { // page 0 is a special case, since the WPF ItemsControl // accesses the first item frequently if ( key != 0 && (DateTime.Now - _pageTouchTimes[key]).TotalMilliseconds > PageTimeout ) { _pages.Remove(key); _pageTouchTimes.Remove(key); } } } 第 页 共 291 页 微软WPF应用 页存储在以页索引为键的字典(Dictionary)中。一个附加的字典(Dictionary)记录着每个页的最后存取时间,它用于在CleanUpPages()方法中移除较长时间没有存取的页。 protected virtual void LoadPage(int pageIndex) { PopulatePage(pageIndex, FetchPage(pageIndex)); } protected IList FetchPage(int pageIndex) { return 为完成该解决方案,FetchPage()执行从ItemProvider中抓取数据,LoadPage()方法完成调用PopulatePage方法获取页并把该页存储到字典(Dictionary)中的工作。 看起来好象有一些太多的不全逻辑的方法(a few too many inconsequential methods),但这样设计是有原因的:每一个方法做且只做一件事,有助于提高代码的可读性,并使在子类中进行功能扩展和维护变得容易,下面可以看到。 类VirtualizingCollection实现了数据虚拟化的基本目标。不幸的是,在使用中,它有一个严重不足:数据抓取方法是全部同步执行的。这就是说它们要在UI线程中执行,造成一个缓慢的程序 AsyncVirtualizingCollection类AsyncVirtualizingCollection继承自VirtualizingCollection,重载了Load方法,以实现数据的异步加载。 WPF中异步数据源的关键是在数据抓取完成后必须通知UI的数据绑定。在规则的对象中,是通过实现INotifyPropertyChanged接口实现的。对一个集合的实现,需要紧密的关系,INotifyCollectionChanged。那是ObservableCollection要使用的接口。 public event NotifyCollectionChangedEventHandler CollectionChanged; protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { NotifyCollectionChangedEventHandler h = CollectionChanged; i 第 页 共 291 页 微软WPF应用 f (h != null) h(this, e); } private void FireCollectionReset() { NotifyCollectionChangedEventArgs e = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset); OnCollectionChanged(e); } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChangedEventHandler h = PropertyChanged; if (h != null) h(this, e); } private void FirePropertyChanged(string propertyName) { PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName); OnPropertyChanged(e); } 实现了INotifyCollectionChanged接口和INotifyPropertyChanged接口。提供数据绑定弹性最大化。这个实现没有任何要注意的。 protected override void LoadCount() { Count = 0; IsLoading = true; ThreadPool.QueueUserWorkItem(LoadCountWork); } private void LoadCountWork(object args) { int count = FetchCount(); SynchronizationContext.Send(LoadCountCompleted, count); } private void LoadCountCompleted(object args) { Count = (int)args; IsLoading = false; FireCollectionReset(); } 第 页 共 291 页 微软WPF应用 在重载的LoadCount()方法中,抓取是由ThreadPool(线程池)异步调用的。一旦完成,就会重置Count,UI的更新是由INotifyCollectionChanged接口调用FireCollectionReset方法实现的。注意LoadCountCompleted方法会在UI线程通过SynchronizationContext再一次被调用。假定集合的实例在UI线程中被创建,SynchronationContext属性就会被设置。 protected override void LoadPage(int index){IsLoading = true; ThreadPool.QueueUserWorkItem(LoadPageWork, index);} private void LoadPageWork(object args){    int pageIndex = (int)args;    IList page = FetchPage(pageIndex); SynchronizationContext.Send(LoadPageCompleted, new object[]{pageIndex, page});} private void LoadPageCompleted(object args){int pageIndex=(int)((object[]) args)[0]; IList page = (IList)((object[])args)[1];    PopulatePage(pageIndex, page); IsLoading = false;    FireCollectionReset();} 页数据的加载遵循相同的惯例,再一次调用FireCollectionReset方法更新用户UI。 也要注意IsLoading属性是一个简单的标识,可以用来告知UI集合正在加载。当IsLoading改变后,由INotifyPropertyChanged机制调用FirePropertyChanged方法更新UI。 public bool IsLoading{ get{ return _isLoading; }    set {if ( value != _isLoading ){  _isLoading = value; FirePropertyChanged("IsLoading");}    }} 演示项目 为了演示这个解决方案,我创建了一个简单的示例项目(包括附加的源代码项目)。 第 页 共 291 页 微软WPF应用 首先,创建一个IItemsProvider的一个实现,它通过使用线程休眠来模拟网络或磁盘行为的延迟提供虚拟数据。 public class DemoCustomerProvider : IItemsProvider { private readonly int _count; private readonly int _fetchDelay; public DemoCustomerProvider(int count, int fetchDelay) { _count = count; _fetchDelay = fetchDelay; } public int FetchCount() { Thread.Sleep(_fetchDelay); return _count; } public IList FetchRange(int startIndex, int count) { Thread.Sleep(_fetchDelay); List list = new List(); for( int i=startIndex; i { Customer customer = new Customer {Id = i+1, Name = "Customer " + (i+1)}; list.Add(customer); } return list; } } 普遍存在的Customer(消费者)对象作为集合中的项。 为了允许用户试验不同的列表实现,创建一个包含ListView的简单WPF窗体。 第 页 共 291 页 微软WPF应用 xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Data Virtualization Demo - By Paul McClean" Height="600" Width="600"> TextAlignment="Right" VerticalAlignment="Center"/> Text="1000000" Width="60" VerticalAlignment="Center"/> TextAlignment="Right" VerticalAlignment="Center"/> Text="1000" Width="60" VerticalAlignment="Center"/> TextAlignment="Right" VerticalAlignment="Center"/> Margin="5" Content="List(T)" VerticalAlignment="Center"/> Margin="5" Content="VirtualizingList(T)" VerticalAlignment="Center"/> Margin="5" Content="AsyncVirtualizingList(T)" IsChecked="True" VerticalAlignment="Center"/> TextAlignment="Right" VerticalAlignment="Center"/> Text="100" Width="60" VerticalAlignment="Center"/> TextAlignment="Right" VerticalAlignment="Center"/> Text="30" Width="60" VerticalAlignment="Center"/> 第 页 共 291 页 微软WPF应用 VerticalAlignment="Center"/> Width="80" VerticalAlignment="Center"/> XAML语法 1. XAML语法术语 本主题定义用来描述可扩展应用程序标记语言 (XAML) 语法的各个元素的术语。这些术语将在本软件开发工具包 (SDK) 的其余部分中经常用到。本主题扩展了 XAML 概述中所介绍的基本术语。 XAML 语法术语的起源 此处定义的 XAML 语法术语在 XAML 语言规范中也有定义或引用。XAML 是一种基于 XML 且遵循 XML 结构规则的语言。其中的术语共享自或基于描述 XML 语言或 XML 文档对象模型 (DOM) 时的常用术语。  对象元素语法 对象元素语法是一种 XAML 标记语法,它通过声明 XML 元素来实例化公共语言运行库 (CLR) 类或结构。此语法与其他标记语言(例如 HTML)的元素语法相似。对象元素语法以左尖括号 (<) 开头,其后紧跟正进行实例化的类或结构的类型名称。类型名称后面可以有零个或多个空格,对于对象元素还可以声明零个或多个属性,并用一个或多个空格来分隔每个“属性名="值"”对。最后,必须存在下列一种情况: 第 页 共 291 页 微软WPF应用 元素和标记必须用正斜杠 (/) 和紧跟的右尖括号 (>) 结尾。 开始标记必须以右尖括号 (>) 结尾。其他对象元素、属性元素或内部文本可以跟在开始标记后面。对象元素还必须存在等效的结束标记,并与其他开始标记/结束标记对形成正确的嵌套和平衡。 例如,下面的示例是一个对象元素语法,该语法实例化 Button 类的一个新实例,而且还指定了一个 Name 属性及其值:   下面的示例是一个还包括可扩展应用程序标记语言 (XAML) 内容属性语法的对象元素语法。其中包含的内部文本将用来设置 TextBox 可扩展应用程序标记语言 (XAML) 内容属性 Text。   属性语法 属性语法是一种 XAML 标记语法,该语法通过针对元素声明属性 (Attribute) 来设置属性 (Property) 值或者命名事件的事件处理程序。元素总是通过对象元素语法来声明。属性名必须与属性或事件的 CLR 成员名称相匹配。属性名后面是赋值运算符 (=)。属性值必须是一个用双引号 (") 引起来的字符串。 为了能够通过属性语法进行设置,属性必须是公共的、可读写的,而且必须具有一个可以由 XAML 处理器实例化或引用的属性值类型。对于事件来说,事件必须是公共的而且必须具有一个公共委托。属性或事件必须是由包含对象元素实例化的类或结构的成员。 属性值由下面的操作之一,按照如下处理顺序进行填充: 如果 XAML 处理器遇到一个大括号,或者遇到一个从 MarkupExtension 派生的对象元素,则将首先计算所引用的标记扩展(而不是将该扩展作为字符串来处理),而且将使用由标记扩展返回的对象。在许多情况下,由标记扩展返回的对象将是对现有对象的引用,或者是一个将计算推迟到运行时的表达式,而不是一个新对象。 如果该属性 (Property) 是用指定的 TypeConverter 声明的,或者该属性 (Property) 的值类型是用属性 (Attribute) 化 TypeConverter 声明的,则该属性 (Attribute) 的字符串值将作为转换输入提交到类型转换器,该转换器将返回一个新的对象实例。 第 页 共 291 页 微软WPF应用 如果没有 TypeConverter,则将尝试直接转换为属性类型。最后一个级别是直接在基元类型之间转换,或者在枚举中检查名称(这将返回匹配的值)。 例如,在使用上面所显示的标记时,可以使用下面的属性 (Attribute) 语法示例为 Name 属性 (Property) 赋予字符串值:   Name 属性是 Button 类的成员表的成员。Button 是用来定义 Name 的 FrameworkElement 类的派生类。 属性值的处理 包含在左引号和右引号之间的字符串值是由 XAML 处理器处理的。对于属性来说,默认处理行为是由基础 CLR 属性的类型确定的。如果该属性 (Property) 是基元类型,则会基于字符串到相关基元类型的隐式转换来赋予属性 (Attribute) 值。如果该属性是一个枚举,则字符串会被视为由该枚举定义的名称,而且将从枚举中返回匹配的值。如果该属性 (Property) 既不是基元类型又不是枚举,则属性 (Attribute) 值必须由针对该属性 (Property) 本身或者目标类型声明的类型转换器来处理。类型转换器必须提供一个能够接受字符串的转换机制,该转换机制必须生成基础 CLR 属性类型的实例。还可以通过标记扩展来推迟转换步骤。 枚举属性值 XAML 中的枚举值由 Enum 结构的本机方法在内部处理。 对于无标志的枚举值,本机行为是处理属性值的字符串并将它解析为某个枚举值。您不必像在代码中那样指定格式为枚举.值 的枚举,而是仅指定值,枚举 将从所设置属性的类型推断。如果您指定格式为枚举.值 的属性,它将无法正确解析。 对于按标志枚举,该行为基于 Enum..::.Parse 方法。您可以通过用逗号分隔每个值来为按标志枚举指定多个值。但是,您不能合并不按标志的枚举值。例如,不能试图使用逗号语法来创建作用于无标志枚举多个条件的 Trigger: 在 WPF 中,能够支持 XAML 中可设置属性的按标志枚举极为罕见。但是,StyleSimulations 就是这样的一个枚举。例如,可以使用逗号分隔的按标志属性语法来修改在 Glyphs 类的“Remarks”(备注)部分中提供的示例;StyleSimulations = "BoldSimulation" 可能会变成 StyleSimulations = "BoldSimulation,ItalicSimulation"。KeyBinding..::.Modifiers 是另一个属性,在该属性中可以指定多个枚举值。但是,此属性是一个特例,因为 ModifierKeys 枚举支持其自身的类型转换器。修饰符的类型转换器使用加号 (+) 而不是逗号 (,) 作为分隔符,因此在标记中支持用更传统的语法来表示组合键(如“Ctrl+Alt”)。 第 页 共 291 页 微软WPF应用 属性引用和事件成员名称引用 在指定属性 (Attribute) 时,可以引用您已经为包含对象元素实例化的 CLR 类型的成员表中存在的任何属性 (Property) 或事件。 或者,可以独立于包含对象元素来引用附加属性或附加事件。 对于可通过默认命名空间访问的任何对象中的任何事件,还可以通过使用类型名.事件 部分限定名来命名。此语法支持为路由事件附加处理程序,在路由事件中,处理程序旨在处理子元素中的事件路由,但是父元素在其成员表中并不拥有该事件。此语法与附加事件语法相似,但此处的事件不是真正的附加事件。相反,您引用的是具有限定名称的事件。 属性 (Property) 名有时作为属性 (Attribute) 值提供,而不是作为属性 (Attribute) 名提供,属性 (Property) 名还可以包括限定符,例如以所有者类型.依赖项属性名称 格式指定的属性。在 XAML 中编写样式或模板时,此情况较为常见。以属性 (Attribute) 值形式提供的属性 (Property) 名具有不同的处理规则,这些规则由所设置的属性 (Property) 类型以及某些上下文因素(如样式或模板是否具有目标类型)来控制。 当属性 (Attribute) 值描述属性 (Property) 之间的关系时,也可以使用属性 (Property) 名。此功能可用于数据绑定和演示图板目标,而且由 PropertyPath 类及其类型转换器启用。 隐式集合元素会在逻辑树中创建一个成员,即使它在标记中不显示为元素也是如此。通常,所拥有类型的构造函数针对作为其属性之一的集合执行实例化,这会将该集合添加到树中。 说明: 由 WPF XAML 处理器执行的集合检测功能不支持泛型列表和字典接口(IList<(Of <(T>)>) 和 IDictionary<(Of <(TKey, TValue>)>))。但是,可以将 List<(Of <(T>)>) 类用作基类(因为它直接实现 IList),或者将 Dictionary<(Of <(TKey, TValue>)>) 用作基类(因为它直接实现 IDictionary)。  XAML 内容语法 XAML 内容语法仅在将 ContentPropertyAttribute 指定为其类声明一部分的类上启用。ContentPropertyAttribute 需要一个按名称指定属性的参数,而该属性名称被定义为这种类型元素(包括派生的类)的内容属性。为此指定的属性是元素的 XAML 内容属性。在由 XAML 处理器处理时,在元素的开始标记和结束标记之间找到的任何子元素或内部文本将被指定为该 XAML 内容属性的值。元素的属性元素标记不按照这种方式赋值;它们是先进行处理,而且不被视为“内容”。 第 页 共 291 页 微软WPF应用 正如对于任何其他属性一样,对象的 XAML 内容属性将属于特定类型。该类型可以是 Object 类型。该内容属性的类型可帮助定义对象的内容模型。例如,鉴于任何对象都可以变为内容,Object 的类型是松散的,但是即使这种松散类型也要求内容必须是单个对象。该单个对象可以是集合对象,但是即便如此,也只能将一个这样的集合对象指定为内容。 特定类型的内容模型在该类型的类页面上进行描述,或者编写成类型系列的单独概念性主题中并与每个相关的类型引用建立链接。 集合类型的内容语法 为了接受多个对象元素(或内部文本)作为内容,内容属性的类型必须是明确的集合类型。与集合类型的属性元素语法相似,XAML 处理器必须标识作为集合类型的类型。如果某个元素具有 XAML 内容属性,则该 XAML 内容属性的类型是集合。不必在标记中将隐含集合类型指定为对象元素,也不必将 XAML 内容属性指定为属性元素。因此,标记中明显的内容模型现在可以将多个子元素作为指定为内容。下面是 Panel 子类的内容语法。所有的 Panel 派生类都建立要成为 Children 的 XAML 内容属性,这需要一个类型为 UIElementCollection 的值。   请注意,标记中既不需要 Children 的属性元素也不需要 UIElementCollection 的元素。这是 XAML 的设计特征,其目的在于,使用直接的父-子元素关系将那些用来定义 UI 的递归包含的元素更直观地表示为嵌套元素树,而不必对属性元素标记或集合对象进行外部干预。实际上,按照设计,UIElementCollection 在标记中不能指定为对象元素。由于 UIElementCollection 唯一的用途就是作为隐式集合,因此它不公开公共的默认构造函数,因此不能实例化为对象元素。 在具有内容属性的对象中混合使用属性元素和对象元素 XAML 规范声明 XAML 处理器可以进行如下强制:用来填充某个对象元素中 XAML 内容属性的对象元素必须是连续的,而且不得混合使用。对于混合使用属性元素和内容的这一限制是由 WPF XAML 处理器强制的。 第 页 共 291 页 微软WPF应用 可以将子对象元素作为某个对象元素中的第一个直接标记,然后可以引入属性元素。也可以指定一个或多个属性元素,接着指定内容,然后指定多个属性元素。但是,一旦内容后面跟有属性元素,您就不能进一步引入任何内容,而只能引入其他属性元素。 这个内容/属性元素顺序要求不适用于用作内容的内部文本。然而,这仍然是使内部文本保持连续的不错的标记样式,原因是,如果属性元素与内部文本交错分布,则很难直观地检测标记中的大量空白。  附加属性 附加属性是 XAML 中引入的一个编程概念,借此,属性可以由类型拥有和定义,但可以在任何元素上设置。附加属性所面向的主要方案就是,允许元素树中的子元素向父元素报告信息,而不要求使用在所有的元素之间广泛共享的对象模型。相反,附加属性可以由任何父元素用来向子元素报告信息。 附加属性使用的语法在表面上与属性元素语法非常相似,因为您还需要指定类型名.属性名 组合。二者有两个重要的差异: 即使在通过属性语法设置附加属性时,也可以使用类型名.属性名 组合。只有附加属性 (Property) 才要求属性 (Attribute) 语法中使用限定属性 (Property) 名。 对于附加属性还可以使用属性元素语法。但是,对于典型的属性元素语法,您指定的类型名 是包含属性元素的对象元素。如果您引用的是附加属性,则类型名 是用来定义附加属性的类,而不是包含对象元素。  附加事件 附加事件是 XAML 中引入的另一个编程概念,事件可以由类型定义,但是处理程序可以附加到任何对象上。用来定义附加事件的类型通常是用来定义服务的静态类型,这些附加事件有时由用来公开服务的类型中的路由事件别名公开。附加事件的处理程序是通过属性语法指定的。正如对于附加事件一样,可以扩展附加事件的属性语法,以便允许使用类型名.事件名,其中类型名 是为附加事件基础结构提供 Add 和 Remove 事件处理程序访问器的类,事件名 是事件名称。  XML 命名空间 上面的所有语法示例均未指定默认命名空间以外的命名空间。在典型的 WPF 应用程序中,默认命名空间被指定为 WPF 命名空间。您可以指定默认命名空间以外的命名空间,而且仍使用实质上同类的语法,但是,只要命名了无法在默认命名空间中访问的类,该类的名称就必须以用来映射对应 CLR 命名空间的 XML 命名空间的前缀作为开头。例如, 是一种用来实例化 MyElement 类的实例的对象元素语法,其中包含该类的 CLR 命名空间(可能还有包含该命名空间的外部程序集)以前映射到 custom 前缀。 第 页 共 291 页 微软WPF应用 标记扩展 XAML 定义了一个标记扩展编程实体,该实体允许从 XAML 处理器对属性或对象元素的常规处理中进行转义,将该处理转给支持类。WPF 对 XAML 处理器的实现将 MarkupExtension 抽象类用作由 WPF 支持的所有标记扩展的基础。在使用属性语法时,用来标识 XAML 处理器的标记扩展的字符是左大括号 ({),其后是右大括号 (}) 以外的任何字符。左大括号后面的第一个字符串必须引用用来提供特定扩展行为的类,如果子字符串“Extension”是实际类名的一部分,则该引用可以省略这个子字符串。该类后面可能会出现一个空格,该空格后面的每个字符都可以由所实现的扩展用作输入,直到遇到右大括号。在使用属性语法时,WPF 中标记扩展的主要用途是提供一种方法来引用其他已经存在的对象,或者将引用转给将在运行时计算的对象。例如,可以指定用 {Binding} 标记扩展来代替给定的属性通常将使用的值类型,从而完成简单的数据绑定。对于无法以其他方式使用属性 (Attribute) 语法的属性 (Property),许多标记扩展都允许使用属性 (Attribute) 语法。例如,Style 对象是一个相对复杂的引用类型,其中包含几个其他属性,每个属性都还采用 byref 对象(而非基元)。但是样式通常作为资源来创建,之后将通过请求资源的两个标记扩展之一来引用。该扩展将对属性 (Property) 值的计算推迟到资源查找时,允许在属性 (Attribute) 语法中提供 Style 属性 (Property) 的值并采用 Style 类型,如下所示: My button  在这里,StaticResource 用来标识 StaticResourceExtension 类,该类提供标记扩展实现。下一个字符串 MyStyle 用作非默认 StaticResourceExtension 构造函数的输入,在该构造函数中,从扩展字符串提取的参数用来声明所请求的 ResourceKey。MyStyle 应当是定义为资源的 Style 的 x:Key 属性 值。StaticResource 标记扩展用法要求使用该资源,在加载时通过静态资源查找逻辑来提供 Style 属性值。 可选的和不建议的 XAML 用法 属性元素的可选用法 属性元素的可选用法包括,具体地“拼出”由 XAML 处理器视为隐式的元素内容属性。例如,当您声明 Menu 的内容时,可以选择将 Menu 的 Items 集合显式声明为 属性元素标记,并将每个 MenuItem 放在 中,而不是使用隐式的 XAML 处理器行为(即,Menu 的所有子元素都必须是 MenuItem 而且放在 Items 集合中)。有时,这个可选用法可以帮助以可视方式阐明标记中所表示的对象结构。或者,属性元素的隐式用法有时可以避免使用在技术上具有功能但是在视觉上容易引起混淆(如在属性值中嵌套标记扩展)的标记。 第 页 共 291 页 微软WPF应用 typeName.memberName 全限定属性 使用属性的类型名.成员名 格式实际上比仅仅使用路由事件的情况更为普遍,但是,在其他应用程序中,如果只是为了实现标记样式和可读性,则该格式是多余的,您应当避免使用它。在下面的示例中,对 Background 属性的三个引用是完全等效的: Button.Background 之所以适用,是因为在 Button 上对于该属性的查找是成功的(Background 是从 Control 继承的),而且 Button 是对象元素的类或者是基类。Control.Background 之所以适用,是因为 Control 类实际上定义 Background,而且 Control 是一个 Button 基类。 但是,下面的类型名.成员名 格式示例并不适用,因此显示为已注释掉:   Label 是 Control 的另一个派生类,而且,如果在 Label 对象元素中指定了 Label.Background,则该用法将适用。但是,由于 Label 不是 Button 的类或基类,因此指定的 XAML 处理器行为是随后以附加属性形式处理 Label.Background。Label.Background 不是附加属性,因此该用法将失败。 baseTypeName.memberName 属性元素 与类型名.成员名 格式如何适用于属性语法相似,基类型名称.成员名 语法适用于属性元素语法。例如,下面的语法适用:   在这里,即使属性元素包含在 Button 中,属性元素也会以 Control.Background 形式提供。 但是,正如属性的类型名.成员名 格式一样,基类型名称.成员名 在标记中是很差的样式,您应当避免将其用于设置样式。 第 页 共 291 页 微软WPF应用 2. 代码隐藏和XAML 代码隐藏是一个术语,用于描述与将 XAML 页编译为应用程序时由 XAML 处理器创建的代码联接的代码。本主题描述代码隐藏的要求以及在 XAML 中的代码的可选内联代码机制。 先决条件 本主题假设您已阅读 XAML 概述并已基本了解 CLR 和面向对象的编程。 代码隐藏、事件处理程序和分部类要求 分部类必须派生自用作根元素的类的类型。您可以在代码隐藏的分部类定义中将派生留空,但编译的结果会假定页根作为分部类的基类,即使在没有指定的情况下也是如此(因为分部类的标记部分确实将页根指定为基)。 编写的事件处理程序必须是 x:Class 标识的命名空间中的分部类所定义的实例方法。您不能限定事件处理程序的名称来指示 XAML 处理器在其他类范围中查找该处理程序,也不能将静态方法用作事件处理程序。 事件处理程序必须与相应事件的委托匹配。 专门针对 Microsoft Visual Basic .NET 语言,您可以使用特定于语言的 Handles 关键字将处理程序与处理程序声明中的实例和事件关联,而不是在 XAML 中将处理程序附加到属性。但是,这一技术确实存在一些限制,因为 Handles 不支持 WPF 事件系统的所有特定功能,例如某些路由事件方案或附加事件。 x:Code x:Code 是在 XAML 中定义的一种指令元素。x:Code 指令元素可以包含内联编程代码。内联定义的代码可以与同一页中的 XAML 进行交互。下面的示例阐释了内联 C# 代码。请注意,该代码位于 x:Code 元素内,并且必须包围在 内,以便针对 XML 对内容进行转义,这样 XAML 处理器(解释 XAML 架构或 WPF 架构时)不会试图按原义将内容解释为 XML。   内联代码限制 应注意对基于 XAML 的应用程序避免或限制使用内联代码。在体系结构和编码原理方面,保留标记和代码隐藏之间的独立性可以更显著地区分设计人员和开发人员这两个角色。从更为技术性的角度看,为内联代码编写的代码更难编写,因为您总是要写入 XAML 页的生成的分部类中,并且只能使用默认的命名空间映射。因为不能添加 using 语句,因此必须完全限定您所进行的大量 API 调用。默认的 WPF 映射包括在 WPF 程序集中出现的大多数但并非全部的 CLR 命名空间;您必须完全限定对其他命名空间中包含的 API 的调用。此外,您还不能在内联代码中定义多个类,并且所有代码实体必须作为生成的分部类中的一个成员或变量存在。其他特定于语言的编程功能(例如宏或对全局变量或生成变量的 #ifdef)也不可用。 第 页 共 291 页 微软WPF应用 3. XAML和自定义类 可扩展应用程序标记语言 (XAML) 支持使用任何公共语言运行库 (CLR) 语言定义自定义类或结构,然后使用 XAML 标记(包括在同一标记文件中混合使用 Windows Presentation Foundation (WPF) 定义的 XAML 和自定义类的 XAML 标记)访问该类的功能。本主题讨论自定义类要用作 XAML 元素时所必须满足的要求。 应用程序或程序集中的自定义类 可以使用两种不同的方法定义 XAML 中使用的自定义类:在生成主 Windows Presentation Foundation (WPF) 应用程序的代码隐藏或其他代码中定义,或者在单独的程序集(如用作类库的可执行文件或 DLL)中定义为类。这些方法中的每一种都有特定的优点和缺点。 创建类库的优点是,任何这样的自定义类都可以在许多可能不同的应用程序中共享。单独的类库也使应用程序的版本问题更易控制,而且也简化了在 XAML 页上创建要用作根元素的类这一过程。 在应用程序中定义自定义类的优点是,此方法是相对轻量的方法,可最大限度减少当引入主可执行文件之外的单独程序集时遇到的部署和测试问题。但是,一个显著的缺点是,不能将同一程序集中定义的类用作 XAML 页的根元素。 无论是在相同还是不同程序集中定义自定义类,都需要在 CLR 命名空间和 XML 命名空间之间映射这些自定义类才能在 XAML 中使用它们。 自定义类作为 XAML 元素的要求 类要能够实例化为对象元素,必须满足以下要求: 自定义类必须是公共的且支持默认(无参数)公共构造函数。(托管代码结构隐式支持这样的构造函数。) 自定义类不能是嵌套类(嵌套类和其语法中的“点”会干扰其他 WPF 功能,例如附加属性)。 第 页 共 291 页 微软WPF应用 除了启用对象元素语法外,还应对将该对象用作其值类型的任何其他公共属性启用属性元素语法。这是因为,对象现在可以实例化为对象元素,而且可以填充此类属性的属性元素值。 自定义类的属性 (Property) 作为 XAML 属性 (Attribute) 的要求 属性必须引用按值类型(如基元),或者为在类级别具有默认构造函数或专用类型转换器的类型使用类。 或者,属性可以引用抽象类类型或接口。对于抽象类或接口,运行时的期望是,属性值必须使用实现该接口的实际类实例或从该抽象类派生的类实例填充。 属性可以在抽象类上声明,但只能在从抽象类派生的实际类上设置,因为创建类的对象元素完全要求有效的默认构造函数和可实例化的类。 启用了类型转换器的属性语法 如果在类级别提供专用的属性化类型转换器,则应用的类型转换将为任何需要实例化该类型的属性启用属性语法。类型转换器不启用该类型的对象元素用法;只有当存在该类型的默认构造函数时才会启用对象元素用法。所以,启用了类型转换器的属性一般而言在属性语法中不可用,除非该类型本身也支持对象元素语法。这一点的一个例外是,可以指定属性元素语法,但允许该属性元素包含一个字符串。该用法实质上相当于属性语法用法,这样的用法不常见,除非需要对属性值进行更可靠的空白处理。例如,以下用法是接受字符串的属性 (property) 元素用法和等效的属性 (attribute) 用法:   允许使用属性语法,但通过 XAML 禁止使用包含对象元素的属性元素语法的属性示例有各种接受 Cursor 类型的属性。Cursor 类有专用类型转换器 CursorConverter,但未公开默认构造函数,因此 Cursor 属性只能通过属性语法进行设置,即使实际 Cursor 类型为引用类型。 每个属性类型转换器 此外,属性本身也可以在属性级别声明类型转换器。对于基于适当类型的 ConvertFrom 操作,通过将属性 (attribute) 的传入字符串值作为输入进行处理,这将启用“mini language”,它将实例化内联属性 (property) 类型的对象。通常,这样做是为了提供方便的访问器,而不是作为在 XAML 中设置属性的唯一手段。但是,对于想要使用现有 CLR 类型(不提供默认构造函数或属性化类型转换器)的属性,也可以使用类型转换器。例如,WPF  API 中接受 CultureInfo 类型的某些属性。在此情况下,WPF 使用现有的 Microsoft .NET Framework CultureInfo 类型来更好地处理与早期版本框架的兼容性以及早期版本框架中使用的迁移方案,但 CultureInfo 类型不支持将必要的构造函数或类型级别的类型转换直接用作 XAML 属性值。 第 页 共 291 页 微软WPF应用 公开具有 XAML 用法的属性时,特别当您是控件作者时,尤其应考虑使用依赖项属性支持该属性。使用 XAML 处理器的现有 Windows Presentation Foundation (WPF) 实现时更是如此,因为使用 DependencyProperty 支持可以提高性能。对于用户期望的 XAML 可访问属性,依赖项属性将公开该属性的属性系统功能。这些功能包括动画、数据绑定和样式支持。 编写和属性化类型转换器 您可能经常需要编写自定义 TypeConverter 派生类,以便为属性类型提供类型转换。 有关自定义类事件的 XAML 事件处理程序属性语法的要求 若要将事件用作 CLR 事件,必须在支持默认构造函数的类上或可以在派生类中访问事件的抽象类上,将该事件公开为公共事件。为了可方便地用作路由事件,CLR 事件应实现显式 add 和 remove 方法,这两种方法分别添加和移除 CLR 事件签名的处理程序,并将这些处理程序转发到 AddHandler 和 RemoveHandler 方法。这些方法在事件所附加到的实例的路由事件处理程序存储区中添加或删除处理程序。 说明: 可以使用 AddHandler 直接注册路由事件的处理程序,而不用特意定义用于公开路由事件的 CLR 事件。通常建议不要这样做,因为该事件不会对附加处理程序启用 XAML 属性语法,并且所生成的类将为类对象模型提供不够透明的 XAML 视图。 编写集合属性 接受集合类型的属性具有的 XAML 语法允许您指定要添加到集合的对象。此语法有两种显著功能。 不需要在对象元素语法中指定属于集合对象的对象。如果在 XAML 中指定接受集合类型的属性,则隐式存在该集合类型。 该集合属性的子元素将被处理为集合的成员。通常,代码对集合成员的访问通过集合方法(如 Add)或集合索引器属性执行。但 XAML 语法不支持方法或索引器。对于生成元素树的操作,集合明显是很常见的要求,您需要在声明性 XAML 中通过某种方法填充这些集合。因此,处理集合属性的子元素的方法是将这些子元素添加到将作为集合属性类型值的集合中。 WPF XAML 处理器对集合属性的构成内容使用以下定义。属性的属性类型必须实现以下内容之一: 实现 IList。 第 页 共 291 页 微软WPF应用 实现 IDictionary 或等效泛型 (IDictionary<(Of <(TKey, TValue>)>))。 从 Array 派生 实现 IAddChild(WPF 定义的接口)。 这每一种类型都具有 Add 方法,XAML 处理器使用该方法向基础集合中添加项。 说明: 由 WPF XAML 处理器执行的集合检测功能不支持泛型列表和字典接口(IList<(Of <(T>)>) 和 IDictionary<(Of <(TKey, TValue>)>))。但是,可以将 List<(Of <(T>)>) 类用作基类(因为它直接实现 IList),或者将 Dictionary<(Of <(TKey, TValue>)>) 用作基类(因为它直接实现 IDictionary)。 声明接受集合的属性时,务必注意在该类型的新实例中初始化属性值的方式。如果未将属性实现为依赖项属性,则使属性使用可调用集合类型构造函数的支持字段是合适的。如果属性是依赖项属性,则可能需要将集合属性初始化为默认类型构造函数的一部分。这是因为依赖项属性从元数据接受其默认值,您通常不会希望集合属性的初始值是静态的共享集合(每个包含类型实例都应有集合实例)。 您可以为集合属性实现自定义集合类型。由于集合属性隐式进行处理,因此自定义集合类型不需要提供默认构造函数就可以在 XAML 中隐式使用。但是,也可以选择为集合类型提供默认构造函数。这可能是一种值得的做法,因为除非确实提供了默认构造函数,否则不能显式将集合声明为对象元素。一些标记作者可能喜欢将显式集合视作一种标记样式。另外,在创建将集合类型用作属性值的新对象时,默认构造函数可以简化初始化要求。  声明 XAML 内容属性 XAML 语言定义了 XAML 内容属性的概念。对象语法中可用的每个类恰好有一个 XAML 内容属性。若要将属性声明为类的 XAML 内容属性,请将 ContentPropertyAttribute 作为类定义的一部分进行应用。在属性中将要使用的 XAML 内容属性的名称指定为 Name。 您可以将集合属性指定为 XAML 内容属性。这将导致使用该属性,由此对象元素可以有一个或多个子元素,而没有任何插入集合对象元素或属性元素标记。这些元素然后被作为 XAML 内容属性的值进行处理,并添加到支持集合实例中。 一些现有的 WPF XAML 内容属性使用 Object 的属性类型。这将使 XAML 内容属性可接受基元值(如 String),并可接受单个引用对象值。如果遵从此模型,则您的类型将负责类型确定和可能类型的处理。使用 Object 类型模型的一般原因有两种,一种是支持将对象内容添加为字符串的简单方式(接受默认呈现处理),另一种是支持添加对象内容(指定非默认呈现)的高级方式。 第 页 共 291 页 微软WPF应用  序列化 XAML 对于某些情况,例如如果您是控件作者,则可能还需要确保 XAML 中可实例化的任何对象表示形式也可以反序列化到等效 XAML。本主题中不介绍序列化要求。 4. 标记扩展和XAML 本主题介绍可扩展应用程序标记语言 (XAML) 的标记扩展概念,包括其语法规则、用途以及底层的类对象模型。  XAML 处理器和标记扩展 XAML 处理器是指可根据其规范(通过编译或解释)将 XAML 接受为语言、并且可以生成结果基础类以供运行时对象模型使用(也是根据 XAML 规范)的任意程序。默认情况下,此类处理器要么将属性值解释为一个文本字符串,要么基于属性类型或该属性特定的类型转换器将该属性值转换为对象。不过,有时也存在要求其他行为的情况。例如,可能指示 XAML 处理器:某个属性的值应该是对已构造对象或静态对象的引用。或者指示 XAML 处理器使用向对象的构造函数提供非默认参数的语法。相对于指定的 XAML 处理器默认行为,这是一种反常行为。  基本标记扩展语法 可以实现标记扩展以便为属性 (Attribute) 用法中的属性 (Property) 和/或属性 (Property) 元素用法中的属性 (Property) 提供值。 当用于提供属性 (Attribute) 值时,将标记扩展与 XAML 处理器区分开来的语法就是左右大括号({ 和 })。然后,由紧跟在左大括号后面的字符串标记来标识标记扩展的类型。 当用在属性元素语法中时,标记扩展在外观上与其他任何用于提供属性元素值的元素相同,即:一个将标记扩展类作为一个元素引用并以尖括号 (<>) 括起的 XAML 元素声明。  特定于 WPF 的标记扩展 WPF 编程中最常用的标记扩展是支持资源引用的标记扩展(StaticResource 和 DynamicResource)以及支持数据绑定的标记扩展 (Binding)。 StaticResource 通过替换已定义资源的值来为 XAML 属性提供值。 DynamicResource 通过将值推迟为对资源的运行时引用来为 XAML 属性提供值。动态资源引用强制在每次访问此类资源时都重新进行查找。 Binding 按应用于元素的数据上下文来为属性提供数据绑定值。此标记扩展相对复杂,因为它会启用大量内联语法来指定数据绑定。 第 页 共 291 页 微软WPF应用 RelativeSource 为可以在运行时元素树中定位若干可能关系的 Binding 提供源信息。对于在多用途模板中创建的绑定,或在未充分了解周围的元素树的情况下以代码创建的绑定,上述标记扩展会提供专用源。 通过 TemplateBinding,控件模板可以使用来自要利用该模板的类的对象模型定义属性中的模板化属性的值。 XAML 定义的标记扩展 有几个标记扩展并非是 XAML 的 WPF 应用程序所特有的,而是属于 XAML 语言的规范和命名空间的一部分。它们通常由语法中的 x: 前缀标识,如您在常见用法中所见到的一样。这些标记扩展的 WPF 实现使用相同的 MarkupExtension 基类来提供实现。 说明: x: 前缀在 XAML 应用程序或文档的根元素中用于 XAML 命名空间的典型命名空间映射。例如,Microsoft Visual Studio 2005 模板使用此 x: 映射启动 XAML 文件。您可以在自己的 xmlns 映射中选择一个不同的前缀标记,但是本文档采用默认的 x: 映射,作为标识那些属于 XAML 命名空间已定义部分的实体的一种方式,这与 WPF 命名空间或其他任意命名空间是相反的。 x:Type 为命名类型提供 Type 对象。此标记扩展最常用于样式和模板。 x:Static 从不直接属于属性值类型、但可以计算为该类型的值类型代码实体中生成静态值。 x:Null 将 null 指定为 XAML 属性的值。 在特意不使用基元素和控件模型提供的集合支持的情况下,x:Array 为 XAML 语法中常规数组的创建提供支持。 有关标记扩展语法的更多信息 *Extension 类 每个标记扩展的行为都会通过从 MarkupExtension 派生的 *Extension 类通知给 XAML 处理器,并提供 ProvideValue 方法的实现。每个扩展的此方法都会定义在计算标记扩展后将返回哪个对象。通常,通过使用传递给标记扩展的各个字符串标记来对返回的对象进行实例化或设置。 例如,StaticResourceExtension 类提供实际资源查找的图面实现,以便其 ProvideValue 实现返回请求的对象,该特定实现的输入是用于按其 x:Key 查找资源的字符串。如果您使用的是现有标记扩展,则其中的大部分实现详细信息都无关紧要。 第 页 共 291 页 微软WPF应用 后续字符串标记的扩展类解释 跟在标记扩展标识符后面、并且仍在括号内的字符串标记由 XAML 处理器通过以下方式之一解释: 逗号始终代表各个标记的分隔符。因此,文本逗号无法传递给标记扩展。 如果各个分隔的标记不包含任何等号,则每个标记都将被视为构造函数参数。每个构造函数参数都必须按该签名所期望的类型给出,并按照该签名所期望的顺序排列。 说明: XAML 处理器必须调用与对的数量这一参数计数匹配的构造函数。为此,如果您要实现自定义标记扩展,请不要提供具有相同实参计数的多个形参;对于当存在多个具有相同形参计数的标记扩展构造函数时将发生的情况,尚未定义相应的行为。 如果各个分隔的标记包含等号,则 XAML 处理器会首先为标记扩展调用默认构造函数。之后,每个“名称=值”对都会被解释为标记扩展上存在的属性名称以及赋给该属性的值。 如果在标记扩展中的构造函数行为与属性设置行为之间存在并行结果,则您使用哪个行为都无关紧要。如果仅仅因为将“属性=值”对用于具有多个可设置属性的标记扩展就可以令您的标记意图性更强,并减小意外转置构造函数参数的可能性(当您指定“属性=值”对时,这些属性可以为任意顺序),那么这种用法更常见。另外,无法保证标记扩展提供设置每个可设置属性的构造函数参数。例如,Binding 是一个标记扩展,具有多个可以通过“属性=值”形式的扩展来设置的属性,但 Binding 仅支持两个构造函数,即默认构造函数和一个设置初始路径的构造函数。 转义文本大括号 XAML 处理器中的属性处理使用大括号作为标记扩展的指示符。必要时,还可以使用后面跟文本大括号的空大括号对来输入转义序列,从而生成文本大括号字符属性值。 嵌套扩展语法 支持多个标记扩展的嵌套,并且将首先计算每个标记扩展的最里层,例如: < P Property="Background"> Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" /> 有关标记扩展和属性元素语法的更多信息 当用作填写属性元素值的对象元素时,标记扩展类在外观上与可用在 XAML 中的普通元素没有区别。这种情况下,普通元素与标记扩展之间的实际差异就在于标记扩展要么可计算为一个类型值,要么延迟为一个表达式,因此属性值的任何可能的类型错误的机制都将是不同的,这与后期绑定属性在其他编程模型中的处理方式类似。普通元素将计算为类型,而属性是在编译时直接设置的。 第 页 共 291 页 微软WPF应用 当用在对象元素语法中以填充属性元素时,大部分标记扩展都不会包含内容或任何进一步的属性元素语法,这样您就可以关闭对象元素标记,而不提供任何子元素。不论何时 XAML 处理器遇到任何对象元素,都会调用该类的构造函数以实例化从已分析元素创建的对象。标记扩展类也一样;因此,如果您希望自己的标记扩展在对象元素语法中可用,就必须提供默认构造函数。有些现有标记扩展至少具有一个必需的属性值,为了使初始化有效,必须指定该值。在这种情况下,通常会将该属性值作为对象元素的属性特性来指定。在 XAML 命名空间 (x:) 语言功能和 WPF 命名空间 XAML 扩展参考页中,将指出具有必需属性的标记扩展(以及必需属性的名称)。参考页还将指出特定标记扩展是否禁止使用对象元素语法或属性语法。一个值得注意的情况是 x:Array 标记扩展,它无法支持属性语法,因为必须指定该数组的内容。数组内容的处理方式与常规对象一样,因此属性没有默认类型转换器是可行的。另外,x:Array 标记扩展需要 Type 参数。 5. XAML命名空间和命名空间映射 本主题进一步解释每个可扩展应用程序标记语言 (XAML) 文件的根标记中存在的两个命名空间映射及其用途,同时还介绍如何生成类似的映射,以便使用在您自己的代码中和/或单独的程序集中定义的元素。 WPF 和 XAML 命名空间声明 在许多可扩展应用程序标记语言 (XAML) 文件的根标记中的命名空间声明内,您都可以看到两个 xmlns 声明。第一个声明将整个 Windows Presentation Foundation (WPF) 命名空间映射为默认命名空间: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 第二个声明映射单独的可扩展应用程序标记语言 (XAML) 命名空间,通常将其映射为 x: 前缀。 xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 这些声明之间的关系是:XAML 实际上是语言标准,而 WPF 是将 XAML 作为语言使用的一个实现。XAML 语言指定一些为了兼容而假定要实现的语言元素,每个元素都应当能通过针对 XAML 命名空间执行的 XAML 处理器实现进行访问。WPF 实现为其自己的 API 保留默认命名空间,为 XAML 中需要的标记语法使用单独的映射前缀。按照约定,该前缀是 x:,此 x: 约定后面是项目模板、示例代码和此 SDK 中语言功能的文档。XAML 命名空间定义了许多常用功能,这些功能即使对于基本的 WPF 应用程序也是必需的。例如,若要通过分部类将任何代码隐藏加入 XAML 文件,您必须将该类命名为相关 XAML 文件的根元素中的 x:Class 属性。或者,在 XAML 页中定义的、您希望作为键控资源访问的任何元素应当对相关元素设置了 x:Key 属性。 第 页 共 291 页 微软WPF应用 映射到自定义类和程序集 您可以在 xmlns 前缀声明中使用一系列标记将命名空间映射到程序集,这与将标准 WPF 和 XAML 命名空间映射到前缀类似。 语法使用下列可能的命名标记和值: clr-namespace: 在包含要作为元素公开的公共类型的程序集中声明的公共语言运行库 (CLR) 命名空间。 assembly= 是指包含部分或全部引用的 CLR 命名空间的程序集。该值通常只是程序集的名称,而不是路径。该程序集的路径必须在生成编译的 XAML 的项目文件中以项目引用的形式建立。另外,为了合并版本管理和强名称签名,该值也可以是 AssemblyName 定义的字符串。 请注意,分隔 clr-namespace 标记和其值的字符是冒号 (:),而分隔 assembly 标记和其值的字符是等号 (=)。这两个标记之间使用的字符是分号。例如: xmlns:custom="clr-namespace:SDKSample;assembly=SDKSampleLibrary" 映射到当前程序集 如果引用的 clr-namespace 是在引用自定义类的应用程序代码所在的程序集中定义的,则可以省略 assembly。这种情况的等效语法是指定 assembly=,等号后不需要任何字符串标记。 如果自定义类是在同一程序集中定义的,则不能将其用作页的根元素。不需要映射分部类;如果您希望在 XAML 中将自定义类作为元素引用,只需要映射应用程序中页的非分部类。  在程序集中将 CLR 命名空间映射为 XML 命名空间 WPF 定义一个 CLR 属性,XAML 处理器使用它来将多个 CLR 命名空间映射到单个 XML 命名空间。XmlnsDefinitionAttribute 属性放置在生成程序集的源代码中的程序集级别。WPF 程序集源代码使用此属性将各种常见的命名空间(如 System.Windows 和 System.Windows.Controls)映射到 http://schemas.microsoft.com/winfx/2006/xaml/presentation 命名空间。 XmlnsDefinitionAttribute 采用两个参数:XML 命名空间名称和 CLR 命名空间名称。可以存在多个 XmlnsDefinitionAttribute,这样便可以将多个 CLR 命名空间映射到同一 XML 命名空间。映射后,如果需要,还可以通过在分部类代码隐藏页中提供相应的 using 语句来引用这些命名空间的成员,而无需完全限定。 第 页 共 291 页 微软WPF应用 6. WPF名称范围 名称范围既是一种概念,也是用于存储对象的 XAML 定义名称及其实例等效项之间的关系的编程对象。加载 XAML 应用程序的页面时,即在 WPF 托管代码中创建了名称范围。作为编程对象的名称范围由 INameScope 接口定义,并且还由实际类 NameScope 实现。  加载的 XAML 应用程序中的名称范围 处理 XAML 页时,即对该页的根元素创建了名称范围。该页中指定的每个名称都会添加到相关的名称范围中。作为常见根元素(例如 Page 和 Window)的元素总是控制名称范围。如果在标记中某个元素(例如 FrameworkElement 或 FrameworkContentElement)是页的根元素,则 XAML 处理器会隐式添加一个 Page 根元素,以便 Page 可以提供一个名称范围。即使最初在 XAML 中没有定义 Name 或 x:Name 属性,也会创建一个名称范围。 如果试图在任意名称范围中两次使用同一个名称,则会引发异常。对于具有代码隐藏并且是已编译的应用程序的一部分的 XAML,在创建页的已生成类时会引发该异常。 将元素添加到已分析的元素树 若要在初始的加载和处理之后向元素树添加任何元素,都必须对定义名称范围的类调用相应的 RegisterName 的实现。否则,无法通过 FindName 等方法按名称引用添加的对象。仅设置 Name 属性(或 x:Name 属性)不会将该名称注册到任何名称范围中。将命名的元素添加到具有名称范围的元素树中也不会将此名称注册到名称范围中。尽管名称范围可以嵌套,但通常您应该将名称注册到根元素上存在的名称范围中,这样您的名称范围位置便可与在等效的加载 XAML 页中可能已创建的名称范围并列。 应用程序开发人员最常用的方案是使用 RegisterName 将名称注册到当前根元素的名称范围中。RegisterName 是查找将作为动画运行的演示图板的一种重要方案的一部分。有关更多信息,请参见演示图板概述。如果您对同一逻辑树中的非根元素的元素调用 RegisterName,则该名称仍然会注册到最靠近根元素的元素,就好像对该根元素调用了 RegisterName 一样。 代码中的名称范围 对于以编程方式创建(而不是来自加载的 XAML)的应用程序,若要支持名称范围,根元素必须实现 INameScope、或者必须是 FrameworkElement 或 FrameworkContentElement 派生类。 此外,对于不是由 XAML 处理器加载和处理的任何元素,默认情况下不会创建或初始化该对象的名称范围。必须为随后要向其中注册名称的任何元素显式创建新的名称范围。若要为元素创建名称范围,可调用静态 SetNameScope 方法。将该元素指定为 dependencyObject 参数,并且将新的 NameScope 构造函数调用指定为 value 参数。 第 页 共 291 页 微软WPF应用 如果作为 SetNameScope 的 dependencyObject 提供的对象不是 INameScope 实现,也不是 FrameworkElement 或 FrameworkContentElement,那么,对任何子元素调用 RegisterName 均无效。如果您无法显式创建新的名称范围,则调用 RegisterName 将引发异常。  样式和模板中的名称范围 WPF 中的样式和模板提供了以简单的方法重新使用和重新应用内容的功能,但样式和模板可能还包括具有在模板级别指定的名称的元素。同一个模板可能在某个页中被多次重复使用。因此,样式和模板都定义其自己的名称范围,而与样式或模板所应用到的包含页无关。 请看下面的示例:   此处,同一个模板被应用到两个不同的按钮。如果模板没有独立的名称范围,则该模板中用到的 TheBorder 名称会导致名称冲突。模板的每个实例化都有其自已的名称范围,因此在本示例中每个实例化模板的名称范围都只包含一个名称。 样式也具有其自己的名称范围,因此大多数情况下演示图板的某些部分会分配有特殊的名称。即使模板重新定义为控件自定义项的一部分,这些名称也会启用以具有此名称的元素为目标的控件特定行为。 由于名称范围是独立的,因此在模板中查找命名的元素比在页中查找非模板化的元素更具有挑战性。首先需要确定所应用的模板,方法是获取该模板所应用到的控件的 Template 属性值。然后,调用 FindName 的模板版本,将模板所应用到的控件作为第二个参数进行传递。 如果您是控件作者,您要生成一个约定,即所应用的模板中的特定命名元素是控件本身所定义的行为的目标,则可以在控件实现代码中使用 GetTemplateChild 方法。由于 GetTemplateChild 方法是受保护的,因此只有控件作者才可以访问它。 第 页 共 291 页 微软WPF应用 如果您正在使用模板,并且需要用到该模板所应用到的名称范围,则获取 TemplatedParent,然后调用其中的 FindName。举一个使用模板的例子:您正在编写事件处理程序实现,其中该事件将从所应用的模板中的元素被引发。  名称范围和名称相关的 API FrameworkElement 具有 FindName、RegisterName 和 UnregisterName 方法。如果您对其调用这些方法的元素拥有名称范围,则这些元素方法仅调入名称范围方法。否则,会查看其父元素是否具有名称范围,此进程会以递归方式继续执行,直到找到一个名称范围为止(由于 XAML 处理器行为的原因,可以保证在根元素有名称范围)。FrameworkContentElement 具有类似的行为,所不同的是 FrameworkContentElement 从不会有名称范围。在 FrameworkContentElement 上存在上述方法,因此这些调用最终可以转发到 FrameworkElement 父元素。 SetNameScope 用于将新的名称范围映射到现有的对象。您可以多次调用 SetNameScope 以重置或清除名称范围,但这种用法并不常见。此外,通常不通过代码使用 GetNameScope。 名称范围实现 下面的类直接实现 INameScope: NameScope Style ResourceDictionary FrameworkTemplate ResourceDictionary 不使用名称范围,而是使用键,原因是它是一种字典哈希表实现。ResourceDictionary 之所以实现 INameScope,其唯一的原因是它可以对用户代码引发异常,从而有助于澄清真正的名称范围和 ResourceDictionary 处理键的方式之间的区别,此外,还可以保证名称范围不会被父元素特别应用到 ResourceDictionary。 FrameworkTemplate 和 Style 通过显式接口定义实现 INameScope。这些显式实现允许这些名称范围在通过 INameScope 接口进行访问时表现为常规行为,这是名称范围与 WPF 内部进程进行通信的方式。但是,这些显式接口定义不是 FrameworkTemplate 和 Style 的常规名称范围的一部分,原因是几乎不需要对 FrameworkTemplate 和 Style 直接调用 INameScope 方法。 下面的类都定义自己的名称范围,方法是使用 System.Windows..::.NameScope 帮助器类并通过 NameScope 附加属性连接到相应的名称范围实现: FrameworkElement 第 页 共 291 页 微软WPF应用 FrameworkContentElement WPF控件开发 1. WPF控件开发之控件概述 Windows Presentation Foundation (WPF) 附带了许多几乎可以在所有 Windows 应用程序中使用的常见 UI 组件,其中包括 Button、Label、TextBox、Menu 和 ListBox。以前,这些对象被称为控件。不过,WPF SDK 仍继续使用术语“控件”,泛指任何代表应用程序中可见对象的类。请注意,类不必从 Control 类继承,即可具有可见外观。从 Control 类继承的类包含一个 ControlTemplate,允许控件的使用方在无需创建新子类的情况下根本改变控件的外观。 本主题讨论在 WPF 中使用控件(包括从 Control 类继承的控件以及不从该类继承的控件)的常见方式。  创建控件的实例 可以通过使用可扩展应用程序标记语言 (XAML) 或以代码形式向应用程序添加控件。 下面的示例演示如何创建一个向用户询问其姓名的简单应用程序。 此示例在 XAML 中创建六个控件:两个标签、两个文本框及两个按钮。所有控件都可以按相似的方式创建。 第 页 共 291 页 微软WPF应用   下面的示例以代码的形式创建同一应用程序。为了简单起见,示例中不包括 Grid grid1 的创建。grid1 与前面的 XAML 示例内容具有相同的列和行定义。 第 页 共 291 页 微软WPF应用   第 页 共 291 页 微软WPF应用   更改控件外观 更改控件的外观以适应应用程序的外观,这是很常见的操作。可以根据您要达到的效果,通过执行以下操作之一来更改控件的外观: 更改控件的属性值。 为控件创建 Style。 为控件创建新 ControlTemplate。 更改控件的属性值 许多控件具有允许您更改控件外观的属性,例如 Button 的 Background。可以在 XAML 和代码中设置值属性。下面的示例在 XAML 中设置 Button 的 Background、FontSize 和 FontWeight 属性。 第 页 共 291 页 微软WPF应用   下面的示例在代码中设置相同的属性。    为控件创建样式 利用 WPF,通过创建 Style,您可以同时为许多控件指定相同的外观,而不是在应用程序中设置每个实例的属性。 下面的示例创建一个 Style,它可以应用于应用程序中的每个 Button。Style 定义通常是在 ResourceDictionary(例如 FrameworkElement 的 Resources)中以 XAML 形式定义的。 第 页 共 291 页 微软WPF应用 通过将键分配给样式并在控件的 Style 属性中指定该键,还可将样式仅应用于某些特定类型的控件。 创建 ControlTemplate 利用 Style,可以一次为多个控件设置属性,但有时除了通过创建 Style 可执行的操作之外,您可能想要自定义 Control 的外观。从 Control 类继承的类具有 ControlTemplate,它用于定义 Control 的结构和外观。Control 的 Template 属性是公共的,因此您可以为 Control 指定非默认 ControlTemplate。通常,您可以为 Control 指定新的 ControlTemplate(而不是从控件继承)以自定义 Control 的外观。 请考虑一个很常用的控件 Button。 Button 的主要行为是当用户单击它时让应用程序实现某些操作。 默认情况下,WPF 中的 Button 显示为一个凸出的矩形。 开发应用程序时,您可能会希望利用 Button 的行为(即通过处理按钮的单击事件),不过,除了通过更改按钮的属性可以执行的操作外,您也可以更改按钮的外观。 在这种情况下,您可以创建新 ControlTemplate。 下面的示例创建 Button 的 ControlTemplate。 ControlTemplate 创建一个带圆角和渐变背景的 Button。 ControlTemplate 包含一个 Border,其 Background 为 LinearGradientBrush,它具有两个 GradientStop 对象。 第一个 GradientStop 使用数据绑定将 GradientStop 的 Color 属性绑定到按钮背景的颜色。 当设置 Button 的 Background 属性时,该值的颜色将用作第一个 GradientStop。此示例还创建一个 Trigger,用于在 IsPressed 为 true 时更改 Button 的外观。 第 页 共 291 页 微软WPF应用    第 页 共 291 页 微软WPF应用 说明: 为使此示例正常工作,Button 的 Background 属性必须设置为 SolidColorBrush。  订阅事件 您可以通过使用 XAML 或代码来订阅控件的事件,但只能在代码中处理事件。 下面的示例演示如何订阅 Button 的 Click 事件。       下面的示例处理 Button 的 Click 事件。     控件中的丰富内容 从 Control 类继承的大多数类具有包含丰富内容的能力。例如,Label 可以包含任意对象,例如字符串、Image 或 Panel。 下列类支持丰富内容,可以用作 WPF 中大多数控件的基类。 ContentControl - 从此类继承的类的部分示例包括有 Label、Button 和 ToolTip。 ItemsControl - 从此类继承的类的部分示例包括有 ListBox、Menu 和 StatusBar。 HeaderedContentControl - 从此类继承的类的部分示例包括有 TabItem、GroupBox 和 Expander。 HeaderedItemsControl - 从此类继承的类的部分示例包括有 MenuItem、TreeViewItem 和 ToolBar。 第 页 共 291 页 微软WPF应用 2. 使用XAML创建按钮 本演练的目的在于介绍如何创建要在 Windows Presentation Foundation (WPF) 应用程序中使用的动画按钮。本演练使用样式和模板来创建自定义的按钮资源,通过该按钮资源可以重用代码并将按钮逻辑与按钮声明分开。本演练完全是用可扩展应用程序标记语言 (XAML) 编写的。 重要说明: 本演练指导您通过在 Microsoft Visual Studio 中键入或复制和粘贴可扩展应用程序标记语言 (XAML) 来逐步创建应用程序。 下图显示的是已完成的按钮。  创建基本按钮 让我们首先从创建一个新项目并向窗口中添加几个按钮开始。 创建新的 WPF 项目并向窗口中添加按钮 启动 Visual Studio。 创建新的 WPF 项目:在“文件”菜单上,指向“新建”,再单击“项目”。此时将打开下图所示的窗口。在左窗格中的“Visual C#”节点下面,选择“.NET Framework 3.0”。在右窗格中,选择“Windows 应用程序 (WPF)”模板。将该项目命名为“AnimatedButton”。这将创建应用程序的主干。 第 页 共 291 页 微软WPF应用 添加基本的默认按钮:本演练中所需的全部文件均由该模板提供。通过在解决方案资源管理器中双击 Window1.xaml 文件来打开它。默认情况下,Window1.xaml 中存在一个 Grid 元素。移除 Grid 元素,并通过向 Window1.xaml 中键入或复制和粘贴以下突出显示的代码来向可扩展应用程序标记语言 (XAML) 页面中添加几个按钮:   按 F5 运行该应用程序;您应当能够看到类似下图的一组按钮。 第 页 共 291 页 微软WPF应用 现在您已经创建了基本按钮,这样就完成了在 Window1.xaml 文件中的工作。本演练其余部分重点介绍如何在 app.xaml 文件中为这些按钮定义样式和模板。 设置基本属性 接着,我们将会为这些按钮设置一些属性,以便控制按钮外观和布局。您将使用资源来为整个应用程序定义按钮属性,而不是为这些按钮单独设置属性。在概念上,应用程序资源与网页的外部级联样式表 (CSS) 相似;但是,资源远比级联样式表 (CSS) 强大,在本演练结束时您将明白这一点。 使用样式为按钮设置基本属性 定义 Application.Resources 块:打开 app.xaml 并添加下面突出显示的标记(如果尚未添加):       资源范围由资源的定义位置来确定。如果资源是在 app.xaml 文件的 Application.Resoureses 中定义的,则将允许从应用程序中的任何位置使用资源。 创建一个样式并用该样式定义基本属性值:向 Application.Resources 块添加下面的标记。此标记创建一个应用于该应用程序中所有按钮的 Style,并将这些按钮的 Width 设置为 90,将 Margin 设置为 10: 第 页 共 291 页 微软WPF应用 TargetType 属性指定将该样式应用于 Button 类型的所有对象。每个 Setter 都为 Style 设置不同的属性值。因此,此时该应用程序中的每个按钮的宽度都为 90,边距都为 10。如果您按 F5 运行该应用程序,则将看到下面的窗口。 您还可以对样式进行更多的处理,这包括以各种方式微调对象的目标、指定复杂的属性值,甚至将样式作为其他样式的输入。 为资源设置样式属性值:使用资源,可以通过一种简单的方式来重用通常定义的对象和值。为了使代码更加模块化,使用资源定义复杂值尤其有用。向 app.xaml 添加下面突出显示的标记。   您已经在 Application.Resources 块的正下方创建了一个名为“GrayBlueGradientBrush”的资源。此资源将定义一个水平渐变。此资源可以在该应用程序中的任何位置(包括在 Background 属性的按钮样式 setter 内部中)用作属性值。现在,所有的按钮都具有此渐变的 Background 属性值。 第 页 共 291 页 微软WPF应用 按 F5 运行该应用程序。其外观类似于下图: 创建一个定义按钮外观的模板 在本节中,将创建一个用来自定义按钮外观(表示)的模板。按钮表示是由几个赋予按钮独特外观的对象(包括矩形和其他组件)组成的。 到目前为止,对应用程序中按钮外观的控制已限制为更改按钮的属性。如果您希望更彻底地改变按钮的外观,该怎么办?使用模板可以强有力地控制对象的表示。由于模板可以在样式中使用,因此您可以将模板应用于所有应用了样式的对象(在本演练中为按钮)。 使用模板定义按钮的外观 设置模板:由于控件(如 Button)具有 Template 属性,因此您可以像对 Style 中所设置的其他属性值那样,使用 Setter 来定义模板属性值。向按钮样式中添加下面突出显示的标记。    第 页 共 291 页 微软WPF应用 更改按钮表示:此时,您需要定义模板。添加下面突出显示的标记。此标记指定了两个后跟 DockPanel 且具有圆角的 Rectangle 元素。DockPanel 用于承载按钮的 ContentPresenter。A ContentPresenter 显示按钮的内容。本演练中,内容为文本 ("Button 1", "Button 2", "Button 3")。所有模板组件(矩形和 DockPanel)都放置在一个 Grid 内。   按 F5 运行该应用程序。其外观类似于下图: 向模板中添加玻璃效果:接着将添加玻璃效果。首先创建一些用来生成玻璃渐变效果的资源。将这些渐变资源添加到 Application.Resources 块中的任何位置: 第 页 共 291 页 微软WPF应用   这些资源将用作按钮模板的 Grid 中所插入的矩形的 Fill。向模板添加下面突出显示的标记。   请注意,x:Name 属性为“glassCube”的矩形的 Opacity 为 0,因此,当您运行该示例时,将看到上面并没有覆盖玻璃矩形。这是由于我们将在以后向该模板中添加用户与该按钮交互时将触发的触发器。但是,您可以通过将 Opacity 值更改为 1 并运行该应用程序来查看按钮现在的外观。请参见下图。在转至下一步之前,请将 Opacity 更改回 0。 第 页 共 291 页 微软WPF应用 创建按钮交互性 在本节中,将创建属性触发器和事件触发器来更改属性值和运行动画,以响应用户操作(如将鼠标指针移到按钮上并单击)。 添加交互性(鼠标悬停、鼠标离开和单击等)的一个简便方法就是在模板或样式内部定义触发器。若要创建 Trigger,需要定义一个属性“条件”,例如:按钮的 IsMouseOver 属性值等于 true。随后将定义在触发条件为 true 时所发生的 setter(操作)。 创建按钮交互性 添加模板触发器:向模板添加突出显示的标记。 第 页 共 291 页 微软WPF应用   添加属性触发器:向 ControlTemplate.Triggers 块添加突出显示的标记: 第 页 共 291 页 微软WPF应用   按 F5 运行应用程序,并查看在将鼠标指针移到按钮上时的效果。 添加焦点触发器:接着,将添加一些类似的 setter 来处理当按钮具有焦点时(例如,当用户单击按钮之后)的情况。   按 F5 运行应用程序,并单击其中的某个按钮。请注意,在单击该按钮之后,该按钮仍具有焦点,因此它仍将保持突出显示状态。如果您单击另一个按钮,则新按钮将获得焦点,而上一个按钮则失去焦点。 为 MouseEnter 和 MouseLeave 添加动画:接着将向触发器添加一些动画。在 ControlTemplate.Triggers 块内部的任意位置添加下面的标记。 第 页 共 291 页 微软WPF应用   当鼠标指针移到该按钮上时,玻璃矩形会收缩;当指针离开该按钮时,镜面矩形会返回到其正常大小。 当指针移到该按钮上(引发 MouseEnter 事件)时会触发两个动画。这些动画会使玻璃矩形沿着 X 轴和 Y 轴收缩。请注意 DoubleAnimation 元素的属性:Duration 和 By。Duration 指定动画持续半秒多,By 指定玻璃收缩 10%。 第二个事件触发器 (MouseLeave) 只是停止第一个触发器。当您停止某个 Storyboard 时,所有的动画属性都恢复到其默认值。因此,当用户将指针移开按钮时,该按钮将返回到鼠标指针移到其上之前的状态。 添加单击按钮时的动画效果:最后一步是添加在用户单击按钮时将触发的触发器。在 ControlTemplate.Triggers 块内部的任意位置添加下面的标记: 第 页 共 291 页 微软WPF应用   按 F5 运行应用程序,并单击其中的某个按钮。当您单击某个按钮时,玻璃矩形会旋转。 摘要 在本演练中,进行了下列练习: 使 Style 面向对象类型 (Button)。 使用 Style 控制整个应用程序中按钮的基本属性。 创建要用于 Style setter 的属性值的资源(如渐变)。 通过向按钮应用模板来自定义整个应用程序中按钮的外观。 自定义按钮的行为(包括动画效果)来响应用户操作(如 MouseEnter、MouseLeave 和 Click)。 3. WPF控件库之Button Button 控件响应来自鼠标、键盘、手写笔或其他输入设备的用户输入,并引发一个 Click 事件。Button 是一个基本的用户界面 (UI) 组件,可以包含简单内容(例如文本),也可以包含复杂内容,例如图像和 Panel 控件。 处于默认状态、具有焦点和处于按下状态的按钮 第 页 共 291 页 微软WPF应用 示例 下面的示例创建两个 Button 控件。一个 Button 包含文本,另一个包含图像。当用户单击包含图像的 Button 时,另一个 Button 的背景和文本将发生变化。 此示例使用标记创建 Button 控件,但使用代码编写 Click 事件处理程序。     4. WPF控件库之Menu Menu 是一个控件,使用该控件可以对那些与命令或事件处理程序相关联的元素以分层方式进行组织。每个 Menu 可以包含多个 MenuItem 控件。每个 MenuItem 都可调用命令或调用 Click 事件处理程序。MenuItem 也可以有多个 MenuItem 元素作为子项,从而构成子菜单。 下图演示了菜单控件的三种不同状态。默认状态是没有设备(如鼠标指针)停留在 Menu 上时的状态。当鼠标指针悬停在 Menu 上时显示焦点状态,当在 Menu 上单击鼠标按钮时显示按下状态。 不同状态下的菜单 第 页 共 291 页 微软WPF应用 使用 Menu 类,可以按照分层顺序对与命令和事件处理程序相关联的元素进行组织。每个 Menu 元素都包含一个由 MenuItem 元素组成的集合。 Menu 控件 Menu 控件表示一系列用来为应用程序指定命令或选项的项。通常,单击 MenuItem 将打开一个子菜单或者导致应用程序执行相应的命令。 创建 Menu 下面的示例将创建一个 Menu 来操作 TextBox 中的文本。Menu 包含多个使用 Command、IsCheckable 和 Header 属性以及 Checked、Unchecked 和 Click 事件的 MenuItem 对象。 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用   具有键盘快捷键的 MenuItem 键盘快捷键是可以用键盘输入以调用 Menu 命令的字符组合。例如,“复制”的快捷键是 Ctrl+C。可以将以下两个属性与键盘快捷键和菜单项结合使用:InputGestureText 和 Command。 InputGestureText 下面的示例演示如何使用 InputGestureText 属性来将键盘快捷键文本分配给 MenuItem 控件。这只是将键盘快捷键放在菜单项中, 而不会将命令与 MenuItem 相关联。应用程序必须处理用户的输入才能执行该操作。   命令 下面的示例演示如何使用 Command 属性来将“打开”和“保存”命令与 MenuItem 控件相关联。该 Command 属性不但将命令与 MenuItem 相关联,而且还提供要用作快捷方式的输入笔势文本。 第 页 共 291 页 微软WPF应用   MenuItem 类还有一个 CommandTarget 属性,该属性指定要在其中执行命令的元素。如果未设置 CommandTarget,则具有键盘焦点的元素将收到命令。 Menu 样式 使用控件样式设置,可以显著改变 Menu 控件的外观和行为,而不必编写自定义控件。除了设置可视化属性外,还可以向控件的各个部分应用 Style,通过属性更改控件各个部分的行为,向控件中添加额外的部分,或者改变控件的布局。下面的示例演示了几种用来将 Style 添加到 Menu 控件中的方法。 第一个代码示例定义了一个名为 Simple 的 Style 来说明如何在您的样式中使用当前的系统设置。该代码将 MenuHighlightBrush 的颜色分配为菜单的背景色,将 MenuTextBrush 分配为菜单的前景色。请注意,您在分配画笔时使用的是资源键。   下面的示例使用的是 Trigger 元素,通过这些元素,可以改变 MenuItem 的外观来响应发生在 Menu 上的事件。当您将鼠标移到 Menu 上时,菜单项的前景色和字体特征会发生变化。   5. WPF控件库之Lable Label 控件通常在用户界面 (UI) 中提供信息。一直以来,Label 只包含文本,但由于 Windows Presentation Foundation (WPF) 附带的 Label 是一个 ContentControl,所以它可以包含文本或 UIElement。 第 页 共 291 页 微软WPF应用 Label 为快捷键提供功能性和可视化支持。它常用于实现对控件(如 TextBox)的快速键盘访问。若要为 Control 指定 Label,请将 Label..::.Target 属性设置为当用户按下快捷键时应获得焦点的控件。 下图演示了一个目标为 ComboBox 的 Label“主题”。当用户按下 Alt+T 时,ComboBox 将获得焦点。 示例 下面的示例演示如何创建一个 Label,该控件使用 AccessText 并且绑定到目标 TextBox。   示例 为标签添加文本换行 Label 控件不支持文本换行。如果您需要一个多次换行的标签,可以嵌套一个支持文本换行的元素,并将该元素放在标签内。下面的示例演示如何使用 TextBlock 创建一个进行多次文本换行的标签。   为标签添加访问键和文本换行 第 页 共 291 页 微软WPF应用 如果您需要一个具有访问键(助记键)的 Label,则可以使用 Label 中的 AccessText 元素。 Label、Button、RadioButton、CheckBox、MenuItem、TabItem、Expander 和 GroupBox 等控件具有默认的控件模板。这些模板包含一个 ContentPresenter。您可以为 ContentPresenter 设置的属性之一是 RecognizesAccessKey="true",您可以使用该属性为控件指定访问键。 下面的示例演示如何创建一个具有访问键并支持文本换行的 Label。为了实现文本换行,本示例设置了 TextWrapping 属性并使用下划线字符指定访问键。(紧跟下划线字符后面的字符就是访问键。)   6. WPF控件库之Toolbar ToolBar 控件是一组通常在功能上相关的命令或控件的容器。 下面的插图显示垂直和水平 ToolBar 控件。 水平 Toolbar 垂直 Toolbar   ToolBar 控件 ToolBar 控件因其按钮或其他控件像条形栏一样排列成一行或一列而得名。WPF ToolBar 控件提供一种溢出机制,将不能自然适合于有大小限制的 ToolBar 的任意项放入一个特殊的溢出区域。另外,WPF ToolBar 控件通常还与相关的 ToolBarTray 控件一起使用,后者提供特殊的布局行为,并支持用户启动的工具栏大小调整和排列。 第 页 共 291 页 微软WPF应用 在 ToolBarTray 中指定工具栏的位置 使用 Band 和 BandIndex 属性可以在 ToolBarTray 中定位 ToolBar。Band 指示 ToolBar 在其父 ToolBarTray 中的位置。BandIndex 指示 ToolBar 放入其 Band 中的顺序。下面的示例演示如何使用此属性在 ToolBarTray 内放置 ToolBar 控件。    具有溢出项的工具栏  通常,ToolBar 控件包含的项多于工具栏大小可以容纳的项数。出现此情况时,ToolBar 会显示一个溢出按钮。要查看溢出项,用户可以单击溢出按钮,这些项即会显示在 ToolBar 下方的弹出窗口中。下图显示了一个带溢出项的 ToolBar。 具有溢出项的工具栏 第 页 共 291 页 微软WPF应用 通过将 ToolBar..::.OverflowMode 附加属性设置为 OverflowMode..::.Always、OverflowMode..::.Never 或 OverflowMode..::.AsNeeded,您可以指定工具栏上的项何时会放置在溢出面板上。下面的示例指定工具栏上的最后四个按钮应总是显示在溢出面板上。   ToolBar 在其 ControlTemplate 中使用 ToolBarPanel 和 ToolBarOverflowPanel。 ToolBarPanel 负责工具栏上的项的布局。 ToolBarOverflowPanel 负责 ToolBar 上容不下的项的布局。 第 页 共 291 页 微软WPF应用 7. WPF控件开发之自定义控件 Windows Presentation Foundation (WPF) 控件模型的扩展性极大减少了创建新控件的需要。但在某些情况下,仍可能需要创建自定义控件。本主题讨论可最大限度减少在 Windows Presentation Foundation (WPF) 中创建自定义控件以及其他控件创作模型的需要的功能。本主题还演示如何创建新控件。 编写新控件的替代方法 以前,如果要通过现有控件获取自定义体验,您只能更改控件的标准属性,例如背景色、边框宽度和字号。如果希望在这些预定义参数的基础之上扩展控件的外观或行为,则需要创建新的控件,通常的方法是继承现有控件并重写负责绘制该控件的方法。虽然这仍是一种可选方法,但也可以利用 WPF 丰富内容模型、样式、模板和触发器来自定义现有的控件。下面的列表提供了一些示例,演示如何在不创建新控件的情况下使用这些功能来实现统一的自定义体验。 丰富内容。 很多标准 WPF 控件支持丰富内容。例如,Button 的内容属性为 Object 类型,因此从理论上讲,任何内容都可以显示在 Button 上。若要让按钮显示图像和文本,可以将图像和 TextBlock 添加到 StackPanel 中,然后将 StackPanel 分配给 Content 属性。由于这些控件可以显示 WPF 可视化元素和任意数据,因此,减少了创建新控件或修改现有控件来支持复杂可视化效果的需要。 样式。 Style 是表示控件属性的值的集合。使用样式可创建所需控件外观和行为的可重用表示形式,而无需编写新控件。例如,假设希望所有 TextBlock 控件都呈现字号为 14 的红色 Airal 字体。您可以创建一个样式作为资源,然后相应地设置适当的属性。这样,添加到应用程序中的每个 TextBlock 都将具有相同的外观。 数据模板。 DataTemplate 可用于自定义数据在控件上的显示方式。例如,DataTemplate 可用于指定数据在 ListBox 中的显示方式。有关这种情况的示例,请参见数据模板概述。除了自定义数据外观之外,DataTemplate 还可以包含 UI 元素,这样大大增加了自定义 UI 的灵活性。例如,使用 DataTemplate 可以创建一个 ComboBox,其中每一项都包含一个复选框。 控件模板 WPF 中的很多控件都使用 ControlTemplate 来定义控件的结构和外观,这样可将控件外观和控件功能分离开。通过重新定义控件的 ControlTemplate,可以彻底更改控件的外观。例如,假设您希望控件看起来像一个交通信号灯。此控件具有简单的用户界面和功能。该控件有三个圆形,一次只能点亮其中的一个。经过考虑之后,您可能意识到 RadioButton 提供了一次只选中一项的功能,但是 RadioButton 的默认外观完全不像交通信号灯上的灯。由于 RadioButton 使用控件模板来定义其外观,因此很容易重新定义 ControlTemplate 以符合该控件的要求,从而使用单选按钮来制作交通信号灯。 第 页 共 291 页 微软WPF应用 说明: 尽管 RadioButton 可以使用 DataTemplate,但在本例中,只使用 DataTemplate 还不够。DataTemplate 定义控件内容的外观。对于 RadioButton,指示 RadioButton 是否选中的那个圆形右侧显示出来的全部都是该控件的内容。在交通信号灯的示例中,单选按钮只需要成为可“点亮”的圆形。由于交通信号灯的外观要求与 RadioButton 的默认外观存在很大差异,因此,有必要重新定义 ControlTemplate。一般而言,DataTemplate 用于定义控件的内容(或数据),ControlTemplate 用于定义控件的构成方式。 触发器。 Trigger 用于在不创建新控件的情况下动态更改控件的外观和行为。例如,假设应用程序中有多个 ListBox 控件,而您希望每个 ListBox 中的项在选中时都显示为红色粗体。您首先想到的可能是创建一个从 ListBox 继承的类,然后重写 OnSelectionChanged 方法,以更改选中项的外观,不过,更好的方法是向 ListBoxItem 的样式添加一个更改选中项外观的触发器。触发器用于更改属性值或根据属性值执行操作。EventTrigger 用于在发生事件时执行操作。 一般而言,如果控件完全复制现有控件的功能,但您希望该控件具有不同的外观,则应先考虑是否可以使用本节中讨论的某些方法来更改现有控件的外观。 控件创作模型 通过丰富内容模型、样式、模板和触发器,最大程度地减少了创建新控件的需要。但是,如果确实需要创建新控件,那么理解 WPF 中的不同控件创作模型就显得非常重要。WPF 提供三个用于创建控件的一般模型,每个模型都提供不同的功能集和灵活度。这三个模型的基类分别为 UserControl、Control 和 FrameworkElement。 从 UserControl 派生 在 WPF 中创建控件的最简单方法是从 UserControl 派生。如果生成继承自 UserControl 的控件,需要将现有组件添加到 UserControl,命名这些组件,然后在 可扩展应用程序标记语言 (XAML) 中引用事件处理程序。执行这些操作之后,即可在代码中引用这些命名元素和定义事件处理程序。此开发模型非常类似于用于 WPF 应用程序开发的模型。 如果生成正确,UserControl 可以利用丰富内容、样式和触发器的优点。但是,如果控件继承自 UserControl,则使用该控件的用户将无法使用 DataTemplate 或 ControlTemplate 来自定义其外观。因此,有必要从 Control 类或其派生类(UserControl 除外)进行派生,以便创建支持模板的自定义控件。 从 UserControl 派生的优点 如果符合以下所有情况,请考虑从 UserControl 派生: 希望以类似于生成应用程序的方式生成控件。 第 页 共 291 页 微软WPF应用 控件仅由现有组件组成。 不需要支持复杂自定义项。 从 Control 派生 从 Control 类派生是大多数现有 WPF 控件使用的模型。在创建继承自 Control 类的控件时,可使用模板定义其外观。通过这种方式,可以将运算逻辑从可视化表示形式中分离出来。通过命令和绑定(而不是事件),也可确保分离 UI 和逻辑,尽可能避免引用 ControlTemplate 中的元素。如果控件的 UI 和逻辑正确分离,该控件的用户即可重新定义其 ControlTemplate,从而自定义其外观。尽管生成自定义 Control 不像生成 UserControl 那样简单,自定义 Control 还是提供了最大的灵活性。 从 Control 派生的优点 如果符合以下任一情况,请考虑从 Control 派生,而不要使用 UserControl 类: 希望控件外观能通过 ControlTemplate 进行自定义。 希望控件支持不同的主题。 从 FrameworkElement 派生 从 UserControl 或 Control 派生的控件依赖于组合现有元素。很多情况下,这是一种可接受的解决方案,因为从 FrameworkElement 继承的任何对象都可以位于 ControlTemplate 中。但是,某些时候,简单的元素组合不能满足控件的外观需要。对于这些情况,使组件基于 FrameworkElement 才是正确的选择。 生成基于 FrameworkElement 的组件有两种标准方法:直接呈现和自定义元素组合。直接呈现涉及的操作包括:重写 FrameworkElement 的 OnRender 方法,并提供显式定义组件视觉效果的 DrawingContext 操作。此方法由 Image 和 Border 使用。自定义元素组合涉及的操作包括使用 Visual 类型的对象组合组件的外观。有关示例,请参见使用 DrawingVisual 对象。Track 是 WPF 中使用自定义元素组合的控件示例。在同一控件中,也可以混合使用直接呈现和自定义元素组合。 从 FrameworkElement 派生的优点 如果符合以下任一情况,请考虑从 FrameworkElement 派生: 希望对控件的外观进行精确控制,而不仅仅是简单的元素组合提供的效果。 想要通过定义自己的呈现逻辑来定义控件的外观。 想要以一种 UserControl 和 Control 之外的新颖方式组合现有元素。  控件创作基础知识 如前所述,WPF 最强大的功能之一是,无需创建自定义控件即可实现远比设置控件的基本属性来更改其外观和行为更强的功能。WPF 属性系统和 WPF 事件系统使样式、数据绑定和触发器功能成为可能。如果在控件中实现依赖项属性和路由事件,则无论使用什么模型创建自定义控件,自定义控件的用户都可以像对 WPF 随附的控件那样使用这些功能。 第 页 共 291 页 微软WPF应用 使用依赖项属性 当属性为依赖项属性时,可以进行下面的操作: 在样式中设置该属性。 将该属性绑定到数据源。 使用动态资源作为该属性的值。 动画处理该属性。 如果希望控件的属性支持以上任一功能,则应将该属性实现为依赖项属性。下面的示例定义一个依赖项属性。   这段代码执行下面的操作: 第 页 共 291 页 微软WPF应用 将一个名为 ValueProperty 的 DependencyProperty 标识符定义为 publicstaticreadonly 字段。 通过调用 DependencyProperty..::.Register 向属性系统注册该属性名,以指定以下内容: 属性的名称。 属性的类型。 拥有该属性的类型。 属性的元数据。元数据包含该属性的默认值、CoerceValueCallback 和 PropertyChangedCallback。 通过实现该属性的 get 和 set 访问器,定义一个名为 Value 的 CLR“包装”属性,这个名称也就是用来注册该依赖项属性的名称。请注意,get 和 set 访问器只是分别调用 GetValue 和 SetValue。建议依赖项属性的访问器不要包含其他逻辑,这是因为客户端和 WPF 可绕过这两个访问器直接调用 GetValue 和 SetValue。例如,如果属性绑定到数据源,则不会调用该属性的 set 访问器。不要向 get 和 set 访问器添加其他逻辑,而应使用 ValidateValueCallback、CoerceValueCallback 和 PropertyChangedCallback 委托在值更改时进行响应或检查该值。 为 CoerceValueCallback 定义一个名为 CoerceValue 的方法。CoerceValue 确保 Value 大于或等于 MinValue 且小于或等于 MaxValue。 为 PropertyChangedCallback 定义一个名为 OnValueChanged 的方法。OnValueChanged 创建一个 RoutedPropertyChangedEventArgs<(Of <(T>)>) 对象,并准备引发 ValueChanged 路由事件。路由事件在下一节中讨论。 使用 RoutedEvent 就像依赖项属性以附加功能扩展 CLR 属性的用途一样,路由事件扩展了标准 CLR 事件的用途。在创建新的 WPF 控件时,将事件实现为路由事件也是一种好方法,这是因为路由事件支持以下行为: 事件可以在多个控件的父级上进行处理。如果事件是冒泡事件,则元素树中的单个父级可预订该事件。然后,应用程序作者可以使用一个处理程序来响应多个控件的该事件。例如,如果控件属于 ListBox 中的每个项(因为它包含在 DataTemplate 中),则应用程序开发人员可以为该控件的 ListBox 事件定义相应的事件处理程序。每当这些控件中的任何控件发生该事件时,都会调用该事件处理程序。 第 页 共 291 页 微软WPF应用 路由事件可在 EventSetter 中使用,应用程序开发人员通过 EventSetter 可以在样式内指定事件的处理程序。 路由事件可在 EventTrigger 中使用,这对于使用 XAML 对属性进行动画处理很有用。 下面的示例定义了一个路由事件。 这段代码执行下面的操作: 将一个名为 ValueChangedEvent 的 RoutedEvent 标识符定义为 publicstaticreadonly 字段。 通过调用 EventManager..::.RegisterRoutedEvent 方法注册路由事件。该示例在调用 RegisterRoutedEvent 时指定以下信息: 事件的名称为 ValueChanged。 路由策略为 Bubble,这意味着首先调用源(引发事件的对象)上的事件处理程序,然后从最近的父元素上的事件处理程序开始,相继调用源的各个父元素上的事件处理程序。 该事件处理程序的类型为 RoutedPropertyChangedEventHandler<(Of <(T>)>),是用 Decimal 类型构造的。 该事件的所属类型为 NumericUpDown。 声明一个名为 ValueChanged 的公共事件,并包含事件访问器声明。该示例调用 add 访问器声明中的 AddHandler 和 remove 访问器声明中的 RemoveHandler 来使用 WPF 事件服务。 第 页 共 291 页 微软WPF应用 创建一个名为 OnValueChanged 的受保护的虚方法,该方法引发 ValueChanged 事件。 使用绑定 若要将控件 UI 与其逻辑分离,请考虑使用数据绑定。如果是使用 ControlTemplate 定义控件的外观,这一点尤其重要。使用数据绑定时,可能能够避免引用 UI 的特定部分。 下面的示例更新 NumericUpDown 控件的 TextBlock,向它分配一个名称,然后在代码中按名称引用该文本框。   下面的示例使用绑定来达到相同的目的。 针对设计器设计 若要在 Visual Studio Windows Presentation Foundation (WPF) 设计器中获得对自定义 WPF 控件的支持(例如,使用“属性”窗口编辑属性),请遵循以下准则。 依赖项属性 确保实现 CLRget 访问器和 set 访问器,如前面“使用依赖项属性”中所述。设计器可以使用包装来检测某个依赖项属性是否存在,但与 WPF 和控件客户端一样,在获取或设置属性时不需要使用设计器来调用访问器。 附加属性 应按照以下原则在自定义控件上实现附加属性: 第 页 共 291 页 微软WPF应用 具有一个使用 RegisterAttached 方法创建的 publicstaticreadonlyDependencyProperty,其形式为“属性名称Property”。传递到 RegisterAttached 的属性名称必须与属性名称 匹配。 实现一对名为 Set属性名称 和 Get属性名称 的 public static CLR 方法。这两种方法都应接受从 DependencyProperty 派生的类作为其第一个参数。Set属性名称 方法还接受其类型与属性的注册数据类型匹配的参数。Get属性名称 方法应返回相同类型的值。如果缺少 Set属性名称 方法,则该属性标记为只读。 Set 属性名称 和 Get属性名称 必须分别直接路由到目标依赖项对象的 GetValue 和 SetValue 方法。通过调用方法包装或直接调用目标依赖项对象,设计器可以访问附加属性。 创建 UserControl 如前所述,在 WPF 中创建控件的最简单方法是从 UserControl 派生。下面的示例演示用于定义 NumericUpDownUserControl 的 用户界面 (UI) 的 XAML:   下面的示例演示此 UserControl 的逻辑。 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用 如此示例所示,自定义 UserControl 的开发模型非常类似于用于应用程序开发的模型。 创建自定义控件 生成支持模板的控件 UserControl 提供了一种简单方法在 WPF 中生成可重用的功能,但要利用模板化和支持不同主题,则要使用的模型为 Control。本节将上一节中的 UserControl 示例转换为自定义 Control。 更改基类 首先,将 UserControl 基类替换为 Control。 移动到模板 一旦更新了基类,则需要将控件的内容移动到模板。模板在可位于应用程序中的很多位置的样式中定义。对于此示例,样式位于应用程序资源中。 在 UserControl 示例中,TextBlock 和 RepeatButton 实例已指定了名称。RepeatButton 实例还引用了代码中定义的事件处理程序。可以在此自定义 Control 中移除这两个实例,因为将通过更松散耦合的方式,改为使用绑定和命令来获取相同的行为。   第 页 共 291 页 微软WPF应用 处理输入 在 UserControl 示例中,RepeatButton 实例直接引用了代码中定义的事件处理程序。对于自定义 Control,命令是实现相同行为的一种更灵活的方式。控件可以定义命令,如下面的示例所示。   第 页 共 291 页 微软WPF应用 然后,模板中的元素可以引用这些命令,如下面的示例所示。   通过定义模板以及使用绑定和命令,您已将 NumericUpDown 控件从具有固定可视化效果的静态 UserControl 更改为可灵活自定义的自定义 Control。 外部控件库 最后一步是将 NumericUpDown 控件打包到其自己的程序集中,以使重用更容易。 创建主题文件 将 NumericUpDown 类移动到库程序集之后,需要移动样式定义。首先,需要创建一个“themes”文件夹用于存储所有主题文件。接下来,创建一个名为 generic.xaml 的文件。此文件将用作此程序集的所有资源查找的回退。 在 generic.xaml 中定义一个 ResourceDictionary,并在 ResourceDictionary 中放置 NumericUpDown 控件的样式。 定义 ThemeInfo 属性 若要找到 generic.xaml 中的 Style,宿主应用程序需要知道该程序集包含控件特定的资源。可以通过向类中添加程序集属性来实现该任务。由于程序集仅包含一般资源,因此将 GenericDictionaryLocation 属性设置为 SourceAssembly。下面的示例显示了 AssemblyInfo.cs 文件的内容。   为 Windows 主题提供支持 一个控件在大量不同 WPF主题中可能有不同的外观。若要支持多个主题,必须使用控件所需的正确样式、模板和其他资源来定义主题文件。还需要将 ThemeInfoAttribute 属性 (Attribute) 的 ThemeDictionaryLocation 属性 (Property) 设置为引用源程序集,如下面的示例所示。 第 页 共 291 页 微软WPF应用   本文档概述在设计可方便地样式化和模板化的控件时需要考虑的一组最佳做法。在为内置的 WPF 控件集处理主题控件样式时,我们通过大量试验和错误总结出了这组最佳做法。我们已经认识到,成功的样式化不只是设计完善的对象模型的功能,也是样式本身的功能。本文档面向控件作者,而不是样式作者。 术语 “样式化和模板化”是一组技术,控件作者可以通过该组技术来将控件的可视化特性延迟到控件的样式和模板。这组技术包括: 样式(包括属性 setter、触发器和演示图板)。 资源。 控件模板。 数据模板。 准备工作:了解您的控件 在开始阅读本文档中的准则之前,一定要了解并定义了控件的一般用法,这一点很重要。样式化公开一组通常不受约束的可能性。旨在由许多开发人员在许多应用程序中广泛使用的控件面临着如下挑战:可以使用样式化来对控件的可视化外观进行广泛的更改。实际上,带有样式的控件甚至可能并非控件作者的本意。由于样式化在本质上可以提供无限的灵活性,因此您可以使用“一般用法”这一概念来帮助您限制自己的决定。 为了了解控件的一般用法,最好考虑控件的价值主张。您的控件能够提供哪些无法由其他控件提供的内容? 一般用法并不表示任何特定的可视化外观,而是表示控件的基本原理和一组有关其用法的合理预期。了解到这一点,就可以对控件在一般情况下的撰写模型和样式定义行为进行一些假设。例如,对于 ComboBox,如果您了解其一般用法,并不意味着您非常清楚特定的 ComboBox 是否有圆角,但是您将由此清楚 ComboBox 可能需要一个弹出窗口和某种用来切换其开关状态的方法。 通用准则 不严格实施模板约定。 控件的模板约定可以包含元素、命令、绑定和触发器,甚至还可以包含必需的或为了使控件正常工作而应当使用的属性设置。 尽可能减少约定。 第 页 共 291 页 微软WPF应用 围绕如下预期来设计:在设计时(即,在使用设计工具时),控件模板通常处于不完整状态。WPF 不提供“正在撰写”状态的基础结构,因此,控件必须围绕这样的状态可能有效这一预期来生成。 在没有遵循模板约定的任何方面时,不引发异常。按照这一原则,当面板的子级太多或太少时,面板不应当引发异常。 将外围功能分解成模板帮助器元素。 每个控件都应当将重点放在其核心功能和真正的价值主张上,而且每个控件都应当由控件的一般用法来定义。为此,请使用模板中的撰写和帮助器元素来实现外围行为和可视化,即,那些不构成控件核心功能的行为和可视化。帮助器元素可分为三类: 独立帮助器类型是以“匿名方式”用在模板中的可重用的公共控件或基元,这意味着帮助器元素和带有样式的控件无法互相识别。在技术上,任何元素都可以是匿名类型,但是在这里,术语“独立”用于描述那些封装专用功能以实现目标方案的类型。 基于类型的帮助器元素是封装专用功能的新类型。通常,这些元素在设计上比公用控件或基元的功能范围要窄。与独立帮助器元素不同的是,基于类型的帮助器元素能够识别它们的使用上下文,而且通常必须与包含它们所属模板的控件共享数据。 命名的帮助器元素是控件应当能够在其模板中根据名称找到的公用控件或基元。这些元素在模板中具有一个已知的名称,这使得控件能够找到这些元素并以编程方式与之交互。在任何模板中,都只能有一个具有给定名称的元素。 下表显示的是由目前的控件样式使用的部分帮助器元素列表:   第 页 共 291 页 微软WPF应用 尽可能减少帮助器元素所必需的、特定于用户的绑定或属性设置。通常,帮助器元素需要某些绑定或属性设置才能在控件模板中正确工作。帮助器元素和模板化控件应当尽可能多地生成这些设置。在设置属性或者建立绑定时,注意不要重写由用户设置的值。具体的最佳做法如下所示: 命名的帮助器元素应当由父级标识,而且父级应当针对帮助器元素建立任何必需的设置。 对于基于类型的帮助器元素,应当直接针对它们建立任何必需的设置。这样做可能需要帮助器元素查找它在使用时的信息上下文,包括其 TemplatedParent(它在使用时的模板的控件类型)。例如,在用于 ContentControl 派生类型时,ContentPresenter 会自动将它的 TemplatedParent 的 Content 属性绑定到它的 Content 属性。 独立帮助器元素不能按这种方式进行优化,这是因为按照定义,帮助器元素和父级不能相互识别。 使用 Name 属性来标记模板中的元素。如果控件需要在样式中查找某个元素才能以编程方式访问它,则该控件应当使用 Name 属性和 FindName 范例来进行查找。控件不应在未找到所需元素时引发异常,而是应在不提示的情况下禁用需要该元素的功能。 使用最佳做法来表示样式中的控件状态和行为。 下面按顺序列出了用来表示样式中的控件状态更改和行为的最佳做法。您应当使用列表上的第一项来实现您的方案。 属性绑定。示例:在 ComboBox..::.IsDropDownOpen 和 ToggleButton..::.IsChecked 之间绑定。 触发的属性更改或属性动画。示例:Button 的悬停状态。 命令。示例:ScrollBar 中的 LineUpCommand / LineDownCommand。 独立帮助器元素。示例:TabControl 中的 TabPanel。 基于类型的帮助器类型。示例:Button 中的 ContentPresenter 和 Slider 中的 TickBar。 命名的帮助器元素。示例:ComboBox 中的 TextBox。 命名帮助器类型中的冒泡事件。如果您侦听样式元素中的冒泡事件,则应当要求生成该事件的元素能够进行唯一标识。示例:ToolBar 中的 Thumb。 自定义 OnRender 行为。示例:Button 中的 ButtonChrome。 慎用样式触发器(与模板触发器相对)。会影响到模板中元素属性的触发器必须在模板中进行声明。会影响到控件属性(没有 TargetName)的触发器可以在样式中声明,除非您知道更改模板还可能会损坏触发器。 第 页 共 291 页 微软WPF应用 与现有的样式模式一致。 一个问题常常有多种解决办法。注意尽可能与现有的控件样式模式保持一致。这对于派生自同一基类型(例如,ContentControl、ItemsControl、RangeBase 等)的控件尤其重要。 在不重新模板化的情况下公开属性来启用常见自定义项方案。WPF 不支持可插入/可自定义的部件,因此控件用户只能使用两种自定义方法:直接设置属性或者使用样式设置属性。请记住,比较合适的做法是,设置数量有限的属性,使其面向极其常见的高优先级自定义项方案,否则的话,这些方案需要重新模板化。下面是有关何时以及如何启用自定义项方案的最佳方法: 极其常见的自定义项应当作为属性在控件上公开并由模板使用。 不太常见(尽管并非极少见)的自定义项应当作为附加属性公开并由模板使用。 需要对已知但是极少见的自定义项重新模板化,这一点也是可以接受的。 主题注意事项 主题样式应当尝试在所有的主题之间具有一致的属性语义,但不保证能够实现这一点。作为控件文档的一部分,控件应当具有一个描述其属性语义(即控件属性的“含义”)的文档。例如,ComboBox 控件应当定义 Background 属性在 ComboBox 中的含义。控件的默认样式应当尝试遵循在其文档中的所有主题中定义的语义。另一方面,控件用户应当注意属性语义可能因主题而异。在某些情况下,给定的属性在由特定主题所需的可视化约束下可能无法表示 (例如,对于许多控件来说,传统主题没有可以向其应用 Thickness 的边框)。 主题样式不需要在所有的主题之间具有一致的触发器语义。由控件样式通过触发器或动画公开的行为可能因主题而异。控件用户应当注意到,控件将不必使用同一个机制在所有的主题中实现一个特定的行为。例如,一个主题可以使用动画来表示悬停行为,而另一个主题则可以使用触发器。这可能会导致自定义控件上的行为保留出现不一致现象 (例如,如果控件的悬停状态是使用触发器来表示的,那么,更改背景属性可能不会影响该状态)。但是,如果悬停状态是使用动画来实现的,那么,更改背景属性可能会不可挽回地中断动画,从而中断状态过渡)。 主题样式不需要在所有的主题之间具有一致的“布局”语义。例如,默认的样式不需要保证控件将在所有的主题中占用同样的大小,也不需要保证控件将在所有的主题中具有同样的内容边距/空白。 8. WPF控件开发之装饰器 装饰器是一种特殊类型的 FrameworkElement,用于向用户提供可视化提示。对于其他用户,装饰器可用于将功能控点添加到元素中或提供有关控件的状态信息。 第 页 共 291 页 微软WPF应用  关于装饰器 Adorner 是绑定到 UIElement 的自定义 FrameworkElement。装饰器在 AdornerLayer 中呈现,这是一个始终位于装饰元素或装饰元素集合上方的呈现图面。装饰器的呈现独立于该装饰器所绑定到的 UIElement 的呈现。装饰器通常使用位于装饰元素左上部的标准 2-D 坐标原点,相对于其绑定到的元素进行定位。 装饰器的常见应用包括: 向 UIElement 添加功能控点,使用这些控点,用户可以通过某种方式(调整大小、旋转、重新定位等等)操作元素。 提供可视反馈以指示各种状态,或响应各种事件。 在 UIElement 上叠加视觉效果。 从视觉上遮盖或重写 UIElement 的一部分或全部。 Windows Presentation Foundation (WPF) 为装饰视觉元素提供一个基本框架。下表列出了装饰对象时使用的主要类型及其用途。下面是几个用法示例。 Adorner 一个抽象基类,所有具体装饰器的实现都从该类继承。 AdornerLayer 一个类,表示一个或多个装饰元素的装饰器的呈现层。 AdornerDecorator 一个类,使装饰器层与元素集合相关联。  实现自定义装饰器 Windows Presentation Foundation (WPF) 提供的装饰器框架主要用于支持创建自定义装饰器。您可以通过实现一个从抽象 Adorner 类继承的类来创建自定义装饰器。 说明: Adorner 的父级为 AdornerLayer,它呈现的是 Adorner,而不是正在装饰的元素。 下面的示例演示一个实现简单装饰器的类。该示例装饰器使用圆简单地装饰 UIElement 的各角。 第 页 共 291 页 微软WPF应用   下面的图像演示了应用于 TextBox 的 SimpleCircleAdorner。 第 页 共 291 页 微软WPF应用    装饰器的呈现行为 注意:装饰器不包括任何继承呈现行为,确保装饰器呈现是装饰器实施者的责任。 实现呈现行为的常见方式是重写 OnRenderSizeChanged 方法,并使用一个或多个 DrawingContext 对象来按需呈现装饰器的视觉效果(如上述示例所示)。 说明: 放置在装饰器层中的所有内容均呈现在您已设置的任何其余样式的顶部。也就是说,装饰器始终以可见的方式位于顶部,无法使用 z 顺序重写。 事件和命中测试 装饰器就像任何其他 FrameworkElement 一样接收输入事件。 因为装饰器的 z 顺序总是高于它所装饰的元素,所以该装饰器接收可能适用于基础装饰元素的输入事件(例如 Drop 或 MouseMove)。 装饰器可以侦听某些输入事件,并通过重新引发这些事件将其传递给基础装饰元素。 若要对装饰器下的元素启用传递命中测试,请在装饰器上将命中测试 IsHitTestVisible 的属性设置为 false。 装饰单个 UIElement 若要将装饰器绑定到特定的 UIElement,请按照以下步骤操作: 调用静态方法 GetAdornerLayer,为要装饰的 UIElement 获取一个 AdornerLayer 对象。GetAdornerLayer 从指定的 UIElement 开始沿着可视树向上走,返回它所发现的第一个装饰器层。(如果未发现装饰器层,则该方法返回 Null。) 调用 Add 方法将装饰器绑定到目标 UIElement。 以下示例将 SimpleCircleAdorner(如上述所示)绑定到名为 myTextBox 的 TextBox。     第 页 共 291 页 微软WPF应用 说明: 目前不支持使用可扩展应用程序标记语言 (XAML) 将装饰器绑定到另一个元素。 装饰面板的子级 若要将装饰器绑定到 Panel 的子级,请按照下列步骤操作: 调用 static 方法 GetAdornerLayer 为要绑定其子级的元素查找一个装饰器层。 依次枚举父元素的子级并调用 Add 方法将装饰器绑定到每个子级元素。 以下示例将 SimpleCircleAdorner(如上所示)绑定到名为 myStackPanel 的 StackPanel 的子级。     WPF数据绑定 1. 数据绑定概述 Windows Presentation Foundation (WPF) 数据绑定为应用程序提供了一种简单而一致的方法来显示数据以及与数据交互。元素可以以公共语言运行库 (CLR) 对象和 XML 的形式绑定到各种数据源的数据。ContentControl(如 Button)和 ItemsControl(如 ListBox 和 ListView)具有内置功能,使单个数据项或数据项集合可以进行灵活的样式设置。可以在数据之上生成排序、筛选和分组视图。 WPF 中的数据绑定功能与传统模型相比具有一些优势,包括本质上支持数据绑定的各种属性、灵活的数据 UI 表示形式,以及业务逻辑与 UI 的完全分离。 本主题首先讨论 WPF 数据绑定的基本概念,然后详细介绍 Binding 类以及数据绑定的其他功能的用法。 什么是数据绑定? 数据绑定是在应用程序 UI 与业务逻辑之间建立连接的过程。如果绑定具有正确设置并且数据提供正确通知,则当数据更改其值时,绑定到数据的元素会自动反映更改。数据绑定可能还意味着如果元素中数据的外部表现形式发生更改,则基础数据可以自动更新以反映更改。例如,如果用户编辑 TextBox 元素中的值,则基础数据值会自动更新以反映该更改。 第 页 共 291 页 微软WPF应用 数据绑定的一种典型用法是将服务器或本地配置数据放置到窗体或其他 UI 控件中。在 WPF 中,此概念得到扩展,包括了大量属性与各种数据源的绑定。在 WPF 中,元素的依赖项属性可以绑定到 CLR 对象(包括 ADO.NET 对象或与 Web 服务和 Web 属性相关联的对象)和 XML 数据。 有关数据绑定的示例,请看一看来自数据绑定演示的以下应用程序 UI:   上面是显示拍卖项列表的应用程序 UI。该应用程序演示数据绑定的以下功能: ListBox 的内容绑定到 AuctionItem 对象的集合。AuctionItem 对象具有一些属性,如 Description、StartPrice、StartDate、Category、SpecialFeatures。 ListBox 中显示的数据(AuctionItem 对象)进行模板化,以便显示每个拍卖项的说明和当前价格。这是使用一个 DataTemplate 实现的。此外,每个项的外观取决于要显示的 AuctionItem 的 SpecialFeatures 值。如果 AuctionItem 的 SpecialFeatures 值为 Color,则该项具有蓝色边框。如果该值为 Highlight,则该项具有橙色边框和一个星号。 第 页 共 291 页 微软WPF应用 用户可以使用提供的 CheckBox 对数据进行分组、筛选或排序。在上面的图像中,选中了“Group by category”(按类别分组)和“Sort by category and date”(按类别和日期排序)CheckBox。您可能已经注意到数据是根据产品类别分组的,而且类别名称按字母顺序排序。这些项在每个类别中也是按照起始日期排序的,虽然从该图像中很难注意到这一点。这是使用集合视图 实现的。 当用户选中一个项时,ContentControl 会显示选定项的详细信息。这称为主从方案。 StartDate 属性的类型为 DateTime,该类型返回一个日期,包括精确到毫秒的时间。在此应用程序中,使用了一个自定义转换器,以便显示较短的日期字符串。 当用户单击“Add Product”(添加产品)按钮时,会出现下面的窗体:   用户可以编辑窗体中的字段,使用简略预览和详细预览窗格来预览产品清单,然后单击“submit”(提交)以添加新的产品清单。任何现有的分组、筛选和排序功能都会应用于新项。在这种特殊情况下,在上面图像中输入的项会作为 Computer 类别中的第二项显示。 “Start Date”(起始日期)TextBox 中提供的验证逻辑未在此图像中显示。如果用户输入一个无效日期(无效的格式或过去的日期),则会通过一个 ToolTip 和 TextBox 旁的一个红色感叹号来通知用户。数据验证一节讨论了如何创建验证逻辑。 在详细介绍数据绑定的上述不同功能之前,我们会先在下一节中讨论一些对理解 WPF 数据绑定非常重要的基本概念。 第 页 共 291 页 微软WPF应用 基本数据绑定概念 不论要绑定什么元素,不论数据源的特性是什么,每个绑定都始终遵循下图所示的模型: 如上图所示,数据绑定实质上是绑定目标与绑定源之间的桥梁。该图演示以下基本的 WPF 数据绑定概念: 通常,每个绑定都具有四个组件:绑定目标对象、目标属性、绑定源,以及要使用的绑定源中的值的路径。例如,如果要将 TextBox 的内容绑定到 Employee 对象的 Name 属性,则目标对象是 TextBox,目标属性是 Text 属性,要使用的值是 Name,源对象是 Employee 对象。 目标属性必须为依赖项属性。大多数 UIElement 属性都是依赖项属性,而大多数依赖项属性(除了只读属性)默认情况下都支持数据绑定。(只有 DependencyObject 类型可以定义依赖项属性,所有 UIElement 都派生自 DependencyObject。) 尽管图中并未指出,但应该注意,绑定源对象并不限于自定义 CLR 对象。WPF 数据绑定支持 CLR 对象和 XML 形式的数据。举例来说,绑定源可以是 UIElement、任何列表对象、与 ADO.NET 数据或 Web 服务关联的 CLR 对象,或是包含 XML 数据的 XmlNode。 在阅读其他软件开发工具包 (SDK) 主题时,请务必记住一点:当您建立绑定时,您是将绑定目标绑定到 绑定源。例如,如果您要使用数据绑定在一个 ListBox 中显示一些基础 XML 数据,就是将 ListBox 绑定到 XML 数据。 若要建立绑定,请使用 Binding 对象。本主题的剩余部分会讨论与 Binding 对象相关的许多概念以及该对象的一些属性和用法。 数据流的方向 正如上文所述和上图中箭头所示,绑定的数据流可以从数据目标流向数据源(例如,当用户编辑 TextBox 的值时,源值会发生更改)和/或(如果绑定源提供正确的通知)从绑定源流向绑定目标(例如,TextBox 内容会随绑定源中的更改而进行更新)。 您可能希望应用程序使用户可以更改数据并将数据传播回源对象。或者,您可能不希望允许用户更新源数据。您可以通过设置 Binding 对象的 Mode 属性来对此进行控制。下图演示不同类型的数据流: 第 页 共 291 页 微软WPF应用 OneWay 绑定导致对源属性的更改会自动更新目标属性,但是对目标属性的更改不会传播回源属性。此绑定类型适用于绑定的控件为隐式只读控件的情况。例如,您可能绑定到如股票行情自动收录器这样的源,或许目标属性没有用于进行更改的控件接口(如表的数据绑定背景色)。如果无需监视目标属性的更改,则使用 OneWay 绑定模式可避免 TwoWay 绑定模式的系统开销。 TwoWay 绑定导致对源属性的更改会自动更新目标属性,而对目标属性的更改也会自动更新源属性。此绑定类型适用于可编辑窗体或其他完全交互式 UI 方案。大多数属性都默认为 OneWay 绑定,但是一些依赖项属性(通常为用户可编辑的控件的属性,如 TextBox 的 Text 属性和 CheckBox 的 IsChecked 属性)默认为 TwoWay 绑定。确定依赖项属性在默认情况下是单向绑定还是双向绑定的一种编程方式是使用 GetMetadata 获取属性的属性元数据,然后检查 BindsTwoWayByDefault 属性的布尔值。 OneWayToSource 与 OneWay 绑定相反;它在目标属性更改时更新源属性。一个示例方案是您只需要从 UI 重新计算源值的情况。 OneTime 绑定未在图中显示,该绑定会导致源属性初始化目标属性,但不传播后续更改。这意味着,如果数据上下文发生了更改,或者数据上下文中的对象发生了更改,则更改会反映在目标属性中。如果您使用的数据的当前状态的快照适于使用,或者这些数据是真正静态的,则适合使用此绑定类型。如果要使用源属性中的某个值初始化目标属性,并且事先不知道数据上下文,则也可以使用此绑定类型。此绑定类型实质上是 OneWay 绑定的简化形式,在源值不更改的情况下可以提供更好的性能。 请注意,若要检测源更改(适用于 OneWay 和 TwoWay 绑定),则源必须实现一种合适的属性更改通知机制(如 INotifyPropertyChanged)。 触发源更新的原因 TwoWay 或 OneWayToSource 绑定侦听目标属性的更改,并将这些更改传播回源。这称为更新源。例如,可以编辑文本框中的文本以更改基础源值。如上一节中所述,数据流的方向由绑定的 Mode 属性的值确定。 第 页 共 291 页 微软WPF应用 但是,源值是在您编辑文本的同时进行更新,还是在您结束编辑文本并将鼠标指针从文本框移走后才进行更新呢? 绑定的 UpdateSourceTrigger 属性确定触发源更新的原因。下图中右箭头的点演示 UpdateSourceTrigger 属性的角色: 如果 UpdateSourceTrigger 值为 PropertyChanged,则 TwoWay 或 OneWayToSource 绑定的右箭头指向的值会在目标属性更改时立刻进行更新。但是,如果 UpdateSourceTrigger 值为 LostFocus,则仅当目标属性失去焦点时,该值才会使用新值进行更新。 与 Mode 属性类似,不同的依赖项属性具有不同的默认 UpdateSourceTrigger 值。大多数依赖项属性的默认值都为 PropertyChanged,而 Text 属性的默认值为 LostFocus。这意味着,只要目标属性更改,源更新通常都会发生,这对于 CheckBox 和其他简单控件很有用。但对于文本字段,每次键击之后都进行更新会降低性能,用户也没有机会在提交新值之前使用退格键修改键入错误。这就是为什么 Text 属性的默认值是 LostFocus 而不是 PropertyChanged 的原因。 下表使用 TextBox 作为示例提供每个 UpdateSourceTrigger 值的示例方案:   创建绑定 前面几节中讨论的一些概念可以概括为:使用 Binding 对象建立绑定,每个绑定通常都具有四个组件:绑定目标、目标属性、绑定源、要使用的源值的路径。本节讨论如何设置绑定。 第 页 共 291 页 微软WPF应用 请看下面的示例,其中的绑定源对象是一个名为 MyData 的类,该类在 SDKSample 命名空间中定义。出于演示的目的,MyData 类具有一个名为 ColorName 的字符串属性,该属性的值设置为“Red”。因此,此示例生成一个具有红色背景的按钮。   如果将此示例应用于基本关系图,则生成的图如下所示。这是一个 OneWay 绑定,因为 Background 属性在默认情况下支持 OneWay 绑定。   您可能会奇怪为什么会这样,即使 ColorName 属性为类型字符串,而 Background 属性为 Brush 类型。这是由于进行了默认类型转换,此类型转换在数据转换一节中进行了讨论。 指定绑定源 请注意,在上一个示例中,绑定源是通过设置 DockPanel 元素上的 DataContext 属性来指定的。Button 随后从 DockPanel(这是其父元素)继承 DataContext 值。在这里重复一下,绑定源对象是绑定的四个必需组件之一。因此,如果未指定绑定源对象,则绑定将没有任何作用。 可通过多种方法指定绑定源对象。在将多个属性绑定到相同源时,可以使用父元素上的 DataContext 属性。但是,在各个绑定声明上指定绑定源有时可能更为合适。对于上一个示例,可以不使用 DataContext 属性,而是通过在按钮的绑定声明上直接设置 Source 属性来指定绑定源,如下面的示例中所示: 第 页 共 291 页 微软WPF应用   除了在元素上直接设置 DataContext 属性、从上级继承 DataContext 值(如第一个示例中的按钮)、通过设置 Binding 上的 Source 属性来显式指定绑定源(如最后一个示例中的按钮),还可以使用 ElementName 属性或 RelativeSource 属性指定绑定源。当绑定到应用程序中的其他元素时(例如在使用滑块调整按钮的宽度时),ElementName 属性是很有用的。当在 ControlTemplate 或 Style 中指定绑定时,RelativeSource 属性是很有用的。 指定值的路径 如果绑定源是一个对象,则可使用 Path 属性指定要用于绑定的值。如果要绑定到 XML 数据,则可使用 XPath 属性指定该值。在某些情况下,可以使用 Path 属性,即使在数据为 XML 时。例如,如果要访问返回的 XmlNode(作为 XPath 查询的结果)的 Name 属性,则应使用 Path 属性和 XPath 属性。 请注意,虽然我们已强调要使用的值的 Path 是绑定的四个必需组件之一,但在要绑定到整个对象的情况下,要使用的值会与绑定源对象相同。在这些情况下,不指定 Path 比较合适。请看下面的示例:   上面的示例使用空绑定语法:{Binding}。在此情况下,ListBox 从父 DockPanel 元素继承 DataContext(此示例中未演示)。当未指定路径时,默认为绑定到整个对象。换句话说,在此示例中路径已被省略,因为要将 ItemsSource 属性绑定到整个对象。 除了绑定到集合以外,在希望绑定到整个对象,而不是仅绑定到对象的单个属性时,也可以使用此方案。例如,在源对象为类型字符串,并且您仅仅希望绑定到该字符串本身时。另一种常见情况是您希望将一个元素绑定到具有多个属性的一个对象。 请注意,您可能需要应用自定义逻辑,使数据对您的绑定目标属性有意义。自定义逻辑的形式可以是自定义转换器(如果默认类型转换不存在)。 Binding 和 BindingExpression 在详细介绍数据绑定的其他功能和用法之前,介绍 BindingExpression 类会十分有用。如前面几节所述,Binding 类是用于绑定声明的高级别类;Binding 类提供了很多属性,您可以利用这些类来指定绑定的特征。相关类 BindingExpression 是维持源与目标之间的连接的基础对象。一个绑定包含可以在多个绑定表达式之间共享的所有信息。BindingExpression 是无法共享的实例表达式,其中包含有关 Binding 的所有实例信息。 第 页 共 291 页 微软WPF应用 例如,请看下面的示例,其中 myDataObject 是 MyData 类的实例,myBinding 是源 Binding 对象,MyData 类是包含一个名为 MyDataProperty 的字符串属性的已定义类。此示例将 mytext(TextBlock 的实例)的文本内容绑定到 MyDataProperty。     您可以使用相同的 myBinding 对象来创建其他绑定。例如,可以使用 myBinding 对象将复选框的文本内容绑定到 MyDataProperty。在这种情况下,将有两个 BindingExpression 实例共享 myBinding 对象。 可以通过对数据绑定对象调用 GetBindingExpression 的返回值来获取 BindingExpression 对象。以下主题演示 BindingExpression 类的一些用法: 如何:从绑定目标属性获取绑定对象 如何:控制文本框文本更新源的时间 数据转换 在上面的示例中,按钮是红色的,因为其 Background 属性绑定到一个值为“Red”的字符串属性。可以这样做的原因是 Brush 类型上提供了一个类型转换器,可以将字符串值转换为 Brush。 要将此信息添加到创建绑定一节的图中,关系图如下所示: 第 页 共 291 页 微软WPF应用   但是,如果绑定源对象不是具有类型字符串的属性,而是具有类型 Color 的 Color 属性,该怎么办? 在这种情况下,为了使绑定正常工作,您需要先将 Color 属性值转换为 Background 属性所接受的值。您需要通过实现 IValueConverter 接口来创建一个自定义转换器,如下面的示例中所示:   现在使用自定义转换器而不是默认转换,关系图如下所示:   在这里重复一下,由于要绑定到的类型中提供了类型转换器,因而可以使用默认转换。此行为取决于目标中可用的类型转换器。如果无法确定,请创建您自己的转换器。 下面提供了一些典型方案,在这些方案中,实现数据转换器是非常有意义的: 数据应根据区域性以不同方式显示。例如,可能需要根据在特定区域性中使用的值或标准,来实现货币转换器或日历日期/时间转换器。 第 页 共 291 页 微软WPF应用 使用的数据不一定会更改属性的文本值,但会更改其他某个值(如图像的源,或显示文本的颜色或样式)。在这种情况下,可以通过转换可能不合适的属性绑定(如将文本字段绑定到表单元格的 Background 属性)来使用转换器。 将多个控件或控件的多个属性绑定到相同数据。在这种情况下,主绑定可能仅显示文本,而其他绑定则处理特定的显示问题,但仍使用同一绑定作为源信息。 到目前为止,我们尚未讨论 MultiBinding(其目标属性具有绑定集合)。对于 MultiBinding,可以使用自定义 IMultiValueConverter 从绑定的值生成最终值。例如,可以从红色、蓝色和绿色的值来计算颜色,这些值可以是来自于相同或不同绑定源对象的值。 绑定到集合 绑定源对象可以视为其属性包含数据的单个对象,也可以视为通常组合在一起的多态对象的数据集合(如查询数据库的结果)。到目前为止,我们仅讨论了绑定到单个对象,但是绑定到数据集合是一个常见方案。例如,一个常见方案是使用 ItemsControl(如 ListBox、ListView 或 TreeView)来显示数据集合,如什么是数据绑定?一节中的应用程序所示。 幸运的是,基本关系图仍然适用。如果要将 ItemsControl 绑定到集合,则关系图如下所示:   正如此图中所示,若要将 ItemsControl 绑定到集合对象,应使用 ItemsSource 属性。可以将 ItemsSource 属性视为 ItemsControl 的内容。请注意,绑定是 OneWay,因为 ItemsSource 属性默认情况下支持 OneWay 绑定。 如何实现集合 可以枚举实现 IEnumerable 接口的任何集合。但是,若要设置动态绑定,以使集合中的插入或移除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。此接口公开一个事件,只要基础集合发生更改,都应该引发该事件。 WPF 提供 ObservableCollection<(Of <(T>)>) 类,它是公开 INotifyCollectionChanged 接口的数据集合的内置实现。请注意,为了完全支持将数据值从源对象传送到目标,支持可绑定属性的集合中的每个对象还必须实现 INotifyPropertyChanged 接口。 第 页 共 291 页 微软WPF应用 在实现自己的集合之前,请先考虑使用 ObservableCollection<(Of <(T>)>) 或一个现有的集合类,如 List<(Of <(T>)>)、Collection<(Of <(T>)>) 和 BindingList<(Of <(T>)>) 等。如果您有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。 集合视图 一旦 ItemsControl 绑定到数据集合,您可能希望对数据进行排序、筛选或分组。若要执行此操作,可以使用集合视图,这是实现 ICollectionView 接口的类。 什么是集合视图? 可以将集合视图视为位于绑定源集合顶部的层,您可以通过它使用排序、筛选和分组查询来导航和显示源集合,所有这些操作都无需操作基础源集合本身。如果源集合实现了 INotifyCollectionChanged 接口,则 CollectionChanged 事件引发的更改将传播到视图。 由于视图不会更改基础源集合,因此每个源集合可以有多个关联的视图。例如,您可以有 Task 对象的集合。通过使用视图,可以通过多种不同的方式来显示相同数据。例如,您可能希望在页面左侧显示按优先级排序的任务,而在页面右侧显示按区域分组的任务。 如何创建视图 创建和使用视图的一种方式是直接实例化视图对象并将其用作绑定源。例如,请考虑在什么是数据绑定?一节中演示的数据绑定演示应用程序。 该应用程序的实现方式是将 ListBox 绑定到数据集合上的视图,而不是直接绑定到数据集合。下面的示例摘自数据绑定演示应用程序。CollectionViewSource 类是 CollectionView 的可扩展应用程序标记语言 (XAML) 代理。在此特定示例中,视图的 Source 绑定到当前应用程序对象的 AuctionItems 集合(类型为 ObservableCollection<(Of <(T>)>))。   资源 listingDataView 随后用作应用程序中元素(如 ListBox)的绑定源: 第 页 共 291 页 微软WPF应用   若要为同一集合创建另一个视图,可以创建另一个 CollectionViewSource 实例并为其提供另一个 x:Key 名称。 将视图用作绑定源并不是创建和使用集合视图的唯一方式。所有集合都具有一个默认集合视图。例如,对于所有实现 IEnumerable 的集合,CollectionView 都是默认视图对象。ListCollectionView 是实现 IList 的集合的默认视图对象,而 BindingListCollectionView 是用于那些实现 IBindingList 的集合的集合视图类。若要获取默认视图,请使用 GetDefaultView 方法。 排序 如前所述,视图可以将排序顺序应用于集合。如同在基础集合中一样,数据可能具有或不具有相关的固有顺序。通过集合上的视图,您可以根据您提供的比较条件来应用顺序或更改默认顺序。由于这是基于客户端的数据视图,因此一种常见情况是用户可能希望根据列对应的值,对多列表格数据进行排序。通过使用视图,可以应用这种用户驱动的排序,而无需对基础集合进行任何更改,甚至不必再次查询集合内容。 下面的示例演示什么是数据绑定?一节中的应用程序 UI 的“Sort by category and date”(按类别和日期排序)CheckBox 的排序逻辑:   筛选 视图还可以将筛选器应用于集合。这意味着即使集合中可能存在一个项,此特定视图也仅用于显示整个集合的某个子集。可以根据条件在数据中进行筛选。例如,正如在什么是数据绑定? 一节中的应用程序的工作方式那样,“Show only bargains”(仅显示成交商品)CheckBox 包含了筛选出成交价为 25 美元或更高的项的逻辑。执行下面的代码可以在选中该 CheckBox 时将 ShowOnlyBargainsFilter 设置为 Filter 事件处理程序: 第 页 共 291 页 微软WPF应用   ShowOnlyBargainsFilter 事件处理程序具有以下实现:   如果直接使用一个 CollectionView 类而不是 CollectionViewSource,则应使用 Filter 属性来指定回调。 分组 视图支持分组功能,此功能使用户能够将集合视图中的集合分区为逻辑组。这些组可以是显式的,其中的用户提供组列表,也可以是隐式的,其中的组依据数据动态生成。 下面的示例演示“Group by category”(按类别分组)CheckBox 的逻辑:   当前记录指针 视图还支持当前项的概念。可以在集合视图中的对象之间导航。在导航时,您是在移动记录指针,该指针可用于检索存在于集合中的特定位置的对象。 请注意,当前记录指针的移动与应用于该集合的任何排序或筛选会相互产生某些影响。排序将当前记录指针保留在所选的最后一条记录上,但集合视图现在是围绕此指针重构的。(或许所选记录以前曾位于列表的开头,但现在所选记录可能在中间的某个位置。) 如果所选内容在筛选之后保留在视图中,则筛选操作会保留所选记录。否则,当前记录指针会设置到经过筛选的集合视图的第一条记录。 第 页 共 291 页 微软WPF应用 主-从绑定方案 当前项的概念不仅用于集合中项的导航,而且用于主-从绑定方案。再考虑一下什么是数据绑定?中的应用程序 UI。 在该应用程序中,ListBox 的所选内容决定了在 ContentControl 中显示的内容。换句话说,当选中一个 ListBox 项时,ContentControl 会显示选定项的详细信息。 通过将两个或更多控件绑定到同一视图可以轻松地实现主-从方案。下面这个摘自数据绑定演示的示例演示什么是数据绑定?中的应用程序 UI 上看到的 ListBox 和 ContentControl 的标记:   请注意,这两个控件都绑定到同一个源,即 listingDataView 静态资源(请参见如何创建视图一节中的此资源的定义)。可以这样做的原因是当单一实例对象(此示例中为 ContentControl)绑定到一个集合视图时,该对象会自动绑定到该视图的 CurrentItem。请注意,CollectionViewSource 对象会自动同步货币与所选内容。如果列表控件没有像示例中那样绑定到 CollectionViewSource 对象,则您需要将其 IsSynchronizedWithCurrentItem 属性设置为 true 以达到此目的。 您可能已经注意到上面的示例使用了一个模板。实际上,如果不使用模板(由 ContentControl 显式使用的模板以及由 ListBox 隐式使用的模板),则数据不会按照我们希望的方式显示。现在,我们开始介绍下一节中的数据模板化。 数据模板化 如果不使用数据模板,则什么是数据绑定?一节中的应用程序 UI 将如下所示: 第 页 共 291 页 微软WPF应用   如上一节中的示例所示,ListBox 控件和 ContentControl 都绑定到 AuctionItem 的整个集合对象(更具体地说,是绑定到集合对象上的视图)。如果没有关于如何显示数据集合的特定说明,则 ListBox 会显示基础集合中的每个对象的字符串表示形式,而 ContentControl 会显示绑定到的对象的字符串表示形式。 若要解决该问题,应用程序应定义 DataTemplate。如上一节中的示例所示,ContentControl 显式使用 detailsProductListingTemplate DataTemplate。在显示集合中的 AuctionItem 对象时,ListBox 控件隐式使用下面的 DataTemplate:    第 页 共 291 页 微软WPF应用 使用这两个 DataTemplate,生成的 UI 如什么是数据绑定?所示。 如屏幕快照所示,除了可以在控件中放置数据以外,使用 DataTemplate 还可以为数据定义引人注目的视觉效果。例如,上面的 DataTemplate 中使用了 DataTrigger,因而 SpecialFeatures 值为 HighLight 的 AuctionItem 会显示为带有橙色边框和一个星号。 数据验证 接受用户输入的大多数应用程序都需要具有验证逻辑,以确保用户输入了需要的信息。验证检查可以基于类型、范围、格式或其他应用程序特定的要求。本节讨论了数据验证在 WPF 中的工作方式。 将验证规则与绑定关联 使用 WPF 数据绑定模型可以将 ValidationRules 与 Binding 对象相关联。例如,下面是什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Price”(起始价格)TextBox 的 XAML: 第 页 共 291 页 微软WPF应用   ValidationRules 属性采用 ValidationRule 对象的集合。ExceptionValidationRule 是内置的 ValidationRule,用于检查在绑定源属性的更新过程中引发的异常。在此特定示例中,绑定源属性为 StartPrice(属于整数类型),而目标属性为 TextBox.Text。当用户输入的值无法转换为整数时,将引发异常,这会导致将绑定标记为无效。 也可以通过从 ValidationRule 类派生和实现 Validate 方法来创建自己的验证规则。下面的示例演示什么是数据绑定?一节中的 Add Product Listing(添加产品清单)“Start Date”(起始日期)TextBox 使用的规则:   StartDateEntryForm TextBox 使用此 FutureDateRule,如下面的示例中所示: 第 页 共 291 页 微软WPF应用   请注意,由于 UpdateSourceTrigger 值为 PropertyChanged,因此在每次键击时,绑定引擎都会更新源值,这意味着该引擎还会在每次键击时检查 ValidationRules 集合中的每条规则。我们会在“验证过程”一节中进行更深入的讨论。 提供视觉反馈 如果用户输入的值无效,则您可能希望在应用程序 UI 上提供一些有关错误的反馈。提供这种反馈的一种方式是将 Validation..::.ErrorTemplate 附加属性设置为自定义 ControlTemplate。如上一小节中所示,StartDateEntryForm TextBox 使用一个名为 validationTemplate 的 ErrorTemplate。下面的示例显示 validationTemplate 的定义:   AdornedElementPlaceholder 元素指定要装饰的控件应放置的位置。 此外,您可能还要使用 ToolTip 显示错误消息。StartDateEntryForm 和 StartPriceEntryForm TextBox 都使用样式 textStyleTextBox,该样式创建一个显示错误消息的 ToolTip。下面的示例显示 textStyleTextBox 的定义。当绑定元素的属性的一个或多个绑定发生错误时,附加属性 Validation.HasError 为 true。 第 页 共 291 页 微软WPF应用   使用自定义 ErrorTemplate 和 ToolTip,StartDateEntryForm TextBox 在发生验证错误时会如下所示:   如果您的 Binding 具有关联验证规则,但是您未在绑定控件上指定 ErrorTemplate,则在出现验证错误时会使用默认 ErrorTemplate 来通知用户。默认 ErrorTemplate 是一个在装饰器层中定义红色边框的控件模板。使用默认 ErrorTemplate 和 ToolTip,StartPriceEntryForm TextBox 的 UI 在发生验证错误时会如下所示:   有关如何提供逻辑以验证对话框中的所有控件的示例,请参见对话框概述中的“自定义对话框”一节。 验证过程 由于数据验证与从目标到源的更新有关,因此它仅应用于 TwoWay 和 OneWayToSource 绑定。每次将输入值传输到绑定源属性时会发生验证。在这里重复一下,导致源更新的原因取决于 UpdateSourceTrigger 属性的值,如触发源更新的原因一节中所述。 下面的插图提供了在数据绑定过程中适合进行验证的位置的可视表示形式: 第 页 共 291 页 微软WPF应用   如上图所示,验证在调用转换器之前从目标到源的值传输过程中发生。下面将介绍验证 过程,如上面的关系图中所标记的那样: 在将值从目标属性传输到源属性时,数据绑定引擎首先移除可能已添加到绑定元素的 Validation.Errors 附加属性的任何 ValidationError。然后,数据绑定引擎检查是否为该 Binding 定义了自定义 ValidationRule;如果已经定义,则它将调用每个 ValidationRule 上的 Validate 方法,直到其中一个规则出错或者全部规则都通过为止。 如果某个自定义规则未通过,则绑定引擎会创建一个 ValidationError 对象,并将该对象添加到绑定元素的 Validation.Errors 集合。如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性会设置为 true。此外,如果 Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将引发该元素上的 Validation.Error 附加事件。 如果所有规则都通过,则绑定引擎会调用转换器(如果存在)。 如果转换器通过,则绑定引擎会调用源属性的 setter。 如果绑定具有与其关联的 ExceptionValidationRule,并且在步骤 4 中引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。您可以选择使用 UpdateSourceExceptionFilter 回调来提供用于处理异常的自定义处理程序。如果未对 Binding 指定 UpdateSourceExceptionFilter,则绑定引擎将对异常创建 ValidationError 并将其添加到绑定元素的 Validation.Errors 集合。 还应注意,任何方向(目标到源或源到目标)的有效值传输操作都将清除 Validation.Errors 附加属性。 调试机制 可以在绑定相关对象上设置附加属性 PresentationTraceSources..::.TraceLevel 以接收有关特定绑定的状态的信息。 第 页 共 291 页 微软WPF应用 2. WPF数据绑定之绑定源 在数据绑定中,绑定源(源)对象指的是您从其获取数据的对象。本主题讨论可以用作源的对象类型。 Windows Presentation Foundation (WPF) 数据绑定支持下列类型的绑定源:    将 CLR 类用作绑定源对象 本节讨论您在实现 CLR 类以用作源对象时需要了解的内容。 提供更改通知 如果您要使用 OneWay 或 TwoWay 绑定(您希望 UI 在源属性发生动态变化时进行更新),则必须实现一个适当的“属性已更改”通知机制。建议的机制是使 CLR 类实现 INotifyPropertyChanged 接口。 如果您不实现 INotifyPropertyChanged,则必须安排您自己的通知系统,确保绑定中使用的数据是最新的。您可以通过支持需要更改通知的每个属性的 PropertyChanged 模式来提供更改通知。若要支持此模式,请定义每个属性的属性名称 Changed 事件,其中属性名称 是属性的名称。每次更改属性时都会引发此事件。 如果源对象实现了正确的通知机制,则会自动进行目标更新。如果源对象由于某种原因未提供正确的属性更改通知,那么您可以选择使用 UpdateTarget 方法来显式更新目标属性。 其他特征 下表提供了需要注意的其他几个要点: 第 页 共 291 页 微软WPF应用 如果您要在 XAML 中创建对象,则类必须使用默认构造函数。在某些 .NET 语言(例如 C#)中,可能已为您创建默认构造函数。 用作绑定源属性的属性必须是类的公共属性。不能出于绑定目的来访问显式定义的接口属性,也不能访问没有基实现的受保护的私有或虚拟属性。 您不能绑定到 CLR 类的公共字段。 在您的类中声明的属性类型是传递到绑定的类型。但是,绑定最终使用的类型取决于绑定目标属性的类型,而不是源属性的类型。如果类型不同,您可能需要编写一个转换器来处理最初将您的自定义属性传递到绑定的方式。 将整个对象用作绑定源 可以将整个对象用作绑定源。要实现此目的,可以使用 Source 或 DataContext 属性将对象指定为绑定源,然后提供一个不含路径的空白绑定声明:{Binding}。这种方法可发挥较大作用的情况包括:要绑定到属于类型字符串的对象;要绑定到具有您关注的多个属性的对象;要绑定到集合对象。 请注意,您可能需要应用自定义逻辑,使数据对您的绑定目标属性有意义。自定义逻辑的形式可以是自定义转换器(如果默认类型转换不存在)或者 DataTemplate。 将集合对象用作绑定源 通常,您要用作源的对象是包含多个自定义对象的集合,其中的每个自定义对象都代表一个数据对象,该数据对象用作重复绑定的一个实例的源。例如,您有一个 CustomerOrders 集合,它由 CustomerOrder 对象组成,您的应用程序可以循环访问该集合以确定存在多少订单以及每个订单包含的数据。 可以枚举实现 IEnumerable 接口的任何集合。但是,若要设置动态绑定,以便集合中的插入或删除操作可以自动更新 UI,则该集合必须实现 INotifyCollectionChanged 接口。此接口公开一个事件,只要基础集合发生更改,就必须引发该事件。 WPF 提供 ObservableCollection<(Of <(T>)>) 类,它是公开 INotifyCollectionChanged 接口的数据集合的内置实现。该集合中的各个数据对象必须满足前面各节中描述的要求。有关示例,请参见如何:创建和绑定到 ObservableCollection。在实现您自己的集合之前,请考虑使用 ObservableCollection<(Of <(T>)>) 或使用现有集合类之一,如 List<(Of <(T>)>)、Collection<(Of <(T>)>) 和 BindingList<(Of <(T>)>) 等。 如果您有高级方案并且希望实现自己的集合,请考虑使用 IList,它提供可以按索引逐个访问的对象的非泛型集合,因而可提供最佳性能。 第 页 共 291 页 微软WPF应用 权限要求 下表总结了在完全信任或部分信任级别下执行的应用程序中可以绑定到的属性类型:   下表描述了数据绑定中有关权限要求的要点: 对于 CLR 属性,只要绑定引擎能够使用反射访问源属性,数据绑定就能进行。否则,绑定引擎会发出找不到属性的警告,并使用回退值或默认值(如果可用)。 始终可以绑定到依赖项属性。 XML 绑定的权限要求与此类似:在部分信任沙盒中,如果 XmlDataProvider 无权访问给定的数据,则它将失败。 3. WPF数据绑定之数据模板 WPF 数据模板模型为定义数据的表示提供了很大的灵活性。WPF 控件具有支持自定义数据表示的内置功能。本主题首先演示如何定义 DataTemplate,然后介绍其他数据模板功能,例如根据自定义逻辑选择模板和支持显示分层数据。 先决条件 本主题重点介绍数据模板功能,不介绍数据绑定概念。有关基本数据绑定概念的信息,请参见数据绑定概述。 DataTemplate 用于数据表示,是 WPF 样式和模板模型提供的许多功能中的一种。 另外,了解Resources也很重要,它实际上是有关使对象(例如,Style 和 DataTemplate)成为可重用对象的内容。 数据模板基础 为了说明 DataTemplate 为什么这么重要,让我们演示一个数据绑定的示例。在本示例中,有一个绑定到 Task 对象列表的 ListBox。每个 Task 对象都有 TaskName (string)、Description (string)、Priority (int) 和类型 TaskType 的属性,它是一个 Enum,其值为 Home 和 Work。 第 页 共 291 页 微软WPF应用   没有 DataTemplate 如果没有 DataTemplate,我们的 ListBox 当前具有如下外观:   由于没有任何特定说明,ListBox 在尝试显示集合中的对象时,默认情况下会调用 ToString。因此,如果 Task 对象重写 ToString 方法,则 ListBox 显示基础集合中每个源对象的字符串表示形式。 例如,如果 Task 类以这种方式重写 ToString 方法,其中 name 是 TaskName 属性的字段: 第 页 共 291 页 微软WPF应用   则 ListBox 如下所示:   但是,这是受到限制的,并不灵活。另外,如果您要绑定到 XML 数据,您不能重写 ToString。 定义简单 DataTemplate 该解决方案定义 DataTemplate。这样做的一种方式是将 ListBox 的 ItemTemplate 属性设置为 DataTemplate。DataTemplate 中指定的内容变成数据对象的可视结构。以下 DataTemplate 相当简单。我们要给出的说明是:每项显示为 StackPanel 中的三个 TextBlock 元素。每个 TextBlock 元素绑定到 Task 类的一个属性。   本主题中的示例的基础数据是 CLR 对象的一个集合。如果您要绑定到 XML 数据,基本概念相同,但是语法稍微不同。例如,不是让 Path=TaskName,而是将 XPath 设置为 @TaskName(如果 TaskName 是 XML 节点的属性)。 现在 ListBox 如下所示: 第 页 共 291 页 微软WPF应用   将 DataTemplate 创建为资源 在上面的示例中,我们定义了 DataTemplate 内联。更为常见的是在资源部分中定义它,以使其成为一个可重用的对象,如下面的示例所示:   现在,您可以将 myTaskTemplate 用作资源,如下面的示例所示:   第 页 共 291 页 微软WPF应用 因为 myTaskTemplate 是资源,所以您现在可以在具有采用 DataTemplate 类型的属性的其他控件中使用它。如上所述,对于 ItemsControl 对象(例如 ListBox),它是 ItemTemplate 属性。对于 ContentControl 对象,它是 ContentTemplate 属性。 DataType 属性 DataTemplate 类具有 DataType 属性,该属性非常类似于 Style 类的 TargetType 属性。因此,在上述示例中不需要为 DataTemplate 指定 x:Key,您可以执行以下操作:   此 DataTemplate 自动应用于所有 Task 对象。请注意,在这种情况下,x:Key 是隐式设置的。因此,如果要为此 DataTemplate 分配一个 x:Key 值,则要重写隐式 x:Key 并且不会自动应用 DataTemplate。 如果要将 ContentControl 绑定到 Task 对象的集合,则 ContentControl 不会自动使用以上 DataTemplate。这是因为在 ContentControl 上绑定需要更多的信息来区分是要绑定到整个集合还是要绑定到单个对象。如果 ContentControl 要跟踪对 ItemsControl 类型的选择,您可以将 ContentControl 绑定的 Path 属性设置为“/”以表示您对当前项感兴趣。有关示例,请参见如何:绑定到集合并基于选择显示信息。否则,需要通过设置 ContentTemplate 属性显式指定 DataTemplate。 DataType 属性在您拥有不同类型数据对象的 CompositeCollection 时尤其有用。 向 DataTemplate 添加更多信息 当前,数据显示了必要的信息,但是还可以显示更多信息。让我们通过添加 Border、Grid 和一些用于描述要显示的数据的 TextBlock 来显示更多信息。 第 页 共 291 页 微软WPF应用   下面的屏幕快照使用此已修改的 DataTemplate 来显示 ListBox。   我们可以在 ListBox 上将 HorizontalContentAlignment 设置为 Stretch 来确保项的宽度占据整个空间: 第 页 共 291 页 微软WPF应用   HorizontalContentAlignment 属性设置为 Stretch 后,ListBox 现在如下所示:   将 DataTemplate 创建为资源 在上面的示例中,我们定义了 DataTemplate 内联。更为常见的是在资源部分中定义它,以使其成为一个可重用的对象,如下面的示例所示:   现在,您可以将 myTaskTemplate 用作资源,如下面的示例所示: 第 页 共 291 页 微软WPF应用   因为 myTaskTemplate 是资源,所以您现在可以在具有采用 DataTemplate 类型的属性的其他控件中使用它。如上所述,对于 ItemsControl 对象(例如 ListBox),它是 ItemTemplate 属性。对于 ContentControl 对象,它是 ContentTemplate 属性。 DataType 属性 DataTemplate 类具有 DataType 属性,该属性非常类似于 Style 类的 TargetType 属性。因此,在上述示例中不需要为 DataTemplate 指定 x:Key,您可以执行以下操作:   此 DataTemplate 自动应用于所有 Task 对象。请注意,在这种情况下,x:Key 是隐式设置的。因此,如果要为此 DataTemplate 分配一个 x:Key 值,则要重写隐式 x:Key 并且不会自动应用 DataTemplate。 如果要将 ContentControl 绑定到 Task 对象的集合,则 ContentControl 不会自动使用以上 DataTemplate。这是因为在 ContentControl 上绑定需要更多的信息来区分是要绑定到整个集合还是要绑定到单个对象。如果 ContentControl 要跟踪对 ItemsControl 类型的选择,您可以将 ContentControl 绑定的 Path 属性设置为“/”以表示您对当前项感兴趣。有关示例,请参见如何:绑定到集合并基于选择显示信息。否则,需要通过设置 ContentTemplate 属性显式指定 DataTemplate。 DataType 属性在您拥有不同类型数据对象的 CompositeCollection 时尤其有用。 向 DataTemplate 添加更多信息 当前,数据显示了必要的信息,但是还可以显示更多信息。让我们通过添加 Border、Grid 和一些用于描述要显示的数据的 TextBlock 来显示更多信息。 第 页 共 291 页 微软WPF应用   下面的屏幕快照使用此已修改的 DataTemplate 来显示 ListBox。   我们可以在 ListBox 上将 HorizontalContentAlignment 设置为 Stretch 来确保项的宽度占据整个空间: 第 页 共 291 页 微软WPF应用   HorizontalContentAlignment 属性设置为 Stretch 后,ListBox 现在如下所示:   DataTemplate 中有哪些内容? 在前面的示例中,我们使用 DataTemplate.Triggers 属性将触发器放入 DataTemplate 中。触发器的 Setter 设置 DataTemplate 中元素(Border 元素)的属性值。但是,如果您的 Setters 相关属性不是当前 DataTemplate 中元素的属性,则使用 Style(用于 ListBoxItem 类)设置属性更合适(如果您要绑定的控件是 ListBox)。例如,如果您想要在鼠标指向某一项时让您的 Trigger 对该项的 Opacity 值进行动画处理,则需要在 ListBoxItem 样式中定义触发器。 通常需要注意:DataTemplate 会应用于每个生成的 ListBoxItem(有关它实际应用的方式和场合的更多信息,请参见 ItemTemplate 页)。DataTemplate 仅与数据对象的表示和外观有关。在大多数情况下,所有其他表示方面(例如,项在被选中时的外观或 ListBox 排列项的方式)不在 DataTemplate 定义范围内。 根据数据对象的属性选择 DataTemplate 在 DataType 属性一节中,我们讨论了您可以针对不同的数据对象定义不同的数据模板。这在您拥有不同类型的 CompositeCollection 或不同类型的项集合时尤其有用。在使用 DataTrigger 来应用属性值一节中,我们演示了如果您拥有相同类型的数据对象集合,您可以创建 DataTemplate,然后根据每个数据对象的属性值使用触发器来应用更改。虽然触发器允许您应用属性值或启动动画,但是它们无法让您灵活重构数据对象的结构。在某些情况下,可能需要您为类型相同但属性不同的数据对象创建其他 DataTemplate。 第 页 共 291 页 微软WPF应用 例如,当 Task 对象的 Priority 值为 1 时,您可能需要为它指定完全不同的外观,以给予您自己一个提醒。在这种情况下,您需要创建 DataTemplate 来显示高优先级的 Task 对象。让我们将以下 DataTemplate 添加到资源部分:   请注意,此示例使用 DataTemplate.Resources 属性。DataTemplate 中的元素共享该部分中定义的资源。 若要提供逻辑以根据数据对象的 Priority 值选择要使用的 DataTemplate,需要创建 DataTemplateSelector 的子类并重写 SelectTemplate 方法。在下面的示例中,SelectTemplate 方法提供逻辑以根据 Priority 属性的值返回适当的模板。可以在封装 Window 元素的资源中找到要返回的模板。   第 页 共 291 页 微软WPF应用 然后,我们可以将 TaskListDataTemplateSelector 声明为资源:   若要使用模板选择器资源,请将其分配到 ListBox 的 ItemTemplateSelector 属性。ListBox 为基础集合中的每一项调用 TaskListDataTemplateSelector 的 SelectTemplate 方法。该调用会将数据对象作为项参数来传递。然后,将由该方法返回的 DataTemplate 应用于该数据对象。   使用模板选择器后,ListBox 现在如下所示:   这正是此示例要得到的结果。 第 页 共 291 页 微软WPF应用  对 ItemsControl 进行样式和模板处理 即使 ItemsControl 不是 DataTemplate 所用于的唯一控件类型,将 ItemsControl 绑定到集合仍然很常见。在 DataTemplate 中有哪些内容一节中,我们讨论了您的 DataTemplate 定义应当仅与数据表示相关。为了明确何时不适合使用 DataTemplate,有必要了解 ItemsControl 提供的不同样式和模板属性。下面的示例旨在演示这些属性中每一个属性的功能。本示例中的 ItemsControl 绑定到与前面示例中的 Tasks 集合。为便于演示,本示例中的样式和模板都进行了内联声明。 第 页 共 291 页 微软WPF应用   第 页 共 291 页 微软WPF应用 下面是该示例在呈现时的屏幕快照:   请注意,您可以使用 ItemTemplateSelector,而不是 ItemTemplate。请参考上一节的示例。同样,可以选择使用 ItemContainerStyleSelector,而不是 ItemContainerStyle。 这里未显示 ItemsControl 的其他两个与样式相关的属性,它们是 GroupStyle 和 GroupStyleSelector。 对分层数据的支持 到目前为止,我们仅讨论如何绑定和显示单个集合。某些时候,您要绑定的集合包含其他集合。HierarchicalDataTemplate 类专用于 HeaderedItemsControl 类型以显示这样的数据。在下面的示例中,ListLeagueList 是 League 对象的列表。每个 League 对象都有一个 Name 和 Division 对象的集合。每个 Division 都有一个 Name 和 Team 对象的集合,并且每个 Team 对象都有一个 Name。   第 页 共 291 页 微软WPF应用 该示例演示通过使用 HierarchicalDataTemplate,您可以轻松地显示包含其他列表的列表数据。下面是该示例的一个屏幕快照。   4. WPF数据绑定之绑定声明 本主题讨论声明绑定的不同方法。 先决条件 在阅读本主题之前,应当先熟悉标记扩展的概念和使用,这一点非常重要。 本主题不介绍数据绑定的概念。 在 XAML 中声明绑定 本节讨论如何在可扩展应用程序标记语言 (XAML) 中声明绑定。 标记扩展使用 Binding 是标记扩展。当您使用绑定扩展来声明绑定时,声明包含一系列子句,这些子句跟在 Binding 关键字后面,并由逗号 (,) 分隔。绑定声明中的子句可以按任意顺序排列,因此有许多可能的组合。子句是名称=值 对,其中名称 是 Binding 属性,值 是您要为该属性设置的值。 当在标记中创建绑定声明字符串时,必须将它们附加到目标对象的特定依赖项属性。下面的示例演示如何通过使用绑定扩展并指定 Source、Path 和 UpdateSourceTrigger 属性来绑定 TextBox.Text 属性。   第 页 共 291 页 微软WPF应用 您可以通过这种方法来指定 Binding 类的大部分属性。 对象元素语法 对象元素语法是创建绑定声明的另一种方法。在大多数情况下,使用标记扩展或对象元素语法没有特定的优势。但是,在标记扩展不支持您的方案的情况下,例如,当您的属性值是不存在类型转换的非字符串类型时,您将需要使用对象元素语法。 下面是对象元素语法和标记扩展使用的一个示例:   此示例通过使用扩展语法声明绑定来绑定 Foreground 属性。Text 属性的绑定声明使用对象元素语法。 MultiBinding 和 PriorityBinding MultiBinding 和 PriorityBinding 不支持 XAML 扩展语法。因此,如果您要在 XAML 中声明 MultiBinding 或 PriorityBinding,则必须使用对象元素语法。 在代码中创建绑定 指定绑定的另一种方法是在代码中直接为 Binding 对象设置属性。下面的示例演示如何在代码中创建 Binding 对象并指定属性:   第 页 共 291 页 微软WPF应用 如果您要绑定的对象是 FrameworkElement 或 FrameworkContentElement,则您可以直接对该对象调用 SetBinding 方法,而不是使用 BindingOperations..::.SetBinding。 绑定路径语法 使用 Path 属性可以指定您要绑定到的源值: 在最简单的情况下,Path 属性值是要用于绑定的源对象的属性名,如 Path=PropertyName。 在 C# 中可以通过类似语法指定属性的子属性。例如,子句 Path=ShoppingCart.Order 设置与对象或属性 ShoppingCart 的 Order 子属性的绑定。 若要绑定到附加属性,应在附加属性周围放置圆括号。例如,若要绑定到附加属性 DockPanel..::.Dock,则语法是 Path=(DockPanel.Dock)。 可以在要应用索引器的属性名后面的方括号内指定属性的索引器。例如,子句 Path=ShoppingCart[0] 将绑定设置为与属性的内部索引处理文本字符串“0”的方式对应的索引。还支持嵌套的索引器。 可以在 Path 子句中混合索引器和子属性;例如,Path=ShoppingCart.ShippingInfo[MailingAddress,Street]. 在索引器内部,您可以有多个由逗号 (,) 分隔的索引器参数。可以使用圆括号指定每个参数的类型。例如,您可以有 Path="[(sys:Int32)42,(sys:Int32)24]",其中 sys 映射到 System 命名空间。 转义机制 在索引器 ([ ]) 内部,插入符号 (^) 用于对下一个字符进行转义。 如果您在 XAML 中设置 Path,则还需要使用 XML 实体对 XML 分析程序专用的某些字符进行转义: 使用 & 对字符“&”进行转义。 使用 > 对结束标记“>”进行转义。 此外,如果您使用标记扩展语法描述属性中的整个绑定,则需要使用反斜杠 \ 对 WPF 标记扩展分析程序专用的字符进行转义: 反斜杠 \ 本身是转义字符。 等号 (=) 将属性名与属性值隔开。 逗号 (,) 用于分隔属性。 第 页 共 291 页 微软WPF应用 右大括号 (}) 是标记扩展的结尾。 默认行为 如果未在声明中指定默认行为,则默认行为如下。 创建一个尝试在绑定源值与绑定目标值之间执行类型转换的默认转换器。如果无法进行转换,则默认转换器会返回 null。 如果您不设置 ConverterCulture,则绑定引擎会使用绑定目标对象的 Language 属性。在 XAML 中,此属性默认为“en-US”,或者从页面的根元素(或任何元素)继承值(如果已显式设置了一个值)。 只要绑定已有数据上下文(例如,来自父元素的继承数据上下文),并且该上下文所返回的项或集合适合于绑定,而不需要进一步的路径修改,则绑定声明可以不必有任何子句:{Binding}。在绑定作用于集合的情况下,这通常是为数据样式指定绑定的方式。 默认 Mode 可能是单向,也可能是双向,具体取决于所绑定的依赖项属性。您始终可以显式声明绑定模式,以确保绑定具有所需的行为。通常,用户可编辑的控件属性(如 TextBox..::.Text 和 RangeBase..::.Value)默认为双向绑定,而其他大多数属性默认为单向绑定。 默认 UpdateSourceTrigger 值可能是 PropertyChanged,也可能是 LostFocus,具体也取决于所绑定的依赖项属性。多数依赖项属性的默认值都为 PropertyChanged,而 TextBox..::.Text 属性的默认值为 LostFocus。 5. 实例一:绑定到ADO.NET数据源 本示例演示如何将 Windows Presentation Foundation (WPF) ListBox 控件绑定到 ADO.NET DataSet。 示例 在本示例中,OleDbConnection 对象用于连接到数据源,该数据源是在连接字符串中指定的 Access MDB 文件。建立连接后,会创建一个 OleDbDataAdpater 对象。OleDbDataAdpater 对象执行一个 select 结构化查询语言 (SQL) 语句,以便从数据库中检索记录集。通过调用 OleDbDataAdapter 的 Fill 方法,此 SQL 命令的结果存储在 DataSet 的 DataTable 中。本示例中,DataTable 命名为 BookTable。然后,此示例将 ListBox 的 DataContext 属性设置为 DataSet 对象。 第 页 共 291 页 微软WPF应用   然后,我们将 ListBox 的 ItemsSource 属性绑定到 DataSet 的 BookTable:   BookItemTemplate 是定义数据显示形式的 DataTemplate:   IntColorConverter 将 int 转换为颜色。利用此转换器,如果 NumPages 的值小于 350,则第三个 TextBlock 的 Background 颜色为绿色,否则为红色。此处未显示此转换器的实现。 6. 实例二:绑定到LINQ查询的结果 本示例演示如何运行 LINQ 查询然后绑定到查询结果。 第 页 共 291 页 微软WPF应用 示例 下面的示例创建两个列表框。第一个列表框包含三个列表项。   在第一个列表框中选择一项会激发下面的事件处理程序。在本示例中,Tasks 是 Task 对象的集合。Task 类具有名为 Priority 的属性。此事件处理程序运行一个 LINQ 查询,该查询返回具有选定优先级值的 Task 对象的集合,然后将其设置为 DataContext:   第二个列表框绑定到该集合,因为该列表框的 ItemsSource 值设置为 {Binding}。因此,该列表框显示返回的集合(基于 myTaskTemplate DataTemplate)。 第 页 共 291 页 微软WPF应用 WPF图形和多媒体开发 1. WPF 图形动画和媒体概述 本主题介绍 Windows Presentation Foundation (WPF) 的图形、动画和媒体功能,使用这些功能,可以向应用程序添加图形、过渡效果、声音和视频。 WPF 提供高级绘图和动画功能,以前只能从专用库(具体来说,就是 Microsoft Windows 图形设备接口 (GDI) 和 Microsoft Windows GDI+)使用这些功能。现在,WPF 提供对多媒体、向量图形、动画和内容撰写的集成支持,使得开发人员可以轻松地生成悦目的用户界面和内容。使用 Microsoft Visual Studio .NET 或者甚至文本编辑器(如 Microsoft 记事本),可以创建矢量图形或复杂的动画并将媒体集成到应用程序中。 WPF 在图形和多媒体方面的新增功能 WPF 向 Windows 开发人员引进了新的图形功能,这些功能具有如下优点: 与分辨率和设备无关的图形。WPF 图形系统使用与设备无关的单元来支持分辨率和设备独立性。每个与设备无关的像素都会随系统上的每英寸点数设置自动缩放。 更高的精度。WPF 坐标系使用双精度值,而不使用浮点值。转换值和不透明度值也是使用双精度值来表示。WPF 还支持更广泛的颜色域 (scRGB),并为管理来自不同颜色空间的输入提供了集成的支持。 高级图形和动画支持。WPF 通过为您管理场景图简化了图形编程;您无需再担心场景处理、呈现循环和双线性内插算法。WPF 提供了命中测试支持、集成的动画系统和全面的字母合成支持。 硬件加速。WPF 图形系统旨在利用图形硬件来最小化 CPU 使用率。 二维形状 WPF 提供了一个库,包含用矢量绘制的常用 二维 形状,如下图中演示的矩形和椭圆。 第 页 共 291 页 微软WPF应用 这些内部的 WPF 形状不仅仅是形状:它们是可编程的元素,能够实现可通过最常见的控件(包括键盘输入和鼠标输入)实现的许多功能。   下面的插图显示前面 XAML 标记和代码隐藏的输出。   二维几何图形 当 WPF 提供的 二维 形状不足时,可以使用 WPF 中的几何图形和路径支持来创建自己的形状。下面的插图显示如何使用几何图形来创建形状、如何将几何图形用作绘图画笔以及如何使用几何图形来剪裁其他 WPF 元素。 第 页 共 291 页 微软WPF应用   二维效果 WPF 提供了一个包含 二维 类的库,可用来创建各种效果。使用 WPF 的 二维 呈现功能,可以绘制具有渐变、位图、绘图和视频的 UI 元素,并借助于旋转、缩放和扭曲功能来操作这些元素。下图举例说明通过 WPF 画笔可获得的多种效果。   三维呈现 WPF 提供了一组三维呈现功能,这些功能可与 WPF 中的二维图形支持功能集成,以便您创建更加令人惊喜的布局、UI 和数据可视化效果。在色谱的一端,WPF 允许您将 二维 图像呈现到 三维 形状的一个图面上,如下图中所示。 第 页 共 291 页 微软WPF应用   动画 使用动画,可以使控件和元素变大、晃动、旋转和淡化,还可以产生有趣的页面过渡和更多效果。由于 WPF 允许您对大多数属性进行动画处理,因此,您不但可以对大多数 WPF 对象进行动画处理,而且还可以使用 WPF 来对您创建的自定义对象进行动画处理。   媒体 图像、视频和音频是用来传达信息和用户体验的富媒体方法。 第 页 共 291 页 微软WPF应用 图像 图像(包括图标、背景甚至动画的一部分)是大多数应用程序的核心部分。由于您经常需要使用图像,因此 WPF 提供了以各种方式处理图像的功能。下图只说明了其中的一种方法。   视频和音频 WPF 的一个核心图形功能就是为处理多媒体(包括视频和音频)提供本机支持。下面的示例说明如何在应用程序中插入媒体播放器。   既能够播放视频又能够播放音频,而且具有很好的可扩展性,可以用来方便地创建自定义的 UI。 2. WPF的图形呈现 本主题概述 WPF 可视化层。本主题重点讲述 WPF 模型中呈现支持的 Visual 类的角色。 Visual 对象的角色 Visual 类是每个 FrameworkElement 对象所派生自的基本抽象。该类还充当在 WPF 中编写新控件的入口点。在 Win32 应用程序模型中,该类在许多方面可以被视为窗口句柄 (HWND)。 Visual 对象是一个核心 WPF 对象,它的主要角色是提供呈现支持。用户界面控件(如 Button 和 TextBox)派生自 Visual 类,并使用该类来保持它们所呈现的数据。Visual 对象为下列功能提供支持: 输出显示:呈现 Visual 对象的持久的序列化绘图内容。 转换:针对 Visual 对象执行转换。 剪辑:为 Visual 对象提供剪辑区域支持。 命中测试:确定 Visual 对象的边界内是否包含坐标或几何形状。 第 页 共 291 页 微软WPF应用 边界框计算:确定 Visual 对象的边框。 但是,Visual 对象不包括对非呈现功能的支持,如: ◆事件处理 ◆布局 ◆样式 ◆数据绑定 ◆全球化 Visual 作为子类必须派生自的公共抽象类进行公开。下图显示了 WPF 中所公开的可视化对象的层次结构。 Visual 类的层次结构   DrawingVisual 类 DrawingVisual 是一个用于呈现形状、图像或文本的轻量绘图类。此类之所以被视为轻量,是因为它不提供布局或事件处理功能,从而能够改善运行时性能。因此,绘图最适于背景和剪贴画。DrawingVisual 可用于创建自定义可视化对象。 Viewport3DVisual 类 Viewport3DVisual 在二维 Visual 和 Visual3D 对象之间起到桥梁作用。Visual3D 类是所有三维可视化元素的基类。Viewport3DVisual 要求您定义一个 Camera 值和一个 Viewport 值。可以借助照相机来查看场景。投影映射到二维图面的区域称作视区。 ContainerVisual 类 ContainerVisual 类用作 Visual 对象集的容器。DrawingVisual 类派生自 ContainerVisual 类,这允许它包含可视化对象的集合。 可视化对象中的绘图内容 Visual 对象将它的呈现数据另存为向量图形指令列表。指令列表中的每一项都以序列化格式表示一组低级别的图形数据及其相关资源。共有四种不同类型的呈现数据可以包含绘图内容。 第 页 共 291 页 微软WPF应用   通过 DrawingContext,您可用可视化内容填充 Visual。当您使用 DrawingContext 对象的绘图命令时,实际上是存储一组日后将由图形系统使用的呈现数据,而不是实时绘制到屏幕上。 当您创建 WPF 控件(如 Button)时,该控件会为绘图对象本身隐式生成呈现数据。例如,设置 Button 的 Content 属性会导致该控件存储标志符号的呈现表示。 Visual 将其内容描述为一个或多个包含在 DrawingGroup 中的 Drawing 对象。DrawingGroup 还描述不透明蒙板、转换、位图效果和应用于其内容的其他操作。呈现内容时,DrawingGroup 操作按如下顺序应用:OpacityMask、Opacity、BitmapEffect、ClipGeometry、GuidelineSet 和 Transform。 下图显示了在呈现过程中 DrawingGroup 操作的应用顺序。   第 页 共 291 页 微软WPF应用 在可视化层绘制内容 绝不能直接实例化 DrawingContext;但可以通过某些方法(例如 DrawingGroup..::.Open 和 DrawingVisual..::.RenderOpen)获取绘图上下文。下面的示例从 DrawingVisual 中检索 DrawingContext 并将其用于绘制矩形。   在可视化层枚举绘图内容 此外,Drawing 对象还可提供用来枚举 Visual 内容的对象模型。 说明: 您在枚举可视化层的内容时,就是相当于在检索 Drawing 对象,而不是以向量图形指令列表形式检索呈现数据的基础表示。 下面的示例使用 GetDrawing 方法来检索 Visual 的 DrawingGroup 值并枚举该值。 第 页 共 291 页 微软WPF应用   如何使用可视化对象来生成控件 WPF 中的许多对象都由其他可视化对象组成,这意味着它们可以包含子代对象的各种层次结构。WPF 中的许多用户界面元素(如控件)都由多个表示不同类型呈现元素的可视化对象组成。例如,Button 控件可以包含许多其他对象,其中包括 ClassicBorderDecorator、ContentPresenter 和 TextBlock。 下面的代码显示的是在标记中定义的 Button 控件。   如果您要枚举包含默认 Button 控件的可视化对象,则将发现如下所示的可视化对象层次结构: 可视化树层次结构的关系图 第 页 共 291 页 微软WPF应用   Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。ClassicBorderDecorator 元素负责为 Button 绘制边框和背景。ContentPresenter 元素负责显示 Button 的内容。在本例中,由于您要显示文本,因此 ContentPresenter 元素中包含一个 TextBlock 元素。Button 控件使用 ContentPresenter,这意味着该控件的内容可以由其他元素(如 Image)或几何形状(如 EllipseGeometry)来表示。 控件模板 将控件扩展为控件层次结构的关键在于 ControlTemplate。控件模板为控件指定默认的可视化层次结构。当您显式引用某个控件时,会隐式引用它的可视化层次结构。您可以重写控件模板的默认值,以便为控件创建自定义的可视化外观。例如,您可以修改 Button 控件的背景颜色值,以便它使用线性渐变颜色值,而不使用纯色值。 用户界面元素(如 Button 控件)包含几个向量图形指令列表,这些列表描述控件的全部呈现定义。下面的代码显示的是在标记中定义的 Button 控件。   如果您要枚举包含 Button 控件的可视化对象和向量图形指令列表,则将发现如下所示的可视化对象层次结构: 可视化树和呈现数据的关系图 第 页 共 291 页 微软WPF应用   Button 控件包含一个 ClassicBorderDecorator 元素,该元素又包含一个 ContentPresenter 元素。ClassicBorderDecorator 元素负责绘制所有构成按钮边框和背景的离散图形元素。ContentPresenter 元素负责显示 Button 的内容。在本例中,由于您要显示图像,因此 ContentPresenter 元素中包含一个 Image 元素。 对于可视化对象和向量图形指令列表的层次结构,需要注意多个事项: 该层次结构中的排序表示绘图信息的呈现顺序。从可视化元素的根,按照从左到右、从上到下的顺序遍历子元素。如果某个元素有可视化子元素,则会先遍历该元素的子元素,然后再遍历该元素的同级。 层次结构中的非叶节点元素(如 ContentPresenter)用于包含子元素,它们并不包含指令列表。 如果可视化元素既包含向量图形指令列表又包含可视化子级,则会先呈现父级可视化元素中的指令列表,然后再呈现任何可视化子对象中的绘图。 向量图形指令列表中的项按照从左到右的顺序呈现。 可视化树 可视化树中包含某个应用程序的用户界面所使用的所有可视化元素。由于可视化元素中包含持久的绘图信息,因此您可以将可视化树视为场景图,其中包含将输出写入显示设备所必需的全部呈现信息。该树汇集了由该应用程序在代码或标记中直接创建的所有可视化元素。该可视化树还包含由元素(如控件和数据对象)的模板扩展功能创建的所有可视化元素。 第 页 共 291 页 微软WPF应用 下面的代码显示的是在标记中定义的 StackPanel 元素。 如果您要枚举包含标记示例中 StackPanel 元素的可视化对象,将发现如下所示可视化对象的层次结构: 可视化树层次结构的关系图     呈现顺序 通过可视化树,可以确定 WPF 可视化对象和绘图对象的呈现顺序。将从位于可视化树中最顶层节点中的可视化元素根开始遍历,然后将按照从左到右的顺序遍历可视化元素根的子级。如果某个可视化元素有子级,则将先遍历该可视化元素的子级,然后再遍历其同级。这意味着子可视化元素的内容先于该可视化元素本身的内容而呈现。 可视化树呈现顺序的关系图     可视化元素根 可视化元素根是可视化树层次结构中最顶层的元素。在大多数应用程序中,可视化元素根的基类是 Window 或 NavigationWindow。但是,如果您在 Win32 应用程序中承载可视化对象,则可视化元素根将是在 Win32 窗口中承载的最顶层的可视化元素。 第 页 共 291 页 微软WPF应用 与逻辑树的关系 WPF 中的逻辑树表示应用程序在运行时的元素。尽管您不直接操作该树,但是该应用程序视图对于了解属性继承和事件路由非常有用。与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。在许多情况下,逻辑树密切映射到应用程序的标记定义。下面的代码显示的是在标记中定义的 DockPanel 元素。   如果您要枚举包含标记示例中 DockPanel 元素的逻辑对象,则将发现如下所示逻辑对象的层次结构: 逻辑树的关系图   可视化树和逻辑树与当前的应用程序元素集合同步,并反映对元素进行的任何添加、删除或修改。但是,这些树表示不同的应用程序视图。与可视化树不同,逻辑树不展开控件的 ContentPresenter 元素。这意味着同一组对象的逻辑树和可视化树之间没有直接的一对一对应关系。实际上,在将同一个元素用作参数的情况下,调用 LogicalTreeHelper 对象的 GetChildren 方法与调用 VisualTreeHelper 对象的 GetChild 方法会生成不同的结果。 使用 XamlPad 查看可视化树 WPF 工具 (XamlPad) 提供了一个用来查看和浏览可视化树的选项,该树与当前所定义的 XAML 内容相对应。单击菜单栏上的“显示可视化树”[Show Visual Tree]按钮可显示相应的可视化树。下面将说明如何在 XamlPad 的“可视化树资源管理器”[Visual Tree Explorer]面板中将 XAML 内容扩展为可视化树节点: XamlPad 中的“可视化树资源管理器”[Visual Tree Explorer]面板 第 页 共 291 页 微软WPF应用   请注意,Label、TextBox 和 Button 控件在 XamlPad 的“可视化树资源管理器”[Visual Tree Explorer]面板中各自显示一个可视化对象层次结构。这是由于 WPF 控件具有一个包含其可视化树的 ControlTemplate。当您显式引用某个控件时,会隐式引用它的可视化层次结构。 分析可视化性能 WPF 提供了一套性能分析工具,来帮助您分析应用程序的运行时行为,并确定可以应用的性能优化的类型。可视化探查器工具通过直接映射到应用程序的可视化树来为性能数据提供一个丰富的图形视图。在此屏幕快照中,可视化探查器的“CPU Usage”[CPU 使用率]部分使您可以清楚地了解对象对 WPF 服务(如呈现和布局)的使用情况。 可视化探查器显示输出   第 页 共 291 页 微软WPF应用 可视化对象的呈现行为 WPF 引进了几个影响可视化对象呈现行为的功能:保留的模式图形、矢量图形和与设备无关的图形。 保留的模式图形 了解即时模式和保留模式图形系统之间的区别是了解 Visual 对象角色的要点之一。基于 GDI 或 GDI+ 的标准 Win32 应用程序使用即时模式图形系统。这意味着应用程序负责重新绘制工作区中由于某项操作(如调整窗口大小)或者对象的可视化外观发生变化而失效的部分。 Win32 呈现顺序的关系图 与之相比,WPF 使用保留模式系统。这意味着具有可视化外观的应用程序对象定义一组序列化绘图数据。在定义了绘图数据之后,系统会响应所有的重新绘制请求来呈现应用程序对象。甚至在运行时,您也可以修改或创建应用程序对象,并仍旧依赖系统响应绘制请求。保留模式图形系统中有一个强大功能,那就是绘图信息总是由应用程序保持为序列化状态,但是呈现功能仍由系统负责。下面的关系图演示应用程序如何依赖 WPF 来响应绘制请求。 WPF 呈现顺序的关系图   智能重绘 使用保留模型图形的最大好处之一就是,WPF 可以高效率地优化需要在应用程序中重绘的内容。即使您有一个具有各种不透明度的复杂场景,通常也不必编写特殊用途的代码来优化重绘功能。请将智能重绘功能与 Win32 编程进行比较,在后者中,可以通过最小化更新区域中的重绘量来尽力优化应用程序。 第 页 共 291 页 微软WPF应用 向量图形 WPF 使用向量图形作为其呈现数据的格式。向量图形(包括可缩放的向量图形 (SVG)、Windows 元文件 (.wmf) 和 TrueType 字体)存储呈现数据,并以指令列表的形式传输该呈现数据,这些指令描述如何使用图形基元来重新创建图像。例如,TrueType 字体是描述一组直线、曲线和命令(而不是像素数组)的矢量字。矢量图形的主要好处之一就是能够伸缩到任何大小和分辨率。 与矢量图形不同,位图图形以图像的逐像素表示形式来存储呈现数据,而且在特定的分辨率下预先呈现。位图图形格式和矢量图形格式的主要区别之一就是对原始图像的保真度。例如,当某个源图像的大小发生变化时,位图图形系统会拉伸该图像,而向量图形系统会伸缩该图像,从而保持图像的保真度。 下图显示了源图像在放大到 3 倍时的情况。请注意,当源图像作为位图图形拉伸时会发生失真,而当源图像作为矢量图形伸缩时,则不会发生失真。 光栅图形和矢量图形之间的区别 下面的标记显示所定义的两个 Path 元素。第二个元素使用 ScaleTransform 将第一个元素的绘图指令放大到 3 倍。请注意 Path 元素中的绘图指令保持不变。 第 页 共 291 页 微软WPF应用   关于分辨率和与设备无关的图形 可通过以下两个系统因子来确定屏幕上的文本大小和图形大小:分辨率和 DPI。分辨率描述出现在屏幕上的像素数量。由于分辨率变大,因此像素会变小,从而导致所显示的图形和文本会变小。在将显示器的分辨率从 1024 x 768 更改为 1600 x 1200 时,显示器上所显示的图形会小得多。 另一个系统设置 (DPI) 以像素数来描述屏幕英寸的大小。大多数 Windows 系统的 DPI 都为 96,这意味着一屏幕英寸等于 96 个像素。增加 DPI 设置会使屏幕英寸变大,减小 DPI 会使屏幕英寸变小。这意味着屏幕英寸与实际的英寸不相等;在多数系统上,二者很有可能不相等。当您增加 DPI 时,屏幕英寸会变大,因此支持 DPI 的图形和文本也会变大。增加 DPI 可能会增强文本的可读性,在高分辨率下尤其如此。 并非所有的应用程序都支持 DPI:一些应用程序将硬件像素作为其主要计量单位;更改系统 DPI 不会对这些应用程序产生任何影响。许多其他应用程序都使用支持 DPI 的单位来描述字号,使用像素来描述任何其他内容。DPI 太小或太大都可能会导致这些应用程序出现布局问题,因为应用程序的文本会随着系统的 DPI 设置而伸缩,而应用程序的 UI 却不会出现此类问题。对于使用 WPF 开发的应用程序,此问题已经消除。 WPF 支持通过将与设备无关的像素(而不是硬件像素)用作其主要计量单位来自动伸缩;图像和文本会适当伸缩,而无需应用程序开发人员执行任何额外的工作。下图显示了 WPF 文本和图形在不同 DPI 设置下的显示方式的示例。 不同 DPI 设置下的图形和文本 第 页 共 291 页 微软WPF应用   VisualTreeHelper 类 VisualTreeHelper 类是一个静态帮助器类,它提供了一个要在可视化对象级别编程的低级功能,该类在非常特殊的方案(如开发高性能自定义控件)中非常有用。在大多数情况下,更高级的 WPF 框架对象(如 Canvas 和 TextBlock)提供更大的灵活性且更易于使用。 命中测试 VisualTreeHelper 类提供了当默认的命中测试支持无法满足您的需要时,针对可视化对象的命中测试方法。可以在 VisualTreeHelper 类中使用 HitTest 方法来确定几何形状或点坐标值是否位于给定对象(如控件或图形元素)的边界内。例如,您可以使用命中测试来确定鼠标在对象边框中的单击点是否落在圆形几何形状内部。您还可以选择重写对命中测试的默认实现来执行自己的自定义命中测试计算。 枚举可视化树 VisualTreeHelper 类提供了用来枚举可视化树成员的功能。若要检索父级,请调用 GetParent 方法。若要检索可视化对象的子级或直接子代,请调用 GetChild 方法。此方法返回父级在指定索引处的子 Visual。 第 页 共 291 页 微软WPF应用 下面的示例演示如何枚举一个可视化对象的所有子代,如果您对序列化可视化对象层次结构的所有呈现信息感兴趣,则可能希望使用该技术。 在大多数情况下,逻辑树更能表示 WPF 应用程序中的元素。尽管您不直接修改逻辑树,但是该应用程序视图对于了解属性继承和事件路由非常有用。与可视化树不同,逻辑树可以表示非可视化数据对象(如 ListItem)。 VisualTreeHelper 类提供用来返回可视化对象边框的方法。可以通过调用 GetContentBounds 来返回可视化对象的边框。可以通过调用 GetDescendantBounds 来返回可视化对象及其所有子代的边框。下面的代码演示如何计算可视化对象及其所有子代的边框。 3. WPF的图像处理 本主题介绍 WPF 图像处理。通过 Microsoft Windows Presentation Foundation 图像处理组件,开发人员可以显示、转换和格式化图像。 WPF 图像处理组件 WPF 图像处理使得 Microsoft Windows 内的图像处理功能得到了极大改进。以前,图像处理功能(如显示位图或在常见控件上使用图像)依赖于 Microsoft Windows 图形设备接口 (GDI) 或 Microsoft Windows GDI+ 库。这些 API 提供有基本图像处理功能,但缺少诸如支持编解码器扩展性和高保真图像支持等功能。WPF 图像处理旨在克服 GDI 和 GDI+ 的缺点,并提供一组新的 API,用以在应用程序内显示和使用图像。 有两种方式可以访问 WPF 图像处理 API:托管组件和非托管组件。非托管组件提供以下功能。 适用于新的或专用图像格式的扩展性模型。 对包括位图 (BMP)、联合图像专家组 (JPEG)、可移植网络图形 (PNG)、标记图像文件格式 (TIFF)、Microsoft Windows Media 照片、图形交换格式 (GIF) 和图标 (.ico) 在内的本机图像格式增强了性能和安全性。 第 页 共 291 页 微软WPF应用 高位深图像数据的保留最多 32 位/通道。 非破坏性图像缩放、裁切和旋转。 简化的颜色管理 支持文件内的专用元数据。 托管组件利用非托管基础结构提供图像与其他 WPF 功能(如用户界面 (UI)、动画和图形)的无缝集成。托管组件还可以从 Windows Presentation Foundation (WPF) 图像处理编解码器扩展性模型获益,利用该模型可以实现自动识别 WPF 中的新图像格式。 大部分托管的 WPF 图像处理 API 驻留在 System.Windows.Media.Imaging 命名空间中,不过,几个重要的类型(如 ImageBrush 和 ImageDrawing)都驻留在 System.Windows.Media 命名空间,Image 驻留在 System.Windows.Controls 命名空间。 本主题提供有关托管组件的其他信息。 WPF 图像格式 编解码器用于对特定媒体格式解码或编码。WPF 图像处理包括一个适用于 BMP、JPEG、PNG、TIFF、Windows Media 照片、GIF 和 ICON 图像格式的编解码器。利用上述每个编解码器,应用程序可以对其各自的图像格式进行解码(ICON 除外)和编码。 BitmapSource 是一个重要的类,用于对图像进行解码和编码。它是 WPF 图像处理管线的基本构造块,表示具有特定大小和分辨率的单个不变的像素集。BitmapSource 可以是多个帧图像中的单个帧,或者可以是在 BitmapSource 上执行转换的结果。它是 WPF 图像处理中使用的许多主要类(如 BitmapFrame)的父级。 BitmapFrame 用于存储图像格式的实际位图数据。许多图像格式仅支持单一 BitmapFrame,不过 GIF 和 TIFF 等格式的图像支持每个图像有多个帧。帧由解码器用作输入数据,并传递到编码器以创建图像文件。 下面的示例演示如何从 BitmapSource 创建一个 BitmapFrame 并将其添加到 TIFF 图像。   第 页 共 291 页 微软WPF应用   图像格式解码 图像解码是指将某种图像格式转换为可以由系统使用的图像数据。然后,此图像数据可以用于显示、处理或编码为其他格式。解码器的选择是基于图像格式做出的。编解码器的选择是自动做出的,除非指定了特定的解码器。在 WPF 中显示图像小节中的示例演示了自动解码。使用非托管 WPF 图像处理界面开发并向系统注册的自定义格式解码器会自动加入到解码器选择队列。这将使得自定义格式可以自动显示在 WPF 应用程序中。 下面的示例演示使用位图解码器对 BMP 格式的图像进行解码。     第 页 共 291 页 微软WPF应用 图像格式编码 图像编码是指将图像数据转换为特定图像格式的过程。然后,已编码的图像数据可以用于创建新图像文件。WPF 图像处理为上面介绍的每种图像格式提供编码器。 下面的示例演示使用编码器保存一个新创建的位图图像。       在 WPF 中显示图像 可以通过多种方式在 Windows Presentation Foundation (WPF) 应用程序中显示图像。可以使用 Image 控件显示图像、使用 ImageBrush 在可视图面上绘制图像或使用 ImageDrawing 绘制图像。 使用 Image 控件 Image 是一个框架元素,是在应用程序中显示图像的主要方式。在 XAML 中,有两种方式可以使用 Image:属性 (attribute) 语法或属性 (property) 语法。下面的示例演示如何使用属性 (attribute) 语法或属性 (property) 标记语法来呈现一个宽 200 像素的图像。   第 页 共 291 页 微软WPF应用 许多示例使用 BitmapImage 对象引用图像文件。BitmapImage 是一个专用的 BitmapSource,已经优化为用于可扩展应用程序标记语言 (XAML) 加载,是一种将图像显示为 Image 控件的 Source 的简便方式。 下面的示例演示如何使用代码呈现宽 200 像素的图像。 说明: BitmapImage 实现 ISupportInitialize 接口,以对多个属性的初始化进行优化。只能在对象初始化过程中进行属性更改。调用 BeginInit 以表示初始化开始;调用 EndInit 以表示初始化结束。初始化一旦开始之后,将忽略所做的属性更改。   第 页 共 291 页 微软WPF应用 旋转、转换和裁切图像 通过 WPF,用户可以使用 BitmapImage 的属性或使用其他 BitmapSource 对象(如 CroppedBitmap 或 FormatConvertedBitmap)来转换图像。上述图像转换可以缩放或旋转图像、更改图像的像素格式或裁切图像。 可以使用 BitmapImage 的 Rotation 属性来执行图像旋转。旋转只能以 90 度的增量来进行。在下面的示例中,图像旋转了 90 度。     第 页 共 291 页 微软WPF应用     可以使用 FormatConvertedBitmap 将图像转换为不同的像素格式,如灰度。在下面的示例中,图像转换为 Gray4。   第 页 共 291 页 微软WPF应用     旋转、转换和裁切图像 通过 WPF,用户可以使用 BitmapImage 的属性或使用其他 BitmapSource 对象(如 CroppedBitmap 或 FormatConvertedBitmap)来转换图像。上述图像转换可以缩放或旋转图像、更改图像的像素格式或裁切图像。 可以使用 BitmapImage 的 Rotation 属性来执行图像旋转。旋转只能以 90 度的增量来进行。在下面的示例中,图像旋转了 90 度。     第 页 共 291 页 微软WPF应用     可以使用 FormatConvertedBitmap 将图像转换为不同的像素格式,如灰度。在下面的示例中,图像转换为 Gray4。   第 页 共 291 页 微软WPF应用     若要裁切图像,可以使用 Image 或 CroppedBitmap 的 Clip 属性。通常情况下,如果您只想调整图像的一部分,则应使用 Clip。如果需要编码和保存裁切过的图像,应使用 CroppedBitmap。下面的示例使用 EllipseGeometry 和 Clip 属性来裁切图像。     第 页 共 291 页 微软WPF应用   拉伸图像 Stretch 属性控制如何拉伸一个图像以使其填充容器。Stretch 属性接受以下值(是由 Stretch 枚举定义的): None:不会拉伸图像以填充输出区域。如果图像比输出区域大,则图像将绘制到输出区域,而无法容纳的内容将被剪裁掉。 Fill:会拉伸图像以适应输出区域。由于图像的高度和宽度是独立进行缩放的,因此图像的原始长宽比可能不会保留。也就是说,为了完全填充输出容器,图像可能会扭曲。 Uniform:图像进行缩放,以便其完全适应输出区域。图像的长宽比会保留。 UniformToFill:图像会进行缩放,以便在保留图像原始长宽比的同时完全填充输出区域。 下面的示例将每个可用的 Stretch 枚举应用于 Image。 下面的图像显示示例的输出,演示了不同的 Stretch 设置在应用到图像时的效果。 不同的拉伸设置   第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用   绘制图像 您还可以利用 Brush 进行绘制,从而在应用程序中显示图像。利用画笔,您可以利用任意内容(从简单的纯色到复杂的图案和图像集)绘制 UI 对象。若要绘制图像,请使用 ImageBrush。ImageBrush 是一种 TileBrush 类型,用于将其内容定义为位图图像。ImageBrush 显示由其 ImageSource 属性指定的单个图像。您可以控制图像的拉伸、对齐和平铺方式,从而可以防止失真并生成图案和其他效果。下图显示了利用 ImageBrush 可以达到的几种效果。 图像的画笔可以填充形状、控件、文本等   下面的示例演示如何使用 ImageBrush 绘制带图像的按钮的背景。   图像元数据 某些图像文件包含用于描述文件的内容或特征的元数据。例如,大多数数码相机创建的图像中包含用于捕获该图像的照相机的品牌和型号的元数据。每个图像格式处理元数据的方式不同,但 WPF 图像处理提供一种统一的方式来为每个支持的图像格式存储和检索元数据。 对元数据的访问是通过 BitmapSource 对象的 Metadata 属性来进行的。Metadata 返回一个 BitmapMetadata 对象,其中包含该图像所包含的所有元数据。此数据可以位于一个元数据架构中或位于不同方案的组合中。WPF 图像处理 支持以下图像元数据架构:可交换图像文件 (Exif)、tEXt(PNG 文本数据)、图像文件目录 (IFD)、国际新闻通信委员会 (IPTC) 和可扩展元数据平台 (XMP)。 第 页 共 291 页 微软WPF应用 为了简化读取元数据的过程,BitmapMetadata 提供易于访问的若干命名属性(如 Author、Title 和 CameraModel)。许多命名属性还可以用于编写元数据。元数据查询读取器提供对读取元数据的其他支持。GetQuery 方法用于检索元数据查询读取器,这是通过提供字符串查询(如 "/app1/exif/")来实现的。在下面的示例中,GetQuery 用于获取存储在 "/Text/Description" 位置中的文本。       若要编写元数据,会使用到元数据查询编写器。SetQuery 获取查询编写器并设置需要的值。在下面的示例中,SetQuery 用于编写存储在 "/Text/Description" 位置中的文本。     第 页 共 291 页 微软WPF应用   编解码器扩展性 WPF 图像处理的核心功能是用于新图像编解码器的扩展性模型。通过这些非托管的接口,编解码器开发人员可以将编解码器与 WPF 集成,这样 WPF 应用程序就可以自动使用新的图像格式。 说明: 编解码器必须进行数字签名,以便于系统识别。 4. WPF的三维图形应用 本主题概述 Windows Presentation Foundation (WPF) 图形系统中的三维功能。通过 WPF 三维实现,开发人员可使用与该平台所提供给二维图形的相同的功能,对标记和过程代码中的三维图形进行绘制、转换和动画处理。 开发人员可以合并二维和三维图形来创建丰富的控件,提供复杂的数据图解,或者增强用户对应用程序界面的体验。WPF 支持三维的设计宗旨不是提供功能齐全的游戏开发平台。 二维容器中的三维 WPF 中的三维图形内容封装在 Viewport3D 元素中,该元素可以参与二维元素结构。与 WPF 中的许多其他内容一样,图形系统将 Viewport3D 视为二维可视化元素。Viewport3D 充当三维场景中的一个窗口(视区)。更准确地说,它是三维场景所投影到的图面。 在传统的二维应用程序中,当您需要使用 Grid 或 Canvas 之类的另一个容器元素时,可以使用 Viewport3D。 尽管您可以将 Viewport3D 与同一个场景图中的其他二维绘图对象结合使用,但是您不能在 Viewport3D 内部渗透二维和三维对象。 本主题重点讲述如何在 Viewport3D 内部绘制三维图形。 三维坐标空间 二维图形的 WPF 坐标系将原点定位在呈现区域(通常是屏幕)的左上角。在二维系统中,x 轴上的正值朝右,y 轴上的正值朝下。 但是,在三维坐标系中,原点位于呈现区域的中心,x 轴上的正值朝右,但是 y 轴上的正值朝上,z 轴上的正值从原点向外朝向观察者。 传统的二维和三维坐标系表示形式 第 页 共 291 页 微软WPF应用    由这些轴定义的空间是三维对象在 WPF 中的固定参考框架。当您在该空间中生成模型并创建光源和照相机以查看这些模型时,一定要在向每个模型应用变换时,将固定参考框架或“全局空间”与您为该模型创建的局部参考框架区分开。另请记住,根据光源和照相机设置,全局空间中的对象可能会看上去完全不同或者根本不可见,但是照相机的位置不会改变对象在全局空间中的位置。 照相机和投影 处理二维对象的开发人员习惯于将绘图基元置于二维屏幕上。当您创建三维场景时,一定要记住您实际上是要创建三维对象的二维表示形式。由于三维场景的外观会因观察者的观察位置不同而异,因此您必须指定观察位置。可以使用 Camera 类来为三维场景指定观察位置。 了解三维场景如何在二维图面上表示的另一种方法就是将场景描述为到观察表面上的投影。使用 ProjectionCamera,可以指定不同的投影及其属性以更改观察者查看三维模型的方式。PerspectiveCamera 指定用来对场景进行透视收缩的投影。 换言之,PerspectiveCamera 提供消失点透视。 您可以指定照相机在场景坐标系中的位置、照相机的方向和视野以及用来定义场景中“向上”方向的向量。下图阐释 PerspectiveCamera 的投影。 ProjectionCamera 的 NearPlaneDistance 和 FarPlaneDistance 属性限制照相机的投影范围。由于照相机可以位于场景中的任何位置,因此照相机实际上可能会位于模型内部或者紧靠模型,这使得很难正确区分对象。使用 NearPlaneDistance,可以指定一个距离照相机的最小距离,即,在超过该距离后将不绘制对象。 相反,使用 FarPlaneDistance,可以指定一个距离照相机的距离(即,在超过该距离后将不绘制对象),从而确保因距离太远而无法识别的对象将不包括在场景中。 照相机位置   OrthographicCamera 指定三维模型到二维可视化图面上的正投影。与其他照相机一样,它指定位置、观察方向和“向上”方向。但是,与 PerspectiveCamera 不同的是,OrthographicCamera 描述了不包括透视收缩的投影。换言之,OrthographicCamera 描述了一个侧面平行的取景框,而不是侧面汇集在场景中一点的取景框。下图演示使用 PerspectiveCamera 和 OrthographicCamera 查看同一模型时的情况。 第 页 共 291 页 微软WPF应用 透视投影和正投影   下面的代码演示一些典型的照相机设置。       第 页 共 291 页 微软WPF应用 模型和网格基元 Model3D 是表示泛型三维对象的抽象基类。 若要生成三维场景,需要一些要查看的对象,而且构成场景图的对象必须派生自 Model3D。 目前,WPF 支持用 GeometryModel3D 对几何形状进行建模。此模型的 Geometry 属性采用网格基元。 若要生成模型,请首先生成一个基元或网格。 三维基元是一系列构成单个三维实体的顶点。 大多数三维系统都提供在最简单的闭合图(由三个顶点定义的三角形)上建模的基元。 由于三角形的三个点在一个平面上,因此您可以继续添加三角形,以便对网格这样较为复杂的形状建模。 WPF 三维系统目前提供 MeshGeometry3D 类,使用该类,可以指定任何几何形状;它目前不支持预定义的三维基元(如球体和立方体)。首先通过将三角形顶点的列表指定为它的 Positions 属性来创建 MeshGeometry3D。每个顶点都指定为 Point3D。 (在可扩展应用程序标记语言 (XAML) 中,将该属性指定为三个一组的数字列表,每组中的三个数字表示每个顶点的坐标)。 根据网格的几何形状,网格可能会由多个三角形组成,其中的一些三角形共用相同的角(顶点)。 若要正确地绘制网格,WPF 需要有关哪些顶点由哪些三角形共用的信息。 可以通过指定具有 TriangleIndices 属性的三角形索引列表来提供此信息。此列表指定在 Positions 列表中指定的点将按哪种顺序确定三角形。   在上面的示例中,Positions 列表指定用八个顶点来定义立方体形状的网格。 TriangleIndices 属性指定了一个包含十二个组的列表,每组由三个索引组成。 列表中的每个数字都指向与 Positions 列表的偏移量。 例如,由 Positions 列表指定的第一组(三个顶点)是 (1,1,0)、(0,1,0) 和 (0,0,0)。由 TriangleIndices 列表指定的第一组(三个索引)是 0、2 和 1,这与 Positions 列表中的第一个、第三个和第二个点相对应。 因此,构成立方体模型的第一个三角形将按照从 (1,1,0) 到 (0,1,0) 再到 (0,0,0) 的顺序组合而成,其余的十一个三角形将按照类似方式确定。 您可以通过为 Normals 和 TextureCoordinates 属性指定值来继续定义模型。 为了呈现模型的图面,图形系统需要有关曲面在任何给定三角形上的朝向信息。图形系统使用此信息来针对该模型进行照明计算:直接朝向光源的图面比偏离光源的图面显得更亮。尽管 WPF 可以使用位置坐标来确定默认的法向量,但是您还可以指定不同的法向量来近似计算曲面的外观。 第 页 共 291 页 微软WPF应用 TextureCoordinates 属性指定 Point 集合,该集合可通知图形系统如何将用来确定纹理绘制方式的坐标映射到网格的顶点。TextureCoordinates 可指定为 0 和 1(包括 0 和 1)之间的值。 如同 Normals 属性一样,图形系统可以计算默认纹理坐标,但是您可以选择设置不同的纹理坐标来控制对包括重复图案一部分的纹理的映射。 下面的示例演示如何在过程代码中创建立方体模型的一面。请注意,您可以将整个立方体绘制为单个 GeometryModel3D;此示例将该立方体的各个面绘制为一个不同的模型,以便在以后向每个面应用不同的纹理。     向模型应用 Material 为了使网格看上去像三维对象,必须向其应用纹理,以便覆盖由顶点和三角形定义的图面,从而使其可以由照相机照明和投影。在二维中,可以使用 Brush 类来向屏幕中的区域应用颜色、图案、渐变或其他可视化内容。 但是,三维对象的外观是照明模型的功能,而不只是应用于它们的颜色或图案。实际对象的图面质量不同,它们反射光的方式也会有所不同:光亮的图面与粗糙或不光滑的图面看上去不同,某些对象似乎可以吸收光,而某些对象似乎能够发光。您可以向三维对象应用与应用于二维对象的完全相同的画笔,但是您不能直接应用它们。 第 页 共 291 页 微软WPF应用 WPF 使用 Material 抽象类来定义模型图面的特征。Material 的具体子类用来确定模型图面的某些外观特征,每个子类还提供一个可以向其传递 SolidColorBrush、TileBrush 或 VisualBrush 的 Brush 属性。 DiffuseMaterial 指定将向模型应用画笔,就好像模型是使用漫射光来照亮的一样。使用 DiffuseMaterial 与直接针对二维模型使用画笔非常相似;模型表面不反射光,就好像是自发光一样。 SpecularMaterial 指定将向模型应用画笔,就好像模型的表面坚硬或者光亮,能够反射光线一样。可以通过为 SpecularPower 属性指定一个值来设置系统将为纹理的反射特质(或“发光”)建议的度数。 使用 EmissiveMaterial 可以指定将应用纹理,就好像模型所发出的光与画笔的颜色相同。这不会使模型成为光源;但是,它参与阴影设置的方式将不同于用 DiffuseMaterial 或 SpecularMaterial 设置纹理时的情况。 为进一步提高性能,可以从场景中精选 GeometryModel3D 的背面(由于它们相对于照相机位于模型的背面,因此您将看不到这些面)。 若要指定要应用于模型(如飞机)背面的 Material,请设置模型的 BackMaterial 属性。 为了实现某些图面质量(如发光或发射效果),您可能希望向模型连续应用几个不同的画笔。您可以使用 MaterialGroup 类来应用和重用多个 Material。MaterialGroup 的子级在多个呈现过程中按照从头到尾的顺序来应用。 下面的代码示例演示如何将纯色和绘图以画笔形式应用于三维模型。     第 页 共 291 页 微软WPF应用   照亮场景 与实际的光一样,三维图形中的光能够使图面可见。更确切地说,光确定了场景的哪个部分将包括在投影中。WPF 中的光对象创建了各种光和阴影效果,而且是按照各种实际光的行为建模的。您必须至少在场景中包括一个光,否则模型将不可见。 下面的光派生自基类 Light: AmbientLight:它所提供的环境光以一致的方式照亮所有的对象,而与对象的位置或方向无关。 DirectionalLight:像远处的光源那样照亮。 将方向光的 Direction 指定为 Vector3D,但是没有为方向光指定位置。 PointLight:像近处的光源那样照亮。PointLight 具有一个位置并从该位置投射光。场景中的对象是根据对象相对于光源的位置和距离而照亮的。PointLightBase 公开了一个 Range 属性,该属性确定一个距离,在超过该距离后模型将无法由光源照亮。PointLight 还公开了多个衰减属性,这些属性确定光源的亮度如何随距离的增加而减小。您可以为光源的衰减指定恒定、线性或二次内插算法。 SpotLight:继承自 PointLight。Spotlight 的照亮方式与 PointLight 类似,但是它既具有位置又具有方向。它们在 InnerConeAngle 和 OuterConeAngle 属性所设置的锥形区域(以度为单位指定)中投射光。 光源是 Model3D 对象,因此您可以转换光源对象并对光源属性(包括位置、颜色、方向和范围)进行动画处理。         变换模型 当您创建模型时,它们在场景中具有特定的位置。为了在场景中移动、旋转这些模型或者更改这些模型的大小而更改用来定义模型本身的顶点是不切实际的。 相反,正如在二维中一样,您可以向模型应用转换。 第 页 共 291 页 微软WPF应用 每个模型对象都有一个可用来对模型进行移动、重定向或调整大小的 Transform 属性。 当您应用转换时,实际上是按照由转换功能指定的向量或值(以适用者为准)来有效地偏移模型的所有点。换言之,您已经变换了在其中定义模型的坐标空间(“模型空间”),但是,您尚未更改在整个场景的坐标系(“全局空间”)中构成模型几何形状的值。 对模型进行动画处理 WPF 三维实现与二维图形参与同一个计时和动画系统。换言之,若要对三维场景进行动画处理,需要对其模型的属性进行动画处理。 可以直接对基元的属性进行动画处理,但是通常很容易对用来更改模型位置或外观的变换进行动画处理。 由于可以向 Model3DGroup 对象及其各个模型应用转换,因此可以向 Model3DGroup 的子级应用一组动画,向一组子对象应用另一组动画。 还可以通过对场景的照明属性进行动画处理来实现各种可视化效果。 最后,您可以选择通过对照相机的位置或视野进行动画处理来对投影本身进行动画处理。 若要对 WPF 中的对象进行动画处理,可以创建时间线、定义动画(实际上是随着时间的推移而更改某个属性值)并指定要向其应用动画的属性。 由于三维场景中的所有对象都是 Viewport3D 的子级,因此要应用于场景的任何动画所面向的属性都是 Viewport3D 的属性。 假设您希望实现模型看上去是在原地摇摆的效果, 您可以选择向模型应用 RotateTransform3D,并对模型从一个向量旋转到另一个向量时所围绕的轴进行动画处理。下面的代码示例演示如何将 Vector3DAnimation 应用于该转换的 Rotation3D 的 Axis 属性,并假设 RotateTransform3D 是应用于具有 TransformGroup 的模型的几个转换之一。         向窗口中添加三维内容 若要呈现场景,请向 Model3DGroup 中添加模型和光源,然后将 Model3DGroup 设置为 ModelVisual3D 的 Content。将 ModelVisual3D 添加到 Viewport3D 的 Children 集合中。 通过设置 Viewport3D 的 Camera 属性来向其添加照相机。 第 页 共 291 页 微软WPF应用 最后,请向该窗口中添加 Viewport3D。在将 Viewport3D 作为布局元素(如 Canvas)的内容来包含时,可以通过设置 Viewport3D 的 Height 和 Width 属性(继承自 FrameworkElement)来指定 Viewport3D 的大小。 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用 5. WPF的三维变换应用 本主题描述如何向 Windows Presentation Foundation (WPF) 图形系统中的三维模型应用变换。开发人员可以借助于变换功能来对模型进行重定位、调整大小和重定向,而无需更改用来定义模型的基值。 三维坐标空间 Windows Presentation Foundation (WPF) 中的三维图形内容封装在 Viewport3D 元素中,该元素可以参与二维元素结构。该图形系统将 Viewport3D 视为一个像 Windows Presentation Foundation (WPF) 中的许多其他元素一样的二维可视化元素。Viewport3D 充当三维场景中的一个窗口(视区)。更准确地说,它是三维场景所投影到的图面。 尽管可以将 Viewport3D 与其他二维绘图对象用在同一个场景关系图中,但是不能在 Viewport3D 中渗透二维和三维对象。以下讨论中所描述的坐标空间包含在 Viewport3D 元素中。 第 页 共 291 页 微软WPF应用 二维图形的 Windows Presentation Foundation (WPF) 坐标系将原点定位在呈现图面(通常是屏幕)的左上角。在二维系统中,x 轴上的正值朝右增加,y 轴上的正值朝下增加。但是,在三维坐标系中,原点位于屏幕的中心,x 轴上的正值朝右增加,但是 y 轴上的正值朝上增加,z 轴上的正值从原点向外朝向观察者增加。 坐标系比较 由这些轴定义的空间是三维对象在 Windows Presentation Foundation (WPF) 中的固定参考框架。当您在该空间中生成模型并创建光源和照相机以查看这些模型时,一定要在向每个模型应用变换时,将固定参考框架或“全局空间”与您为该模型创建的局部参考框架区分开。另请记住,根据光源和照相机设置,全局空间中的对象可能会看上去完全不同或者根本不可见,但是照相机的位置不会改变对象在全局空间中的位置。 变换模型 当您创建模型时,它们在场景中具有特定的位置。为了在场景中移动、旋转这些模型或者更改这些模型的大小而更改用来定义模型本身的顶点是不切实际的。相反,正如在二维对象中一样,您可以向模型应用变换。 每个模型对象都有一个可用来对模型进行移动、重定向或调整大小的 Transform 属性。当您应用变换时,实际上是按照由变换功能指定的向量或值(以适用者为准)来偏移模型的所有点。换言之,您已经变换了在其中定义模型的坐标空间(“模型空间”),但是,您尚未更改在整个场景的坐标系(“全局空间”)中构成模型几何形状的值。 平移变换 三维变换继承自抽象基类 Transform3D;这些变换包括仿射变换类 TranslateTransform3D、ScaleTransform3D 和 RotateTransform3D。Windows Presentation Foundation (WPF) 三维系统还提供一个 MatrixTransform3D 类,使用该类,可以用更简明的矩阵操作来指定同样的变换。 TranslateTransform3D 沿着您用 OffsetX、OffsetY 和 OffsetZ 属性指定的偏移向量所指示的方向来移动 Model3D 中的所有点。例如,假设立方体的一个顶点位于 (2,2,2),则偏移向量 (0,1.6,1) 会将该顶点 (2,2,2) 移到 (2,3.6,3)。该立方体的顶点在模型空间中仍位于 (2,2,2),但是现在,该模型空间与世界空间的关系已经发生改变,因此,模型空间中的 (2,2,2) 是世界空间中的 (2,3.6,3)。 第 页 共 291 页 微软WPF应用 带有偏移的平移 下面的代码示例演示如何应用平移。 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用 缩放变换 ScaleTransform3D 沿着指定的缩放向量,相对于中心点来更改模型的比例。可以指定等比缩放,即在 X、Y 和 Z 轴上将模型缩放同样的值来按比例更改模型的大小。例如,将该变换的 ScaleX、ScaleY 和 ScaleZ 属性设置为 0.5 会将此模型二等分;将这些属性设置为 2 会将此模型在所有这三个轴上的值放大一倍。 ScaleVector 示例 通过指定非等比变换(X、Y 和 Z 值不相等的缩放变换),可以使模型在一个或两个维度上拉伸或收缩,而不会影响其他维度。例如,如果将 ScaleX、ScaleY 和 ScaleZ 分别设置为 1、2 和 1,则将导致变换模型的高度增加一倍,但是 X 和 Z 轴上的值仍保持不变。 默认情况下,ScaleTransform3D 会导致顶点围绕原点 (0,0,0) 拉伸或收缩。但是,如果要变换的模型不是从原点绘制的,那么,在从原点缩放模型时,模型将不会“就地”缩放。相反,当模型的顶点与缩放向量相乘时,缩放操作对模型的平移和缩放都会产生影响。 缩放中心示例 若要“就地”缩放模型,请通过设置 ScaleTransform3D 的 CenterX、CenterY 和 CenterZ 属性来指定模型的中心。这可确保图形系统缩放模型空间,然后平移该空间,使其在指定的 Point3D 上居中。相反,如果模型是围绕原点生成的,而且您指定了一个不同的中心点,则将看到模型会背离原点平移。 旋转变换 可以通过几种不同的方法来旋转三维模型。典型的旋转变换指定一个轴以及围绕该轴的旋转角度。使用 RotateTransform3D 类,可以用 Rotation 属性来定义 Rotation3D。然后可以在 Rotation3D(在本例中为 AxisAngleRotation3D)上指定 Axis 和 Angle 属性来定义变换。下面的几个示例围绕 Y 轴将模型旋转 60 度。 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用 第 页 共 291 页 微软WPF应用 注意:Windows Presentation Foundation (WPF) 三维是一个右手系统,这意味着,如果旋转角度为正数,则将围绕该轴逆时针旋转。 对于指定了轴和角度的旋转来说,如果没有为 RotateTransform3D 上的 CenterX、CenterY 和 CenterZ 属性指定值,则假设围绕原点旋转。与缩放时一样,一定要记住旋转时会变换模型的整个坐标空间。如果模型不是围绕原点创建的,或者它以前经过平移,则旋转可能会围绕原点“转动”,而不是就地旋转。 在指定了新中心的情况下旋转 若要“就地”旋转模型,请将模型的实际中心指定为旋转中心。由于几何形状通常是围绕原点建模的,因此,在依次执行下列操作时通常将获取一组预期的变换结果:调整模型大小(缩放该模型),然后设置模型方向(旋转该模型),最后将模型移到所需的位置(平移该模型)。 旋转示例 指定了轴和角度的旋转非常适于静态变换和某些动画。但是,请考虑围绕 X 轴将立方体模型旋转 60 度,然后围绕 Z 轴将其旋转 45 度。您可以将这种变换描述为两个离散的仿射变换,也可以将其描述为一个矩阵。但是,对于按照这种方式定义的旋转,可能很难平滑地进行动画处理。尽管按照这两种方法计算的模型起始位置和结束位置相同,但是,从计算的角度来看,该模型经过的中间位置是不确定的。四元数提供了一种用来在旋转的起始位置和结束位置之间计算内插值的替代方法。 第 页 共 291 页 微软WPF应用 四元数表示三维空间中的一个轴以及围绕该轴的旋转。例如,四元数可以表示 (1,1,2) 轴以及 50 度的旋转角度。四元数在定义旋转方面的价值在于可以针对它们执行的两个运算:合成和内插。应用于一个几何形状的两个四元数的合成是指“围绕 axis2 将几何形状旋转 rotation2 度,然后围绕 axis1 将其旋转 rotation1 度”。通过使用合成运算,可以将应用于几何形状的两个旋转合成在一起,以便获得一个代表合成结果的四元数。由于四元数内插可以计算出从一个轴和方向到另一个轴和方向的平滑而又合理的路径,因此您可以从原始位置到合成的四元数之间进行内插,以便实现从一个位置到另一个位置的平滑过渡,从而可以对该变换进行动画处理。对于要进行动画处理的模型,可以通过针对 Rotation 属性使用 QuaternionRotation3D 来为旋转指定目标 Quaternion。 使用变换集合 在生成场景时,通常会向模型应用多个变换。向 Transform3DGroup 类的 Children 集合中添加变换,从而方便地将多个变换组合在一起以应用于场景中的各种模型。通常,可以很方便地在几个不同的组中重用某个变换,这与通过向每个实例应用一组不同的变换来重用模型大体相同。请注意,将变换添加到该集合中的顺序至关重要:集合中的变换是按照从第一个到最后一个的顺序应用的。 对变换进行动画处理 Windows Presentation Foundation (WPF) 三维实现与二维图形参与同一个计时和动画系统。换言之,若要对三维场景进行动画处理,需要对其模型的属性进行动画处理。可以直接对基元的属性进行动画处理,但是通常很容易对用来更改模型位置或外观的变换进行动画处理。由于可以向 Model3DGroup 对象及其各个模型应用变换,因此可以向 Model3DGroup 的子级应用一组动画,向一组对象应用另一组动画。 若要对 Windows Presentation Foundation (WPF) 中的对象进行动画处理,可以创建时间线、定义动画(实际上是随着时间的推移而更改某个属性值)并指定要向其应用动画的属性。此属性必须是 FrameworkElement 的属性。由于三维场景中的所有对象都是 Viewport3D 的子级,因此要应用于场景的任何动画所面向的属性都是 Viewport3D 属性的属性。一定要仔细设计动画的属性路径,因为语法可能会极为冗长。 假设您希望就地旋转某个对象,而且还希望应用摆动动作以公开要查看的对象的更多内容。您可以选择向模型应用 RotateTransform3D,并对模型从一个向量旋转到另一个向量时所围绕的轴进行动画处理。下面的代码示例演示如何将 Vector3DAnimation 应用于该变换的 Rotation3D 的 Axis 属性,并假设 RotateTransform3D 是应用于具有 TransformGroup 的模型的几个变换之一。     第 页 共 291 页 微软WPF应用 使用类似的语法可以将其他变换属性作为目标来移动或缩放该对象。 例如,可以在进行缩放变换时将 Point3DAnimation 应用于 ScaleCenter 属性,以便使模型的形状平滑地扭曲。 尽管上面的几个示例对 GeometryModel3D 的属性进行变换,但是还可以对场景中其他模型的属性进行变换。 例如,可以通过对应用于 Light 对象的平移进行动画处理来创建移动灯光和阴影效果,这些效果可以显著改变模型的外观。 由于照相机也是模型,因此也可以对照相机的属性进行变换。 尽管您确实可以通过改变照相机的位置或平面距离来改变场景的外观(实际上是变换整个场景的投影),但应注意,对于观察者来说,以这种方法实现的许多效果不如将变换应用于场景中模型的地点或位置更有“视觉意义”。 6. WPF的动画开发 Windows Presentation Foundation (WPF) 提供了一组强大的图形和布局功能,通过应用这些功能,可以创建漂亮的用户界面和吸引人的文档。动画不仅可以使漂亮的用户界面更加引人注目,还可以使其更加便于使用。只需对背景色进行动画处理或应用动画 Transform,就可以创造出生动的屏幕过渡效果或提供有帮助的视觉提示。 本概述介绍了 WPF 动画和计时系统,并使用演示图板重点讨论 WPF 对象的动画。 本主题包括以下部分。 动画简介 动画是快速播放一系列图像(其中每个图像与下一个图像略微不同)给人造成的一种幻觉。大脑感觉这组图像是一个变化的场景。在电影中,摄像机每秒钟拍摄许多照片(帧),便可使人形成这种幻觉。用投影仪播放这些帧时,观众便可以看电影了。 计算机上的动画与此类似。例如,使一个矩形逐渐从视野中消失的程序可能按以下方式工作。 程序创建一个计时器。 程序按照设置的时间间隔检查计时器以查看经历了多长时间。 程序每次检查计时器时,它将根据经历的时间来计算矩形的当前不透明度值。 然后程序用新值更新矩形并重画此矩形。 在 WPF 出现之前,Microsoft Windows 开发人员必须创建和管理自己的计时系统或使用特殊的自定义库。WPF 包括一个通过托管代码和 可扩展应用程序标记语言 (XAML) 公开的高效计时系统,该系统紧密地集成到 WPF 框架中。通过使用 WPF 动画,可以轻松地对控件和其他图形对象进行动画处理。 WPF 可以高效地处理管理计时系统和重绘屏幕的所有后台事务。它提供了计时类,使用这些类,可以重点关注要创造的效果,而非实现这些效果的方法。另外,WPF 通过公开动画基类(您的类可以继承自这些类)让您可以轻松创建自己的动画,这样便可以制作自定义动画。这些自定义的动画获得了标准动画类的许多性能优点。 第 页 共 291 页 微软WPF应用 WPF 属性动画系统 如果了解关于计时系统的一些重要概念,则在使用 WPF 时可能会更加轻松一些。最重要的是,在 WPF 中,通过对对象的个别属性应用动画,可以对对象进行动画处理。例如,若要使框架元素增大,请对其 Width 和 Height 属性进行动画处理。若要使对象逐渐从视野中消失,可以对其 Opacity 属性进行动画处理。 要使属性具有动画功能,属性必须满足以下三个要求: 它必须是依赖项属性。 它必须属于继承自 DependencyObject 并实现 IAnimatable 接口的类。 必须存在可用的兼容动画类型. WPF 包含许多具有动画属性的对象。诸如 Button、TabControl 和 Panel 控件以及 Shape 对象都继承自 DependencyObject。它们的大多数属性都是依赖项属性。 您几乎可以在任何地方使用动画,包括在样式和控件模板中使用。动画未必可见;对于不属于用户界面的对象,只要它们满足本节中所述的条件,便可以对其进行动画处理。 示例:使元素逐渐进入视野并从视野中逐渐消失 此示例演示如何使用 WPF 动画对依赖项属性的值进行动画处理。本示例使用 DoubleAnimation(一种生成 Double 值的动画类型)对 Rectangle 的 Opacity 属性进行动画处理。因此,Rectangle 将逐渐进入视野并逐渐从视野中消失。 示例的第一部分创建一个 Rectangle 元素,并将其显示在 Page 中。下面的步骤表明如何创建动画并将其应用于矩形的 Opacity 属性。   第 页 共 291 页 微软WPF应用   第 1 部分:创建 DoubleAnimation 使元素逐渐进入视野并逐渐从视野中消失的一种方法是对其 Opacity 属性进行动画处理。由于 Opacity 属性的类型是 Double,因此需要一个产生双精度值的动画。DoubleAnimation 就是这样的一个动画。DoubleAnimation 创建两个双精度值之间的过渡。若要指定其起始值,可设置其 From 属性。若要指定其终止值,可设置其 To 属性。 不透明度值 1.0 使对象完全不透明,不透明度值 0.0 使对象完全不可见。若要使动画的不透明度值从 1.0 过渡为 0.0,可以将其 From 属性设置为 1.0,将其 To 属性设置为 0.0。   [C#]   然后,必须指定 Duration。动画的 Duration 指定了从其起始值过渡为目标值所需的时间。在下面的示例中,为动画指定的持续时间为五秒钟。   第 页 共 291 页 微软WPF应用   上面的代码显示了不透明度值从 1.0 向 0.0 转换的动画,此转换使目标元素从完全不透明逐渐转变为完全不可见。若要使元素在消失后再逐渐回到视野中,请将动画的 AutoReverse 属性设置为 true。若要使动画无限期地重复,请将其 RepeatBehavior 属性设置为 Forever。     第 2 部分:创建演示图板 若要向对象应用动画,请创建 Storyboard 并使用 TargetName 和 TargetProperty 附加属性指定要进行动画处理的对象和属性。 创建 Storyboard 并将动画添加为其子项。   在代码中,将 Storyboard 声明为类成员。   然后初始化 Storyboard 并将动画添加为其子项。   必须知道要在哪里应用动画。使用 Storyboard..::.TargetName 附加属性指定要进行动画处理的对象。在下面的代码中,为 DoubleAnimation 指定了一个目标名称 MyRectangle,这是要进行动画处理的对象的名称。 第 页 共 291 页 微软WPF应用      说明: 在代码中创建演示图板时,必须执行两个附加步骤:创建名称范围以及注册要进行动画处理的对象的名称。本节开头的代码创建了页面和矩形,还声明了 NameScope 并注册了矩形的名称。如果还没有 NameScope,则可以使用 SetNameScope 方法创建一个。您可以使用 RegisterName 方法向为其创建了 NameScope 的元素注册目标对象的名称。否则,Storyboard 无法找到要进行动画处理的对象。 使用 TargetProperty 附加属性指定要进行动画处理的属性。在下面的代码中,动画被配置为面向 Rectangle 的 Opacity 属性。     第 3 部分 (XAML):将演示图板与触发器关联 在 XAML 中应用和启动 Storyboard 的最简单的方法是使用事件触发器。 创建一个 BeginStoryboard 对象并将演示图板与其关联。BeginStoryboard 是一种应用和启动 Storyboard 的 TriggerAction。   第 页 共 291 页 微软WPF应用 创建 EventTrigger 并将 BeginStoryboard 添加至其 Actions 集合。将 EventTrigger 的 RoutedEvent 属性设置为启动 Storyboard 所需的路由事件。   将 EventTrigger 添加至矩形的 Triggers 集合。   第 3 部分(代码):将演示图板与事件处理程序关联 在代码中应用和启动 Storyboard 的最简单的方法是使用事件处理程序。 注册矩形的 Loaded 事件。 [C#]   声明事件处理程序。在事件处理程序中,使用 Begin 方法来应用演示图板。 [C#] 第 页 共 291 页 微软WPF应用   完整的示例 下面的示例演示了一段完整代码,这段代码用于创建逐渐进入视野并从视野中逐渐消失的矩形。   第 页 共 291 页 微软WPF应用   动画类型 由于动画生成属性值,因此对于不同的属性类型,会有不同的动画类型。若要对采用 Double 的属性(例如元素的 Width 属性)进行动画处理,请使用生成 Double 值的动画。若要对采用 Point 的属性进行动画处理,请使用生成 Point 值的动画,依此类推。由于存在许多不同的属性类型,因此 System.Windows.Media.Animation 命名空间中存在一些动画类。幸好它们都遵循严格的命名约定,因此可以轻松地区分它们: 第 页 共 291 页 微软WPF应用 <类型>Animation 这些动画称为“From/To/By”或“基本”动画,它们在起始值和目标值之间进行动画处理,或者通过将偏移量值与其起始值相加来进行动画处理。 若要指定起始值,请设置动画的“From”属性。 若要指定终止值,请设置动画的“To”属性。 若要指定偏移量值,请设置动画的“By”属性。 此概述中的示例使用这些动画,因为这些动画使用起来最简单。From/To/By 动画概述中详细描述了“From/To/By”动画。 <类型>AnimationUsingKeyFrames 关键帧动画的功能比“From/To/By”动画的功能更强大,因为您可以指定任意多个目标值,甚至可以控制它们的插值方法。某些类型的内容只能用关键帧动画进行动画处理。关键帧动画概述中详细描述了关键帧动画。 <类型>AnimationUsingPath 路径动画使您能够使用几何路径来生成动画值。 <类型>AnimationBase 在实现时对 <类型> 值进行动画处理的抽象类。此类用作 <类型>Animation 和 <类型>AnimationUsingKeyFrames 类的基类。只有在想要创建自己的自定义动画时,才必须直接处理这些类。否则,请使用 <类型>Animation 或 KeyFrame<类型>Animation。 在大多数情况下,您将需要使用 <类型>Animation 类,例如 DoubleAnimation 和 ColorAnimation。 下表显示了一些常用动画类型以及与这些类型一起使用的一些属性。 第 页 共 291 页 微软WPF应用   动画是时间线 所有动画类型均继承自 Timeline 类,因此所有动画都是专用类型的时间线。Timeline 定义时间段。您可以指定时间线的以下计时行为:其 Duration 和重复次数,甚至可以为时间线指定时间走得多快。 因为动画是 Timeline,所以它还表示一个时间段。在动画的指定时间段(即 Duration)内运行动画时,动画还会计算输出值。在运行或“播放”动画时,动画将更新与其关联的属性。 Duration、AutoReverse 和 RepeatBehavior 是三个常用的计时属性。 Duration 属性 如前文所述,时间线代表一个时间段。该时间段的长度由时间线的 Duration(通常用 TimeSpan 值来指定)来决定。当时间线达到其持续时间的终点时,表示时间线完成了一次重复。 动画使用其 Duration 属性来确定其当前值。如果没有为动画指定 Duration 值,它将使用默认值(1 秒)。 下面的语法显示了 Duration 属性 (Property) 的 可扩展应用程序标记语言 (XAML) 属性 (Attribute) 语法的简化版本。 下表显示了一些 Duration 设置及其结果值。   第 页 共 291 页 微软WPF应用 在代码中指定 Duration 的一种方法是使用 FromSeconds 方法创建 TimeSpan,然后使用该 TimeSpan 声明新的 Duration 结构。 AutoReverse AutoReverse 属性指定时间线在到达其 Duration 的终点后是否倒退。如果将此动画属性设置为 true,则动画在到达其 Duration 的终点后将倒退,即从其终止值向其起始值反向播放。默认情况下,该属性为 false。 RepeatBehavior RepeatBehavior 属性指定时间线的播放次数。默认情况下,时间线的重复次数为 1.0,即播放一次时间线,根本不进行重复。 对属性应用动画 前面几节描述动画的不同类型及其计时属性。本节演示如何对要进行动画处理的属性应用动画。Storyboard 对象提供了对属性应用动画的一种方法。Storyboard 是一个为其所包含的动画提供目标信息的容器时间线。 以对象和属性为目标 Storyboard 类提供 TargetName 和 TargetProperty 附加属性。通过在动画上设置这些属性,您将告诉动画对哪些内容进行动画处理。但是,通常必须为对象提供一个名称,动画才能以该对象作为处理目标。 为 FrameworkElement 分配名称不同于为 Freezable 对象分配名称。大多数控件和面板是框架元素;而大多数纯图形对象(如画笔、转换和几何图形)是可冻结的对象。 若要使 FrameworkElement 成为动画目标,应通过设置其 Name 属性为其提供一个名称。在代码中,还必须使用 RegisterName 方法向元素名称所属的页面注册该元素名称。 要使用 XAML 使 Freezable 对象成为目标,应使用 x:Name 属性为其分配一个名称。在代码中,只需使用 RegisterName 方法向对象所属的页面注册该对象。 下面几节举例说明如何使用 XAML 和代码为元素命名。 应用和启动演示图板 若要使用 XAML 启动演示图板,应将其与 EventTrigger 关联。EventTrigger 是一个描述在发生指定事件时执行哪些操作的对象。这些操作中的一个操作可以是 BeginStoryboard 操作,您可以使用此操作启动演示图板。事件触发器与事件处理程序的概念类似,因为通过使用它们,您都可以指定应用程序如何响应特定事件。与事件处理程序不同的是,您完全可以使用 XAML 来描述事件触发器,而无需使用其他代码。 若要使用代码启动 Storyboard,您可以使用 EventTrigger 或使用 Storyboard 类的 Begin 方法。 以交互方式控制演示图板 前面的示例演示如何在事件发生时启动 Storyboard。您也可以在 Storyboard 启动后以交互方式控制它:可以暂停、继续和停止它,并可以使其进入到其填充期,还可以查找和移除 Storyboard。 第 页 共 291 页 微软WPF应用 动画结束后会发生什么情况? FillBehavior 属性指定时间线结束时的行为方式。默认情况下,时间线结束时将启动 Filling。处于 Filling 状态动画将保持其最终输出值。 前面示例中的 DoubleAnimation 不会终止,因为它的 RepeatBehavior 属性设置为 Forever。下面的示例使用类似的动画对矩形进行动画处理。与前面的示例不同,此动画的 RepeatBehavior 和 AutoReverse 属性设为默认值。因此,动画运行五秒钟使其不透明度值从 1 转变成 0,然后停止。     因为动画的 FillBehavior 未发生变化,仍然为其默认值 HoldEnd,所以当动画结束时动画将保持其最终值 0。因此,在动画结束后,矩形的 Opacity 仍然为 0。如果将矩形的 Opacity 设置为另一个值,则您的代码似乎无效,因为动画仍将影响 Opacity 属性。 在代码中重新获得对动画属性的控制的一种方法是使用 BeginAnimation 方法,并将 AnimationTimeline 参数指定为 null。 请注意,虽然设置一个具有 Active 或 Filling 动画的属性值好像不起作用,但属性值确实发生了变化。 对动画进行数据绑定和动画处理 您可以对大多数动画属性绑定数据或进行动画处理;例如,可以对 DoubleAnimation 的 Duration 属性进行动画处理。但是,由于计时系统工作方式的缘故,绑定有数据或进行过动画处理的动画的行为与其他绑定有数据或进行过动画处理的对象的行为不同。若要了解它们的行为,请了解对属性应用动画的意义,这将十分有用。 第 页 共 291 页 微软WPF应用 加载前面示例中的矩形时,它的事件触发器将应用 Storyboard。计时系统会创建 Storyboard 及其动画的副本。系统将冻结这些副本(使它们成为只读副本),并且将根据它们来创建 Clock 对象。这些时钟将执行对目标属性进行动画处理的实际工作。 计时系统为 DoubleAnimation 创建一个时钟,并将该时钟应用于 DoubleAnimation 的 TargetName 和 TargetProperty 所指定的对象和属性。在本例中,计时系统将时钟应用于名为“MyRectangle”的对象的 Opacity 属性。 虽然也会为 Storyboard 创建一个时钟,但未对任何属性应用该时钟。该时钟的用途是控制其子时钟(为 DoubleAnimation 创建的时钟)。 若要使动画反映数据绑定或动画更改,必须重新生成其时钟。系统不会为您自动生成时钟。若要使动画反映更改,请使用 BeginStoryboard 或 Begin 方法重新应用其演示图板。当使用其中的任何一种方法时,动画将重新启动。在代码中,可以使用 Seek 方法将演示图板移回到其从前的位置。 其他动画处理方式 此概述中的示例演示如何使用演示图板进行动画处理。如果使用代码,则可以采用一些其他方法进行动画处理。 动画示例 以下示例可以帮助您开始向应用程序添加动画。 属性动画示例 演示如何对框架元素和可冻结的对象应用动画。 From/To/By 动画的目标值示例 演示不同的 From/To/By 设置。 动画计时行为的示例 演示可控制动画计时行为的不同方法。此示例还演示如何绑定动画目标值数据。 7. WPF的多媒体开发 本主题将介绍 Windows Presentation Foundation (WPF) 的多媒体功能。使用多媒体功能可以将声音和视频集成到应用程序中,从而增强了用户体验。 媒体 API MediaElement 和 MediaPlayer 用于播放音频、视频以及包含音频内容的视频。这两种类型都可以以交互方式或时钟驱动方式进行控制。这两种类型都至少依赖 Microsoft Windows Media Player 10 OCX 进行媒体播放。但这两种 API 的用法因具体情况而异。 MediaElement 是一个 UIElement,它受 布局系统 支持并可用作许多控件的内容。它也可用在可扩展应用程序标记语言 (XAML) 以及代码中。另一方面,MediaPlayer 用于 Drawing 对象,因而缺少对布局的支持。只能使用 VideoDrawing 或通过直接与 DrawingContext 进行交互来呈现使用 MediaPlayer 加载的媒体。不能在 XAML 中使用 MediaPlayer。 第 页 共 291 页 微软WPF应用 说明: 如果将媒体与应用程序一起分发,则不能将媒体文件用作项目资源。在项目文件中,必须将媒体类型改设为 Content,并将 CopyToOutputDirectory 设置为 PreserveNewest 或 Always。 媒体播放模式 说明: MediaElement 和 MediaPlayer 具有类似的成员。本部分中的链接指的是 MediaElement 类成员。除非明确说明,否则链接到 MediaElement 类中的成员也可在 MediaPlayer 类中找到。 若要了解 Windows Presentation Foundation (WPF) 中的媒体播放,需要先了解可播放媒体的不同模式。MediaElement 和 MediaPlayer 可以用于两种不同的媒体模式中:独立模式和时钟模式。媒体模式由 Clock 属性确定。如果 Clock 为 null,则媒体对象处于独立模式。如果 Clock 不为 null,则媒体对象处于时钟模式。默认情况下,媒体对象处于独立模式。 独立模式 在独立模式下,由媒体内容驱动媒体播放。独立模式实现了下列功能选项: 可直接指定媒体的 Uri。 可直接控制媒体播放。 可修改媒体的 Position 和 SpeedRatio 属性。 通过设置 MediaElement 对象的 Source 属性或者调用 MediaPlayer 对象的 Open 方法来加载媒体。 若要在独立模式下控制媒体播放,可使用媒体对象的控制方法。提供了下列控制方法:Play、Pause、Close 和 Stop。对于 MediaElement,仅当将 LoadedBehavior 设置为 Manual 时,使用这些方法的交互式控件才可用。当媒体对象处于时钟模式时,这些方法将不可用。 时钟模式 在时钟模式下,由 MediaTimeline 驱动媒体播放。时钟模式具有下列特征: 媒体的 Uri 是通过 MediaTimeline 间接设置的。 可由时钟控制媒体播放。不能使用媒体对象的控制方法。 可通过以下方法加载媒体:设置 MediaTimeline 对象的 Source 属性,从时间线创建时钟,并将时钟分配给媒体对象。当位于 Storyboard 中的 MediaTimeline 针对 MediaElement 时,也可用这种方法加载媒体。 若要在时钟模式下控制媒体播放,必须使用 ClockController 控制方法。ClockController 是从 MediaClock 的 ClockController 属性获取的。如果尝试在时钟模式下使用 MediaElement 或 MediaPlayer 对象的控制方法,则会引发 InvalidOperationException。 第 页 共 291 页 微软WPF应用 MediaElement 类 向应用程序添加媒体的操作十分简单,只需向应用程序的用户界面 (UI) 添加 MediaElement 控件,并为要包含的媒体提供 Uri。Windows Presentation Foundation (WPF) 中支持 Microsoft Windows Media Player 10 所支持的所有媒体类型。下面的示例演示 MediaElement 在可扩展应用程序标记语言 (XAML) 中的简单用法。     在此示例中,媒体在加载后即会自动播放。播放完后,就会关闭媒体,并且会释放所有媒体资源(包括视频内存)。此行为是 MediaElement 对象的默认行为,由 LoadedBehavior 和 UnloadedBehavior 属性控制。 控制 MediaElement 当 IsLoaded 为 true 或 false 时,可分别使用 LoadedBehavior 和 UnloadedBehavior 属性控制 MediaElement 的行为。设置 MediaState 属性的目的是影响媒体播放行为。例如,默认的 LoadedBehavior 为 Play,而默认的 UnloadedBehavior 为 Close。这意味着加载 MediaElement 并完成预播放后,即会开始播放媒体。播放完后,就会关闭媒体,并且会释放所有媒体资源。 LoadedBehavior 和 UnloadedBehavior 属性不是控制媒体播放的唯一方法。在时钟模式下,时钟可以控制 MediaElement,并且这些交互式控制方法在 LoadedBehavior 为 Manual 时具有控制权。MediaElement 通过计算下列优先级来处理此控制权的竞争。 UnloadedBehavior. 在卸载媒体时发生。这可确保默认情况下释放所有媒体资源,即使 MediaClock 与 MediaElement 关联也是如此。 MediaClock. 在媒体具有 Clock 时发生。如果卸载媒体,则只要 UnloadedBehavior 为 Manual,MediaClock 就会生效。时钟模式始终重写 MediaElement 的加载行为。 LoadedBehavior. 在加载媒体时发生。 第 页 共 291 页 微软WPF应用 交互式控制方法。在 LoadedBehavior 为 Manual 时发生。提供了下列控制方法:Play、Pause、Close 和 Stop。 显示 MediaElement 若要显示 MediaElement,它必须具有要呈现的内容,并在加载内容之前将其 ActualWidth 和 ActualHeight 属性设置为零。对于仅包含音频的内容,这些属性将始终为零。对于视频内容,在 MediaOpened 事件引发 ActualWidth 和 ActualHeight 后,会报告已加载媒体的大小。这意味着在加载媒体之前,MediaElement 不会占用用户界面 (UI) 中的任何物理空间,除非设置了 Width 或 Height 属性。 如果设置 Width 和 Height 属性,则会导致拉伸媒体来填充为 MediaElement 提供的区域。若要保持媒体的原始纵横比,应设置 Width 或 Height 属性,但不能同时设置这两者。如果同时设置 Width 和 Height 属性,则会使媒体以固定元素大小显示,可能无法达到预期效果。 为避免元素大小固定,Windows Presentation Foundation (WPF) 可以预播放媒体。为此,需要将 LoadedBehavior 设置为 Play 或 Pause。在 Pause 状态下,媒体将预播放第一帧。在 Play 状态下,媒体将预播放,然后再开始播放。  MediaPlayer 类 当 MediaElement 类为框架元素时,MediaPlayer 类设计为在 Drawing 对象中使用。在可牺牲框架级功能来获得性能的提高或者需要 Freezable 功能时,可使用 Drawing 对象。通过 MediaPlayer,您可以在应用程序中提供媒体内容,同时使用这些功能。与 MediaElement 类似,MediaPlayer 可在独立模式或时钟模式下使用,但不具有 MediaElement 对象的卸载和加载状态。这会降低 MediaPlayer 的播放控制的复杂程度。 控制 MediaPlayer 由于 MediaPlayer 是无状态的,因此只能使用两种方法控制媒体播放。 交互式控制方法。在处于独立模式(null Clock 属性)时采用。 MediaClock. 在媒体具有 Clock 时采用。 显示 MediaPlayer 从技术角度来说,不能显示 MediaPlayer,因为它没有物理表示形式。但它可用于通过使用 VideoDrawing 类在 Drawing 中呈现媒体。下面的示例演示如何使用 VideoDrawing 显示媒体。 第 页 共 291 页 微软WPF应用   第 页 共 291 页

下载文档到电脑,查找使用更方便

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 1 人已下载

下载文档