Windows Presentation Foundation Unleashed (WPF) 摘录






3.30/5 (5投票s)
2007 年 9 月 7 日
50分钟阅读

54413
本文介绍了一些 WPF 相较于 .NET 程序员已熟悉的概念所引入的主要概念。
第三章:WPF 中的重要新概念
本章内容
- 逻辑树和视觉树
- 依赖属性
- 路由事件
- 命令
- 类层次结构之旅
为了完成本书第一部分,并在深入探讨真正有趣的主题之前,了解 WPF 相较于 .NET 程序员已熟悉的概念所引入的一些主要概念是有益的。本章的主题是导致 WPF 学习曲线陡峭的一些主要原因。通过现在熟悉这些概念,您将能够自信地学习本书的其余部分(或其他任何 WPF 文档)。
本章中的一些概念是全新的(例如逻辑树和视觉树),而另一些则只是您应该很熟悉的概念的扩展(例如属性和事件)。在学习每个概念时,您还将看到如何将其应用于大多数程序都需要的一个非常简单的用户界面——“关于”对话框。
逻辑树和视觉树
XAML 由于其分层性质,自然适合表示用户界面。在 WPF 中,用户界面由一组称为“逻辑树”的对象构成。
列表 3.1 定义了一个假设的“关于”对话框的开头,使用 Window
作为逻辑树的根。Window
有一个 StackPanel
子元素(在第六章“使用面板进行布局”中有介绍),其中包含几个简单的控件以及另一个包含 Button
的 StackPanel
。
列表 3.1 XAML 中简单的“关于”对话框
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
图 3.1 显示了渲染后的对话框(您可以通过将列表 3.1 的内容粘贴到 XamlPad 等工具中轻松生成),而 图 3.2 说明了该对话框的逻辑树。

列表 3.1 中渲染的对话框。
请注意,即使对于不是用 XAML 创建的 WPF 用户界面,逻辑树也存在。列表 3.1 可以完全用过程代码实现,并且逻辑树将是相同的。
逻辑树的概念很简单,但您为什么要关心它呢?因为 WPF 的几乎所有方面(属性、事件、资源等)都具有与逻辑树相关的行为。例如,属性值有时会自动向下传播到子元素,而引发的事件可以沿着树向上或向下传播。这些行为将在本章后面讨论。
与逻辑树类似的概念是“视觉树”。视觉树基本上是逻辑树的扩展,其中节点被分解成其核心视觉组件。视觉树不是将每个元素留作“黑匣子”,而是暴露了视觉实现细节。例如,尽管 ListBox
在逻辑上是一个单独的控件,但其默认视觉表示由更原始的 WPF 元素组成:一个 Border
、两个 ScrollBar
以及更多元素。

列表 3.1 的逻辑树。
并非所有逻辑树节点都出现在视觉树中;只有派生自 System.Windows.Media.Visual
或 System.Windows.Media.Visual3D
的元素才包含在内。其他元素(以及简单的字符串内容,如列表 3.1 中所示)不包含在内,因为它们本身没有固有的渲染行为。
提示 - XamlPad 的工具栏中有一个按钮,可以显示它渲染的任何 XAML 的视觉树(和属性值)。当托管 Window
(如图 3.1 中所示)时,它不起作用,但您可以将 Window
元素更改为 Page
(并删除 SizeToContent
属性)来利用此功能。
图 3.3 说明了列表 3.1 在 Windows Vista 上使用 Aero 主题运行时默认的视觉树。此图揭示了一些当前不可见的 UI 内部组件,例如 ListBox
的两个 ScrollBar
和每个 Label
的 Border
。它还显示 Button
、Label
和 ListBoxItem
都由相同的元素组成,只是 Button
使用一个晦涩的 ButtonChrome
元素而不是 Border
。(这些控件由于默认属性值不同而具有其他视觉差异。例如,Button
在所有侧面都有 10 的默认 Margin
,而 Label
的默认 Margin
为 0。)

列表 3.1 的视觉树,其中逻辑树节点已加粗显示。
由于视觉树使您能够深入了解 WPF 元素的深层组成,因此它们可能非常复杂。幸运的是,尽管视觉树是 WPF 基础设施的重要组成部分,但除非您要对控件进行大规模样式重构(在第十章“样式、模板、皮肤和主题”中介绍)或进行低级绘图(在第十一章“2D 图形”中介绍),否则您通常不需要担心它们。例如,编写依赖于特定 Button
视觉树的代码会违反 WPF 的核心原则之一——外观与逻辑的分离。当有人使用第十章中描述的技术使用样式重构 Button
等控件时,其整个视觉树将被替换为可能完全不同的内容。
话虽如此,您可以使用 System.Windows.LogicalTreeHelper
和 System.Windows.Media.VisualTreeHelper
类相对对称地遍历逻辑树和视觉树。列表 3.2 包含列表 3.1 的一个代码隐藏文件,当在调试器下运行时,它会输出“关于”对话框的逻辑树和视觉树的简单深度优先表示。(这需要向列表 3.1 添加 x:Class="AboutDialog"
和相应的 xmlns:x
指令,以便将其与此过程代码连接起来。)
警告 - 避免编写依赖于特定视觉树的代码!- 尽管逻辑树在没有程序员干预的情况下是静态的(例如,动态添加/删除元素),但视觉树可以通过用户切换到不同的 Windows 主题而简单地改变!
列表 3.2 遍历并打印逻辑树和视觉树
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Media;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
PrintLogicalTree(0, this);
}
protected override void OnContentRendered(EventArgs e)
{
base.OnContentRendered(e);
PrintVisualTree(0, this);
}
void PrintLogicalTree(int depth, object obj)
{
// Print the object with preceding spaces that represent its depth
Debug.WriteLine(new string(' ', depth) + obj);
// Sometimes leaf nodes aren't DependencyObjects (e.g. strings)
if (!(obj is DependencyObject)) return;
// Recursive call for each logical child
foreach (object child in LogicalTreeHelper.GetChildren(
obj as DependencyObject))
PrintLogicalTree(depth + 1, child);
}
void PrintVisualTree(int depth, DependencyObject obj)
{
// Print the object with preceding spaces that represent its depth
Debug.WriteLine(new string(' ', depth) + obj);
// Recursive call for each visual child
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
PrintVisualTree(depth + 1, VisualTreeHelper.GetChild(obj, i));
}
}
当使用深度为 0 和当前 Window
实例调用这些方法时,结果是文本树,其中包含与图 3.2 和图 3.3 中显示的节点完全相同的节点。尽管逻辑树可以在 Window
的构造函数中进行遍历,但视觉树在 Window
至少经过一次布局之前是空的。这就是为什么 PrintVisualTree
在 OnContentRendered
中被调用,它在布局发生后才会被调用。
有时可以使用元素实例上的实例方法来导航任一树。例如,Visual
类包含三个受保护成员(VisualParent
、VisualChildrenCount
和 GetVisualChild
)用于检查其视觉父级和子级。FrameworkElement
,作为 Button
和 Label
等控件的常见基类,定义了一个公共 Parent
属性来表示逻辑父级。FrameworkElement
的特定子类以不同的方式公开其逻辑子级。例如,一些类公开 Children
集合,而其他类(如 Button
和 Label
)则公开 Content
属性,强制要求元素只能有一个逻辑子级。
提示 - 如图 3.3 所示的视觉树通常简称为元素树,因为它们包含逻辑树中的元素和视觉树特有的元素。然后,视觉树一词用于描述任何包含仅视觉(非逻辑?)元素的子树。例如,大多数人会说 Window
的默认视觉树由 Border
、AdornerDecorator
、两个 AdornerLayer
、一个 ContentPresenter
组成,仅此而已。在图 3.3 中,尽管 VisualTreeHelper
将顶层的 StackPanel
显示为 ContentPresenter
的视觉子级,但通常不认为它是 ContentPresenter
的视觉子级。
依赖属性
WPF 引入了一种新的属性类型,称为依赖属性,它在整个平台中用于实现样式、自动数据绑定、动画等。您最初可能会对这个概念持怀疑态度,因为它使 .NET 类型拥有简单字段、属性、方法和事件的图景变得复杂。但是,一旦您理解了依赖属性要解决的问题,您很可能会认为它们是一个受欢迎的补充。
依赖属性在任何给定时间依赖于多个提供者来确定其值。这些提供者可能是不断改变其值的动画,或者是其属性值向下传递到其子级的一个父元素,等等。可以说,依赖属性的最大特点是其内置的更改通知能力。
向属性添加这种智能的动机是为了直接从声明性标记实现丰富的功能。WPF 的声明性友好设计关键在于其对属性的大量使用。例如,Button
有 96 个公共属性!属性可以在 XAML 中轻松设置(直接或通过设计工具),而无需任何过程代码。但是,如果没有依赖属性中的额外基础结构,仅仅设置属性就很难在不编写额外代码的情况下获得所需的结果。
在本节中,我们将简要了解依赖属性的实现,以使讨论更具体,然后我们将深入探讨一些依赖属性如何超越普通 .NET 属性增加价值。
- 更改通知
- 属性值继承
- 支持多个提供者
理解依赖属性的大多数细微差别通常仅对自定义控件作者很重要。然而,即使是 WPF 的随意用户也需要了解它们是什么以及它们如何工作。例如,您只能设置和动画化依赖属性。在与 WPF 合作一段时间后,您可能会发现自己希望所有属性都是依赖属性!
依赖属性实现
在实践中,依赖属性只是普通的 .NET 属性,已连接到一些额外的 WPF 基础设施。这都是通过 WPF API 完成的;除了 XAML 之外,没有 .NET 语言能内在理解依赖属性。
列表 3.3 演示了 Button
如何有效地实现其名为 IsDefault
的一个依赖属性。
列表 3.3 标准依赖属性实现
public class Button : ButtonBase
{
// The dependency property
public static readonly DependencyProperty IsDefaultProperty;
static Button()
{
// Register the property
Button.IsDefaultProperty = DependencyProperty.Register("IsDefault",
typeof(bool), typeof(Button),
new FrameworkPropertyMetadata(false,
new PropertyChangedCallback(OnIsDefaultChanged)));
...
}
// A .NET property wrapper (optional)
public bool IsDefault
{
get { return (bool)GetValue(Button.IsDefaultProperty); }
set { SetValue(Button.IsDefaultProperty, value); }
}
// A property changed callback (optional)
private static void OnIsDefaultChanged(
DependencyObject o, DependencyPropertyChangedEventArgs e) { ... }
...
}
静态 IsDefaultProperty
字段是实际的依赖属性,由 System.Windows.DependencyProperty
类表示。按照惯例,所有 DependencyProperty
字段都是公共的、静态的,并且具有 Property
后缀。通常通过调用静态 DependencyProperty.Register
方法创建依赖属性,该方法需要一个名称(IsDefault
)、一个属性类型(bool
)以及声称拥有该属性的类的类型(Button
)。可选地(通过 Register
的不同重载),您可以传递元数据来定制 WPF 如何处理该属性,以及用于处理属性值更改、强制转换值和验证值的回调。Button
在其静态构造函数中调用 Register
的一个重载,以将依赖属性的默认值设置为 false
,并附加一个更改通知委托。
最后,名为 IsDefault
的传统 .NET 属性通过调用从 System.Windows.DependencyObject
继承的 GetValue
和 SetValue
方法来实现其访问器,DependencyObject
是所有具有依赖属性的类都必须派生自的一个低级基类。GetValue
返回最后传递给 SetValue
的值,或者,如果 SetValue
之前从未被调用过,则返回与属性注册的默认值。在这种情况下,IsDefault
.NET 属性(有时称为属性包装器)并非严格必需;Button
的使用者始终可以直接调用 GetValue
/SetValue
方法,因为它们是公开的。但是,.NET 属性使得属性的以编程方式读取和写入对使用者更加自然,并且它允许通过 XAML 设置属性。
警告 - 在 XAML 中设置依赖属性时,会绕过 .NET 属性包装器!- 尽管 XAML 编译器在编译时依赖于属性包装器,但在运行时,WPF 会直接调用底层的 GetValue
和 SetValue
方法!因此,为了在 XAML 中设置属性与过程代码之间保持一致,属性包装器中除了 GetValue
/SetValue
调用之外不包含任何逻辑至关重要。如果您想添加自定义逻辑,那正是注册的回调函数的作用。WPF 的所有内置属性包装器都遵守此规则,因此此警告适用于任何编写自己的依赖属性的自定义类的用户。
表面上看,列表 3.3 看起来像是表示简单布尔属性的过度冗长的方式。然而,由于 GetValue
和 SetValue
内部使用高效的稀疏存储系统,并且 IsDefaultProperty
是一个静态字段(而不是实例字段),因此依赖属性实现比典型的 .NET 属性节省了每个实例的内存。如果 WPF 控件上的所有属性都是实例字段的包装器(如大多数 .NET 属性那样),由于每个实例附加了大量本地数据,它们将消耗大量内存。每个 Button
拥有 96 个字段,每个 Label
拥有 89 个字段,等等,这将很快累积起来!相反,Button
的 96 个属性中有 78 个是依赖属性,而 Label
的 89 个属性中有 71 个是依赖属性。
依赖属性实现的好处不仅仅是内存使用,还远远不止于此。它集中并标准化了属性实现者需要编写的相当多的代码,以检查线程访问、提示包含元素重新渲染等。例如,如果一个属性在值更改时需要其元素重新渲染(例如 Button
的 Background
属性),它可以简单地将 FrameworkPropertyMetadataOptions.AffectsRender
标志传递给 DependencyProperty.Register
的一个重载。此外,此实现启用了我们将在下面逐一探讨的三个功能,首先是更改通知。
更改通知
每当依赖属性的值更改时,WPF 都可以根据属性的元数据自动触发一系列操作。这些操作可以重新渲染适当的元素、更新当前布局、刷新数据绑定等等。此内置更改通知启用的最有趣功能之一是属性触发器,它使您无需编写任何过程代码即可在属性值更改时执行自己的自定义操作。
例如,假设您希望列表 3.1 中“关于”对话框中每个 Button
的文本在鼠标指针悬停在其上方时变为蓝色。如果没有属性触发器,您可以向每个 Button
附加两个事件处理程序,一个用于其 MouseEnter
事件,一个用于其 MouseLeave
事件。
<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave"
MinWidth="75" Margin="10">Help</Button>
<Button MouseEnter="Button_MouseEnter" MouseLeave="Button_MouseLeave"
MinWidth="75" Margin="10">OK</Button>
这两个处理程序可以在 C# 代码隐藏文件中实现如下:
// Change the foreground to blue when the mouse enters the button
void Button_MouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null) b.Foreground = Brushes.Blue;
}
// Restore the foreground to black when the mouse exits the button
void Button_MouseLeave(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null) b.Foreground = Brushes.Black;
}
但是,通过属性触发器,您可以纯粹在 XAML 中完成相同的行为。以下简洁的 Trigger
对象(差不多)就是您需要的所有内容:
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Blue"/>
</Trigger>
此触发器可以作用于 Button
的 IsMouseOver
属性,该属性在 MouseEnter
事件引发时变为 true
,在 MouseLeave
事件引发时变为 false
。请注意,您不必担心在 IsMouseOver
变为 false
时将 Foreground
恢复为黑色。WPF 会自动完成此操作!
唯一的技巧是将此 Trigger
应用于每个 Button
。不幸的是,由于 WPF 3.0 的一个人为限制,您无法直接将属性触发器应用于 Button
等元素。它们只能在 Style
对象内部应用,因此对属性触发器的深入探讨推迟到第十章。在此期间,如果您想尝试属性触发器,可以通过将它们包装在几个中间 XML 元素中来将前面的 Trigger
应用于 Button
,如下所示:
<Button MinWidth="75" Margin="10">
<Button.Style>
<Style TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="Foreground" Value="Blue"/>
</Trigger>
</Style.Triggers>
</Style>
</Button.Style>
OK
</Button>
属性触发器只是 WPF 支持的三种触发器类型之一。数据触发器是属性触发器的一种形式,适用于所有 .NET 属性(不仅仅是依赖属性),也将在第十章中介绍。事件触发器使您能够以声明方式指定在引发路由事件(本章后面介绍)时要执行的操作。事件触发器总是涉及动画或声音,因此它们将在第十三章“动画”中介绍。
警告 - 不要被元素的 Triggers 集合所迷惑!- FrameworkElement
的 Triggers
属性是 TriggerBase
项目(所有三种触发器类型的共同基类)的读写集合,因此它看起来像是将属性触发器轻松应用于 Button
等控件的便捷方式。不幸的是,在 WPF 3.0 中,此集合只能包含事件触发器,原因是因为 WPF 团队没有时间实现此支持。尝试将属性触发器(或数据触发器)添加到集合会在运行时引发异常。
属性值继承
“属性值继承”(简称“属性继承”)一词不指传统的面向对象类继承,而是指属性值在元素树中的流动。列表 3.4 显示了一个简单的例子,它通过显式设置其 FontSize
和 FontStyle
依赖属性来更新列表 3.1 中的 Window
。图 3.4 显示了此更改的结果。(请注意,由于其出色的 SizeToContent
设置,Window
会自动调整大小以适应所有内容!)

在根
Window
上设置了 FontSize
和 FontStyle
的“关于”对话框。
列表 3.4 在根 Window
上设置了字体属性的“关于”对话框
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
FontSize="30" FontStyle="Italic"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
在大多数情况下,这两个设置会一直向下流动到树,并被子级继承。这甚至会影响 Button
和 ListBoxItem
,它们在逻辑树中有三个层级。第一个 Label
的 FontSize
没有改变,因为它明确标记为 20
的 FontSize
,覆盖了 30
的继承值。然而,继承的 FontStyle
设置 Italic
会影响所有 Label
、ListBoxItem
和 Button
,因为它们都没有显式设置此项。
请注意,尽管 StatusBar
与其他控件一样支持这两个属性,但 StatusBar
中的文本不受这两个值的影响。属性值继承的行为在这样的情况下可能很微妙,原因有两个:
- 并非所有依赖属性都参与属性值继承。(在内部,依赖属性可以通过将
FrameworkPropertyMetadataOptions.Inherits
传递给DependencyProperty.Register
来选择加入继承。) - 可能有其他更高优先级的源正在设置属性值,如下一节所述。
在这种情况下,后一个原因是罪魁祸首。一些控件(如 StatusBar
、Menu
和 ToolTip
)在内部将其字体属性设置为匹配当前系统设置。这样,用户就可以通过控制面板以熟悉的方式控制其字体。结果可能令人困惑,因为这些控件会“吞噬”任何从元素树向下传递的继承。例如,如果您将列表 3.4 中的 StatusBar
的逻辑子级添加一个 Button
,那么它的 FontSize
和 FontStyle
将分别是 12
和 Normal
的默认值,与其他不在 StatusBar
中的 Button
不同。
注意 - 在其他地方的属性值继承 - 属性值继承最初被设计用于元素树,但它已被扩展到在其他一些上下文中使用。例如,值可以传递给某些看起来像 XML 意义上的子级的元素(因为 XAML 的属性元素语法),但它们在逻辑树或视觉树方面不是子级。这些伪子级可以是元素的触发器或任何属性(不仅仅是 Content
或 Children
)的值,只要它是派生自 Freezable
的对象。这可能听起来有些武断且未被充分记录,但其目的是使许多基于 XAML 的场景“正常工作”,而无需您去思考它。
支持多个提供者
WPF 包含许多强大的机制,它们独立地尝试设置依赖属性的值。如果没有一个明确定义的机制来处理这些不同的属性值提供者,系统就会有些混乱,属性值也可能不稳定。当然,正如其名称所示,依赖属性旨在以一致且有序的方式依赖于这些提供者。
图 3.5 说明了 WPF 为计算其最终值而为每个依赖属性运行的五步过程。由于依赖属性中内置的更改通知,此过程会自动发生。

计算依赖属性值的管道。
步骤 1:确定基本值
大多数属性值提供者都属于基本值计算。以下列表显示了可以设置大多数依赖属性值的八个提供者,按优先级从高到低排序:
- 本地值
- 样式触发器
- 模板触发器
- 样式设置器
- 主题样式触发器
- 主题样式设置器
- 属性值继承
- 默认值
您已经看到了一些属性值提供者,例如属性值继承。本地值严格来说是指对 DependencyObject.SetValue
的任何调用,但这通常是通过 XAML 或过程代码中的简单属性赋值来看到的(因为依赖属性的实现方式,如前面关于 Button.IsDefault
的示例所示)。默认值是指与依赖属性注册的初始值,该值自然具有最低优先级。其他涉及样式和模板的提供者将在第十章中进一步介绍。
这个优先级顺序解释了为什么列表 3.4 中 StatusBar
的 FontSize
和 FontStyle
未受属性值继承的影响。StatusBar
字体属性的设置是为了匹配系统设置,这是通过主题样式设置器(列表中的 #6)完成的。尽管这具有比属性值继承(列表中的 #7)更高的优先级,但您仍然可以使用任何具有更高优先级的机制(例如,在 StatusBar
上简单地设置本地值)来覆盖这些字体设置。
步骤 2:评估
如果步骤一中的值是表达式(派生自 System.Windows.Expression
的对象),那么 WPF 会执行一个特殊的评估步骤,将表达式转换为具体结果。在 WPF 3.0 版本中,只有在使用动态资源(在第八章“资源”中介绍)或数据绑定(第九章“数据绑定”的主题)时,表达式才会发挥作用。WPF 的未来版本可能会启用其他类型的表达式。
步骤 3:应用动画
如果一个或多个动画正在运行,它们就有能力改变当前属性值(使用步骤 2 之后的值作为输入)或完全替换它。因此,动画(第十三章的主题)可以胜过所有其他属性值提供者——甚至本地值!这通常是 WPF 新手的一个绊脚石。
步骤 4:强制转换
在所有属性值提供者都表达了他们的意见之后,WPF 会获取几乎最终的属性值,并将其传递给 CoerceValueCallback
委托(如果已与依赖属性注册)。回调负责根据自定义逻辑返回一个新值。例如,WPF 的内置控件(如 ProgressBar
)使用此回调将 Value
依赖属性限制在 Minimum
和 Maximum
值之间,如果输入值小于 Minimum
则返回 Minimum
,如果输入值大于 Maximum
则返回 Maximum
。
步骤 5:验证
最后,潜在强制转换的值会传递给 ValidateValueCallback
委托(如果已与依赖属性注册)。此回调必须返回 true
(如果输入值有效)或 false
(否则)。返回 false
会导致异常被抛出,从而取消整个过程。
提示 - 如果您无法弄清楚给定依赖属性的当前值来自何处,可以使用静态 DependencyPropertyHelper.GetValueSource
方法作为调试辅助。它返回一个 ValueSource
结构,其中包含一些数据:一个 BaseValueSource
枚举,它揭示了基本值来自何处(过程中的步骤 1),以及布尔值 IsExpression
、IsAnimated
和 IsCoerced
属性,它们提供了关于步骤 2-4 的信息。
当在列表 3.1 或 3.4 的 StatusBar
实例上使用 FontSize
或 FontStyle
属性调用此方法时,返回的 BaseValueSource
是 DefaultStyle
,表明该值来自主题样式设置器。(主题样式有时被称为默认样式。主题样式触发器的枚举值为 DefaultStyleTrigger
。)
不要在生产代码中使用此方法!WPF 的未来版本可能会破坏您对值计算的假设,而且根据源以不同方式处理属性值与 WPF 应用程序的预期工作方式背道而驰。
注意 - 清除本地值 - 前面的“更改通知”部分演示了使用过程代码将 Button
的 Foreground
更改为蓝色以响应 MouseEnter
事件,然后又在响应 MouseLeave
事件时将其改回为黑色。这种方法的问题在于,黑色是在 MouseLeave
中作为本地值设置的,这与 Button
的初始状态(其黑色 Foreground
来自其主题样式中的设置器)非常不同。如果更改了主题,并且新主题尝试更改默认 Foreground
颜色(或者具有更高优先级的其他提供者尝试这样做),它就会被本地设置的黑色所覆盖。
您可能想做的是清除本地值,让 WPF 从下一个最高优先级的相关提供者那里设置值。幸运的是,DependencyObject
提供了这种机制,其 ClearValue
方法。可以在 C# 中像这样对 Button
b
调用它:
b.ClearValue(Button.ForegroundProperty);
(Button.ForegroundProperty
是静态 DependencyProperty
字段。) 调用 ClearValue
后,当 WPF 重新计算基本值时,本地值将被简单地排除在外。
请注意,“更改通知”部分中的 IsMouseOver
属性上的触发器没有像事件处理程序实现那样的问题。触发器要么处于活动状态,要么处于非活动状态,当处于非活动状态时,它在属性值计算中会被简单地忽略。
附加属性
附加属性是一种特殊的依赖属性,可以有效地附加到任意对象。乍听之下这可能有些奇怪,但这种机制在 WPF 中有多种应用。
对于“关于”对话框示例,假设您宁愿不像列表 3.4 中那样为整个 Window
设置 FontSize
和 FontStyle
,而是将其设置在内部 StackPanel
上,以便它们仅由两个 Button
继承。然而,将属性属性移动到内部 StackPanel
元素不起作用,因为 StackPanel
本身没有字体相关属性!相反,您必须使用恰好定义在名为 TextElement
的类上的 FontSize
和 FontStyle
附加属性。列表 3.5 演示了这一点,引入了专门为附加属性设计的新 XAML 语法。这实现了所需的属性值继承,如图 3.6 所示。

“关于”对话框,其中
FontSize
和 FontStyle
通过从内部 StackPanel
继承被设置在两个 Button
上。
列表 3.5 在内部 StackPanel
上移动了字体属性的“关于”对话框
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel TextElement.FontSize="30" TextElement.FontStyle="Italic"
Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
在 StackPanel
元素中必须使用 TextElement.FontSize
和 TextElement.FontStyle
(而不是简单地 FontSize
和 FontStyle
),因为 StackPanel
没有这些属性。当 XAML 解析器或编译器遇到此语法时,它要求 TextElement
(有时称为附加属性提供程序)具有名为 SetFontSize
和 SetFontStyle
的静态方法,可以相应地设置值。因此,列表 3.5 中的 StackPanel
声明等同于以下 C# 代码:
StackPanel panel = new StackPanel();
TextElement.SetFontSize(panel, 30);
TextElement.SetFontStyle(panel, FontStyles.Italic);
panel.Orientation = Orientation.Horizontal;
panel.HorizontalAlignment = HorizontalAlignment.Center;
Button helpButton = new Button();
helpButton.MinWidth = 75;
helpButton.Margin = new Thickness(10);
helpButton.Content = "Help";
Button okButton = new Button();
okButton.MinWidth = 75;
okButton.Margin = new Thickness(10);
okButton.Content = "OK";
panel.Children.Add(helpButton);
panel.Children.Add(okButton);
请注意,像 FontStyles.Italic
、Orientation.Horizontal
和 HorizontalAlignment.Center
这样的枚举值,之前在 XAML 中仅被指定为 Italic
、Horizontal
和 Center
。这要归功于 .NET Framework 中的 EnumConverter
类型转换器,它可以转换任何不区分大小写的字符串。
尽管列表 3.5 中的 XAML 很好地表示了 FontSize
和 FontStyle
到 StackPanel
的逻辑附加,但 C# 代码表明这里没有真正的魔力;只是一个方法调用,将一个元素与一个实际上无关的属性关联起来。附加属性抽象的一个有趣之处在于,没有 .NET 属性属于它!
在内部,像 SetFontSize
这样的方法只是调用与普通依赖属性访问器相同的 DependencyObject.SetValue
方法,但作用于传入的 DependencyObject
而不是当前实例。
public static void SetFontSize(DependencyObject element, double value)
{
element.SetValue(TextElement.FontSizeProperty, value);
}
类似地,附加属性还定义一个静态的 Get
XXX 方法(其中 XXX 是属性的名称),该方法调用熟悉的 DependencyObject.GetValue
方法。
public static double GetFontSize(DependencyObject element)
{
return (double)element.GetValue(TextElement.FontSizeProperty);
}
与普通依赖属性的属性包装器一样,这些 Get
XXX 和 Set
XXX 方法除了调用 GetValue
和 SetValue
之外,不得执行任何其他操作。
注意 - 理解附加属性提供程序 - 列表 3.5 中使用的 FontSize
和 FontStyle
附加属性最令人困惑的部分是,它们不是由 Button
甚至 Control
(定义普通 FontSize
和 FontStyle
依赖属性的基类)定义的!相反,它们是由看似无关的 TextElement
类(以及 TextBlock
类,后者也可用于前面的示例)定义的。
这怎么可能有效呢,因为 TextElement.FontSizeProperty
是一个独立于 Control.FontSizeProperty
的 DependencyProperty
字段(并且 TextElement. FontStyleProperty
独立于 Control.FontStyleProperty
)?关键在于这些依赖属性的内部注册方式。如果您查看 TextElement
的源代码,您会看到类似以下内容:
TextElement.FontSizeProperty = DependencyProperty.RegisterAttached(
"FontSize", typeof(double), typeof(TextElement), new FrameworkPropertyMetadata(
SystemFonts.MessageFontSize, FrameworkPropertyMetadataOptions.Inherits |
FrameworkPropertyMetadataOptions.AffectsRender |
FrameworkPropertyMetadataOptions.AffectsMeasure),
new ValidateValueCallback(TextElement.IsValidFontSize));
这与前面注册 Button
的 IsDefault
依赖属性的示例类似,只是 RegisterAttached
方法优化了附加属性场景的属性元数据处理。
另一方面,Control
没有注册其 FontSize
依赖属性!相反,它调用 TextElement
已注册属性的 AddOwner
,从而获取对完全相同的实例的引用。
Control.FontSizeProperty = TextElement.FontSizeProperty.AddOwner(
typeof(Control), new FrameworkPropertyMetadata(SystemFonts.MessageFontSize,
FrameworkPropertyMetadataOptions.Inherits));
因此,所有控件继承的 FontSize
、FontStyle
和其他字体相关的依赖属性与 TextElement
公开的属性是相同的!
幸运的是,在大多数情况下,公开附加属性的类(例如 Get
XXX 和 Set
XXX 方法)与定义普通依赖属性的类是相同的,从而避免了这种混淆。
尽管“关于”对话框示例使用附加属性进行高级属性值继承,但附加属性最常用于用户界面元素的布局。(事实上,附加属性最初是为 WPF 的布局系统设计的。)各种 Panel
派生类定义了旨在附加到其子级的附加属性,用于控制它们的排列方式。这样,每个 Panel
都可以将其自己的自定义行为应用于任意子级,而无需所有可能的子元素都承担其自身的属性集。它还使得布局等系统易于扩展,因为任何人都可以编写具有自定义附加属性的新 Panel
。第六章“使用面板进行布局”和第十七章“使用自定义面板进行布局”中有所有细节。
注意 - 附加属性作为扩展机制 - 就像 Windows Forms 等早期技术一样,WPF 中的许多类都定义了 Tag
属性(类型为 System.Object
),用于存储每个实例的任意自定义数据。但附加属性是一种更强大、更灵活的机制,用于将自定义数据附加到任何派生自 DependencyObject
的对象。人们常常忽略附加属性可以让你有效地向密封类的实例(WPF 有很多这样的类!)添加自定义数据。
附加属性故事的进一步转折是,尽管在 XAML 中设置它们依赖于静态 Set
XXX 方法的存在,但在过程代码中,您可以绕过此方法并直接调用 DependencyObject.SetValue
。这意味着您可以在过程代码中将任何依赖属性用作附加属性。例如,以下代码行将 ListBox
的 IsTextSearchEnabled
属性附加到 Button
并为其分配一个值:
// Attach an unrelated property to a Button and set its value to true:
okButton.SetValue(ListBox.IsTextSearchEnabledProperty, true);
尽管这似乎毫无意义,而且肯定不会神奇地为这个 Button
启用新功能,但您有自由以您的应用程序或组件认为有意义的方式使用此属性值。
还有更有趣的方法可以以这种方式扩展元素。例如,FrameworkElement
的 Tag
属性是一个依赖属性,因此您可以将其附加到 GeometryModel3D
的实例(您将在第十二章“3D 图形”中再次看到的类,它是密封的且没有 Tag
属性),如下所示:
GeometryModel3D model = new GeometryModel3D();
model.SetValue(FrameworkElement.TagProperty, "my custom data");
这只是 WPF 提供无需传统继承即可实现扩展的一种方式。
路由事件
正如 WPF 在简单的 .NET 属性概念之上增加了更多基础结构一样,它也在简单的 .NET 事件概念之上增加了更多基础结构。路由事件是旨在与元素树良好配合的事件。当引发路由事件时,它可以沿着视觉树和逻辑树向上或向下传播,以简单一致的方式在每个元素上引发,而无需任何自定义代码。
事件路由有助于大多数应用程序忽略视觉树的细节(这对重构有利),并且对于 WPF 的元素组合成功至关重要。例如,Button
公开一个基于处理低级 MouseLeftButtonDown
和 KeyDown
事件的 Click
事件。然而,当用户在标准 Button
上按下鼠标左键时,他们实际上是在与其 ButtonChrome
或 TextBlock
视觉子级交互。由于事件沿着视觉树向上传播,Button
最终会看到事件并可以处理它。类似地,对于上一章中的 VCR 式停止 Button
,用户可能会直接按下鼠标左键在其 Rectangle
逻辑子级上。由于事件沿着逻辑树向上传播,Button
仍然会看到事件并可以处理它。(但是,如果您确实希望区分 Rectangle
上的事件与外部 Button
上的事件,您仍然可以这样做。)
因此,您可以将任意复杂的内容嵌入 Button
等元素中,或者给它一个任意复杂的视觉树(使用第十章中的技术),并且内部任何元素的鼠标左键单击仍然会引发父 Button
的 Click
事件。没有路由事件,内部内容的生产者或 Button
的消费者将不得不编写代码来修补所有内容。
路由事件的实现和行为与依赖属性有很多相似之处。与依赖属性的讨论一样,我们将首先看看一个简单的路由事件是如何实现的,以使内容更具体。然后,我们将检查路由事件的一些功能,并将其应用于“关于”对话框。
路由事件实现
在大多数情况下,路由事件看起来与普通 .NET 事件没有太大区别。与依赖属性一样,除了 XAML 之外,没有 .NET 语言能内在理解路由的含义。额外的支持基于少数 WPF API。
列表 3.6 演示了 Button
如何有效地实现其 Click
路由事件。(Click
实际上是由 Button
的基类实现的,但这对于本次讨论并不重要。)
就像依赖属性表示为具有传统 Property
后缀的公共静态 DependencyProperty
字段一样,路由事件表示为具有传统 Event
后缀的公共静态 RoutedEvent
字段。路由事件与依赖属性一样在静态构造函数中注册,并且定义了一个普通的 .NET 事件(或事件包装器)以实现更熟悉的过程代码使用以及在 XAML 中使用事件属性语法添加处理程序。与属性包装器一样,事件包装器在其访问器中除了调用 AddHandler
和 RemoveHandler
之外,不得执行任何操作。
列表 3.6 标准路由事件实现
public class Button : ButtonBase
{
// The routed event
public static readonly RoutedEvent ClickEvent;
static Button()
{
// Register the event
Button.ClickEvent = EventManager.RegisterRoutedEvent("Click",
RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(Button));
...
}
// A .NET event wrapper (optional)
public event RoutedEventHandler Click
{
add { AddHandler(Button.ClickEvent, value); }
remove { RemoveHandler(Button.ClickEvent, value); }
}
protected override void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
...
// Raise the event
RaiseEvent(new RoutedEventArgs(Button.ClickEvent, this));
...
}
...
}
这些 AddHandler
和 RemoveHandler
方法不是从 DependencyObject
继承的,而是从 System.Windows.UIElement
继承的,UIElement
是 Button
等元素的更高级别基类。(本章末尾将更深入地研究这个类层次结构。)这些方法将委托附加和分离到相应的路由事件。在 OnMouseLeftButtonDown
内部,调用 RaiseEvent
(也定义在基类 UIElement
上)并传入相应的 RoutedEvent
字段来引发 Click
事件。当前的 Button
实例(this
)被传递为事件的源元素。此列表未显示,但 Button
的 Click
事件也是响应 KeyDown
事件而引发的,以支持使用空格键或 Enter 键进行单击。
路由策略和事件处理程序
注册时,每个路由事件都会选择三种路由策略之一——事件引发在元素树中传播的方式。这些策略作为 RoutingStrategy
枚举的值公开:
- 隧道(Tunneling)——事件首先在根上引发,然后依次在树下的每个元素上引发,直到达到源元素(或者直到处理程序将事件标记为已处理并停止隧道)。
- 冒泡(Bubbling)——事件首先在源元素上引发,然后依次在树上的每个元素上引发,直到达到根(或者直到处理程序将事件标记为已处理并停止冒泡)。
- 直接(Direct)——事件仅在源元素上引发。这与普通的 .NET 事件行为相同,只是这类事件仍然可以参与特定于路由事件的机制,例如事件触发器。
路由事件的处理程序具有与通用 .NET 事件处理程序模式匹配的签名:第一个参数是 System.Object
,通常命名为 sender
,第二个参数(通常命名为 e
)是一个派生自 System.EventArgs
的类。传递给处理程序的 sender
参数始终是附加了处理程序的元素。e
参数是(或派生自)RoutedEventArgs
的实例,它是 EventArgs
的一个子类,公开了四个有用的属性:
- Source——在逻辑树中最初引发事件的元素。
- OriginalSource——在视觉树中最初引发事件的元素(例如,标准
Button
的TextBlock
或ButtonChrome
子级)。 - Handled——一个布尔值,可以设置为
true
来将事件标记为已处理。这正是停止任何隧道或冒泡的操作。 - RoutedEvent——实际的路由事件对象(例如
Button.ClickEvent
),当同一个处理程序用于多个路由事件时,这有助于识别引发的事件。
Source
和 OriginalSource
的存在使您能够处理更高级别的逻辑树或更低级别的视觉树。这种区别仅适用于鼠标事件等物理事件。对于与视觉树中的元素没有直接关系的更抽象事件(如由于其键盘支持而导致的 Click
),将传递相同的对象给 Source
和 OriginalSource
。
实际中的路由事件
UIElement
类为键盘、鼠标和触笔输入定义了许多路由事件。其中大部分是冒泡事件,但许多事件都配有隧道事件。隧道事件很容易识别,因为按照惯例,它们以 Preview
前缀命名。这些事件也按惯例,在它们的冒泡对应事件之前引发。例如,PreviewMouseMove
是一个在 MouseMove
冒泡事件之前引发的隧道事件。
注意 - 使用触笔事件 - 触笔(平板电脑使用的笔状设备)默认充当鼠标。换句话说,它的使用会引发 MouseMove
、MouseDown
和 MouseUp
等事件。这种行为对于触笔在未专门为平板电脑设计的程序中使用至关重要。但是,如果您想提供为触笔优化的体验,可以处理特定于触笔的事件,例如 StylusMove
、StylusDown
和 StylusUp
。触笔比鼠标可以执行更多“技巧”,正如其某些事件(没有鼠标对应事件)所示,例如 StylusInAirMove
、StylusSystemGesture
、StylusInRange
和 StylusOutOfRange
。然而,还有其他方法可以利用触笔而不直接处理这些事件。下一章,“WPF 控件简介”,将展示如何使用功能强大的 InkCanvas
元素来实现这一点。
为各种活动设置一对事件的想法是为了让元素有机会有效地取消或以其他方式修改即将发生的事件。按照惯例,WPF 的内置元素仅响应冒泡事件(当定义了冒泡和隧道事件对时),确保隧道事件名副其实地是“预览”。例如,假设您想实现一个 TextBox
,它将输入限制为特定模式或正则表达式(例如电话号码或邮政编码)。如果您处理 TextBox
的 KeyDown
事件,您最多只能删除已显示在 TextBox
中的文本。但是,如果您处理 TextBox
的 PreviewKeyDown
事件,您可以将其标记为“已处理”,以不仅停止隧道,还阻止 KeyDown
冒泡事件被引发。在这种情况下,TextBox
将永远不会收到 KeyDown
通知,并且当前字符将不会显示。
为了演示简单冒泡事件的使用,列表 3.7 使用了列表 3.1 的原始“关于”对话框,并向 Window
的 MouseRightButtonDown
事件附加了一个事件处理程序。列表 3.8 包含实现事件处理程序的 C# 代码隐藏文件。
列表 3.7 带有根窗口事件处理程序的“关于”对话框
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AboutDialog" MouseRightButtonDown="AboutDialog_MouseRightButtonDown"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
列表 3.8 列表 3.7 的代码隐藏文件
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Controls;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void AboutDialog_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
// Display information about this event
this.Title = "Source = " + e.Source.GetType().Name + ", OriginalSource = " +
e.OriginalSource.GetType().Name + " @ " + e.Timestamp;
// In this example, all possible sources derive from Control
Control source = e.Source as Control;
// Toggle the border on the source control
if (source.BorderThickness != new Thickness(5))
{
source.BorderThickness = new Thickness(5);
source.BorderBrush = Brushes.Black;
}
else
source.BorderThickness = new Thickness(0);
}
}
每当右键单击冒泡到 Window
时,AboutDialog_MouseRightButtonDown
处理程序都会执行两个操作:它将有关事件的信息打印到 Window
的标题栏,并围绕逻辑树中右键单击的特定元素添加(然后随后删除)一个粗黑边框。图 3.7 显示了结果。请注意,右键单击 Label
会显示 Source
设置为 Label
,而 OriginalSource
设置为它的 TextBlock
视觉子级。

修改后的“关于”对话框,在右键单击第一个
Label
后。
如果您运行此示例并右键单击所有内容,您会注意到两个有趣的现象:
- 当您右键单击任一
ListBoxItem
时,Window
永远不会收到MouseRightButtonDown
事件。那是因为ListBoxItem
在内部处理此事件以及MouseLeftButtonDown
事件(停止冒泡),以实现项目选择。 - 当您右键单击
Button
时,Window
会收到MouseRightButtonDown
事件,但设置Button
的Border
属性没有视觉效果。这是由于Button
的默认视觉树,它在图 3.3 中已显示。与Window
、Label
、ListBox
、ListBoxItem
和StatusBar
不同,Button
的视觉树中没有Border
元素。
处理鼠标中键按下事件的事件在哪里?- 如果您浏览 UIElement
或 ContentElement
公开的各种鼠标事件,您会找到 MouseLeftButtonDown
、MouseLeftButtonUp
、MouseRightButtonDown
和 MouseRightButtonUp
的事件(以及每种事件的隧道 Preview
版本)。但是,对于某些鼠标上的附加按钮呢?
可以通过更通用的 MouseDown
和 MouseUp
事件(它们也有 Preview
对应项)检索此信息。传递给这些事件处理程序的参数包括一个 MouseButton
枚举,指示哪个按钮的状态刚刚发生变化:Left
、Right
、Middle
、XButton1
或 XButton2
。相应的 MouseButtonState
枚举指示该按钮是 Pressed
(按下)还是 Released
(释放)。
注意 - 停止路由事件是一种错觉 - 尽管将路由事件处理程序中的 RoutedEventArgs
参数的 Handled
属性设置为 true
似乎会停止隧道或冒泡,但树上其他位置的单独处理程序可以选择继续接收事件!这只能通过过程代码完成,使用 AddHandler
的一个重载,该重载添加一个布尔值 handledEventsToo
参数。
例如,可以从列表 3.7 中删除事件属性,并将其替换为 AboutDialog
构造函数中的以下 AddHandler
调用:
public AboutDialog()
{
InitializeComponent();
this.AddHandler(Window.MouseRightButtonDownEvent,
new MouseButtonEventHandler(AboutDialog_MouseRightButtonDown), true);
}
当第三个参数传递 true
时,AboutDialog_MouseRightButtonDown
现在会在您右键单击 ListBoxItem
时收到事件,并且可以添加黑色边框!
您应该尽可能避免处理已处理的事件,因为事件被处理很可能有其原因。附加处理程序到事件的 Preview
版本是首选的替代方法。
底线是,停止隧道或冒泡实际上只是一种错觉。更准确地说,当路由事件被标记为已处理时,隧道和冒泡仍然继续,但默认情况下,事件处理程序只看到未处理的事件。
附加事件
路由事件的隧道和冒泡在每个元素都公开该事件时是自然的。但是 WPF 支持通过甚至不定义该事件的元素来隧道和冒泡路由事件!这得益于附加事件的概念。
附加事件的操作方式与附加属性非常相似(并且它们与隧道或冒泡的使用方式与附加属性与属性值继承的使用方式非常相似)。列表 3.9 再次更改了“关于”对话框,通过根 Window
直接处理其 ListBox
引发的冒泡 SelectionChanged
事件以及其两个 Button
引发的冒泡 Click
事件。由于 Window
没有自己的 SelectionChanged
或 Click
事件,因此事件属性名称必须加上定义这些事件的类的名称前缀。列表 3.10 包含实现两个事件处理程序的相应代码隐藏文件。两个事件处理程序都只是显示一个 MessageBox
,其中包含有关刚刚发生的事情的信息。
列表 3.9 在根窗口上带有两个附加事件处理程序的“关于”对话框
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AboutDialog" ListBox.SelectionChanged="ListBox_SelectionChanged"
Button.Click="Button_Click"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10">Help</Button>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
列表 3.10 列表 3.9 的代码隐藏文件
using System.Windows;
using System.Windows.Controls;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (e.AddedItems.Count > 0)
MessageBox.Show("You just selected " + e.AddedItems[0]);
}
void Button_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("You just clicked " + e.Source);
}
}
每个路由事件都可以用作附加事件。列表 3.9 中使用的附加事件语法是有效的,因为 XAML 编译器看到了定义在 ListBox
上的 SelectionChanged
.NET 事件和定义在 Button
上的 Click
.NET 事件。然而,在运行时,AddHandler
会被直接调用以将这两个事件附加到 Window
。因此,这两个事件属性等同于在 Window
的构造函数中放置以下代码:
public AboutDialog()
{
InitializeComponent();
this.AddHandler(ListBox.SelectionChangedEvent,
new SelectionChangedEventHandler(ListBox_SelectionChanged));
this.AddHandler(Button.ClickEvent, new RoutedEventHandler(Button_Click));
}
注意 - 整合路由事件处理程序 - 由于传递给路由事件的丰富信息,如果您真的愿意,您可以用一个顶级的“超级处理程序”来处理每个隧道或冒泡的事件!这个处理程序可以检查 RoutedEvent
对象来确定哪个事件被引发,将 RoutedEventArgs
参数强制转换为适当的子类(例如 KeyEventArgs
、MouseButtonEventArgs
等),然后继续进行。
例如,列表 3.9 可以修改为将 ListBox.SelectionChanged
和 Button.Click
分配给同一个 GenericHandler
方法,定义如下:
void GenericHandler(object sender, RoutedEventArgs e)
{
if (e.RoutedEvent == Button.ClickEvent)
{
MessageBox.Show("You just clicked " + e.Source);
}
else if (e.RoutedEvent == ListBox.SelectionChangedEvent)
{
SelectionChangedEventArgs sce = (SelectionChangedEventArgs)e;
if (sce.AddedItems.Count > 0)
MessageBox.Show("You just selected " + sce.AddedItems[0]);
}
}
这也要归功于 .NET Framework 2.0 版本中添加的委托逆变(contravariance)功能,它允许使用与预期参数的基类签名的方法来使用委托(例如,使用 RoutedEventArgs
而不是 SelectionChangedEventArgs
)。GenericHandler
只是在需要时强制转换 RoutedEventArgs
参数,以获取 SelectionChanged
事件特有的额外信息。
命令
WPF 提供了对命令的原生支持,命令是事件的一种更抽象、耦合度更低的替代形式。而事件与特定用户操作的细节相关联(例如,单击 Button
或选择 ListBoxItem
),命令则代表独立于其用户界面暴露的操作。命令的典型示例是剪切、复制和粘贴。应用程序通常同时通过多种机制公开这些操作:Menu
中的 MenuItem
、ContextMenu
上的 MenuItem
、ToolBar
上的 Button
、键盘快捷键等。
您可以用事件很好地处理命令(如剪切、复制和粘贴)的多种暴露。例如,您可以为这三个操作中的每一个定义一个通用的事件处理程序,然后将每个处理程序附加到相关元素上的相应事件(Button
上的 Click
事件、主 Window
上的 KeyDown
事件等)。此外,您可能希望在相应的操作无效时启用和禁用适当的控件(例如,当剪贴板上没有内容时禁用粘贴的任何用户界面)。这种双向通信变得有些麻烦,特别是如果您不想硬编码需要更新的控件列表。
幸运的是,WPF 对命令的支持旨在使此类场景非常容易。支持减少了您需要编写的代码量(在某些情况下消除了所有过程代码),并为您提供了更大的灵活性来更改用户界面而不破坏后端逻辑。命令不是 WPF 的新发明;像 Microsoft Foundation Classes (MFC) 这样的旧技术也有类似的机制。当然,即使您熟悉 MFC,WPF 中的命令也有其独特的特性需要学习。
命令的大部分功能来自以下三个特性:
- WPF 定义了许多内置命令。
- 命令具有对输入手势(如键盘快捷键)的自动支持。
- WPF 的一些控件具有与各种命令绑定的内置行为。
内置命令
命令是任何实现 ICommand
接口(来自 System.Windows.Input
)的对象,该接口定义了三个简单的成员:
- Execute——执行命令特定逻辑的方法。
- CanExecute——一个返回
true
(如果命令已启用)或false
(如果命令已禁用)的方法。 - CanExecuteChanged——当
CanExecute
的值更改时引发的事件。
如果您想创建剪切、复制和粘贴命令,您可以定义并实现三个实现 ICommand
的类,找到一个存储它们的位置(可能是您主 Window
的静态字段),从相关的事件处理程序调用 Execute
(当 CanExecute
返回 true
时),并处理 CanExecuteChanged
事件来切换相关用户界面上的 IsEnabled
属性。但这听起来比仅仅使用事件好不了多少。
幸运的是,Button
、CheckBox
和 MenuItem
等控件具有与您代表的任何命令交互的逻辑。它们公开一个简单的 Command
属性(类型为 ICommand
)。设置后,当这些控件的 Click
事件引发时(当 CanExecute
返回 true 时),它们会自动调用命令的 Execute
方法。此外,它们会自动通过利用 CanExecuteChanged
事件将 IsEnabled
的值与 CanExecute
的值同步。通过通过简单的属性赋值支持所有这些,所有这些逻辑都可以从 XAML 中获得。
更幸运的是,WPF 已经定义了很多命令,所以您不必实现 ICommand
对象来执行剪切、复制和粘贴,也不必担心在哪里存储它们。WPF 的内置命令通过五个不同的类公开为静态属性:
- ApplicationCommands——
Close
、Copy
、Cut
、Delete
、Find
、Help
、New
、Open
、Paste
、Print
、PrintPreview
、Properties
、Redo
、Replace
、Save
、SaveAs
、SelectAll
、Stop
、Undo
等。 - ComponentCommands——
MoveDown
、MoveLeft
、MoveRight
、MoveUp
、ScrollByLine
、ScrollPageDown
、ScrollPageLeft
、ScrollPageRight
、ScrollPageUp
、SelectToEnd
、SelectToHome
、SelectToPageDown
、SelectToPageUp
等。 - MediaCommands——
ChannelDown
、ChannelUp
、DecreaseVolume
、FastForward
、IncreaseVolume
、MuteVolume
、NextTrack
、Pause
、Play
、PreviousTrack
、Record
、Rewind
、Select
、Stop
等。 - NavigationCommands——
BrowseBack
、BrowseForward
、BrowseHome
、BrowseStop
、Favorites
、FirstPage
、GoToPage
、LastPage
、NextPage
、PreviousPage
、Refresh
、Search
、Zoom
等。 - EditingCommands——
AlignCenter
、AlignJustify
、AlignLeft
、AlignRight
、CorrectSpellingError
、DecreaseFontSize
、DecreaseIndentation
、EnterLineBreak
、EnterParagraphBreak
、IgnoreSpellingError
、IncreaseFontSize
、IncreaseIndentation
、MoveDownByLine
、MoveDownByPage
、MoveDownByParagraph
、MoveLeftByCharacter
、MoveLeftByWord
、MoveRightByCharacter
、MoveRightByWord
等。
这些属性中的每一个都不返回实现 ICommand
的唯一类型。相反,它们都是 RoutedUICommand
的实例,该类不仅实现了 ICommand
,而且支持像路由事件一样的冒泡。
“关于”对话框有一个“帮助”Button
,目前什么都不做,所以让我们通过附加 ApplicationCommands
中定义的 Help
命令来演示这些内置命令如何工作。假设 Button
名为 helpButton
,您可以在 C# 中将其与 Help
命令关联如下:
helpButton.Command = ApplicationCommands.Help;
所有 RoutedUICommand
对象都定义了一个 Text
属性,其中包含一个适合在用户界面中显示的命令名称。(此属性是 RoutedUICommand
和其基类 RoutedCommand
之间的唯一区别。)例如,Help
命令的 Text
属性(毫不奇怪)设置为字符串 Help
。因此,这个 Button
上硬编码的 Content
可以替换为:
helpButton.Content = ApplicationCommands.Help.Text;
提示 - 所有 RoutedUICommand
定义的 Text
字符串都会自动本地化到 WPF 支持的每种语言!这意味着当线程的当前 UI 文化代表西班牙语而不是英语时,Button
的 Content
分配给 ApplicationCommands. Help.Text
会自动显示“Ayuda”而不是“Help”。即使在您想公开图像而不是文本的情况下(可能是在 ToolBar
上),您仍然可以在其他地方利用此本地化字符串,例如在 ToolTip
中。
当然,您仍然负责本地化您在用户界面中显示的任何自己的字符串。利用命令上的 Text
可以简单地减少您需要翻译的术语数量。
如果您运行此更改后的“关于”对话框,您会发现 Button
现在已永久禁用。那是因为内置命令无法知道它们何时应该启用或禁用,甚至不知道执行它们时该采取什么操作。它们将此逻辑委托给命令的消费者。
要插入自定义逻辑,您需要将 CommandBinding
添加到将执行命令的元素或任何父元素(感谢路由命令的冒泡行为)。所有派生自 UIElement
(和 ContentElement
)的类都包含一个 CommandBindings
集合,该集合可以包含一个或多个 CommandBinding
对象。因此,您可以在其代码隐藏文件中将 Help
的 CommandBinding
添加到“关于”对话框的根 Window
,如下所示:
this.CommandBindings.Add(new CommandBinding(ApplicationCommands.Help,
HelpExecuted, HelpCanExecute));
这假设已定义名为 HelpExecuted
和 HelpCanExecute
的方法。这些方法将在适当的时候被调用,以插入 Help
命令的 CanExecute
和 Execute
方法的实现。
列表 3.11 和 3.12 最后一次修改了“关于”对话框,完全在 XAML 中将 Help Button
绑定到 Help
命令(尽管这两个处理程序必须在代码隐藏文件中定义)。
列表 3.11 “关于”对话框支持 Help 命令
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="AboutDialog"
Title="About WPF Unleashed" SizeToContent="WidthAndHeight"
Background="OrangeRed">
<Window.CommandBindings>
<CommandBinding Command="Help"
CanExecute="HelpCanExecute" Executed="HelpExecuted"/>
</Window.CommandBindings>
<StackPanel>
<Label FontWeight="Bold" FontSize="20" Foreground="White">
WPF Unleashed (Version 3.0)
</Label>
<Label>© 2006 SAMS Publishing</Label>
<Label>Installed Chapters:</Label>
<ListBox>
<ListBoxItem>Chapter 1</ListBoxItem>
<ListBoxItem>Chapter 2</ListBoxItem>
</ListBox>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button MinWidth="75" Margin="10" Command="Help" Content=
"{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<Button MinWidth="75" Margin="10">OK</Button>
</StackPanel>
<StatusBar>You have successfully registered this product.</StatusBar>
</StackPanel>
</Window>
列表 3.12 列表 3.11 的代码隐藏文件
using System.Windows;
using System.Windows.Input;
public partial class AboutDialog : Window
{
public AboutDialog()
{
InitializeComponent();
}
void HelpCanExecute(object sender, CanExecuteRoutedEventArgs e)
{
e.CanExecute = true;
}
void HelpExecuted(object sender, ExecutedRoutedEventArgs e)
{
System.Diagnostics.Process.Start("http://www.adamnathan.net/wpf");
}
}
Window
的 CommandBinding
可以在 XAML 中设置,因为它定义了一个默认构造函数并允许使用属性设置其数据。Button
的 Content
甚至可以设置为所选命令的 Text
属性,这要归功于第九章讨论的一个流行的数据绑定技术。此外,请注意,类型转换器简化了在 XAML 中指定 Help
命令。CommandConverter
类了解所有内置命令,因此 Command
属性可以设置为 Help
(而不是更冗长的 {x:Static ApplicationCommands.Help}
)。(自定义命令没有得到同样的特殊处理。)在代码隐藏文件中,HelpCanExecute
始终保持命令启用状态,而 HelpExecuted
会启动一个 Web 浏览器,其中包含适当的帮助 URL。
使用输入手势执行命令
在一个简单的对话框中使用 Help
命令可能显得有些多余,因为一个简单的 Click
事件处理程序就足够了,但命令提供了一个额外的优势(除了本地化文本):自动绑定到键盘快捷键。
应用程序通常在用户按下 F1 键时调用其帮助版本。果然,当显示列表 3.10 定义的对话框时按下 F1,Help
命令会自动启动,就好像您单击了 Help Button
一样!这是因为像 Help
这样的命令定义了一个默认的输入手势来执行该命令。您可以通过将 KeyBinding
和/或 MouseBinding
对象添加到相关元素的 InputBindings
集合来绑定自己的输入手势到命令。例如,要将 F2 分配为执行 Help
的键盘快捷键,您可以将以下语句添加到 AboutDialog
的构造函数中:
this.InputBindings.Add(
new KeyBinding(ApplicationCommands.Help, new KeyGesture(Key.F2)));
但这只会使 F1 和 F2 都执行 Help
。您还可以通过将 F1 绑定到一个特殊的 NotACommand
命令来禁止默认的 F1 行为,如下所示:
this.InputBindings.Add(
new KeyBinding(ApplicationCommands.NotACommand, new KeyGesture(Key.F1)));
这两个语句都可以用 XAML 表示为:
<Window.InputBindings>
<KeyBinding Command="Help" Key="F2"/>
<KeyBinding Command="NotACommand" Key="F1"/>
</Window.InputBindings>
具有内置命令绑定的控件
当您遇到它时,这似乎几乎是神奇的,但 WPF 中的一些控件包含自己的命令绑定。最简单的例子是 TextBox
控件,它为与剪贴板交互的 Cut
、Copy
和 Paste
命令以及 Undo
和 Redo
命令提供了自己的内置绑定。这不仅意味着 TextBox
响应标准的 Ctrl+X、Ctrl+C、Ctrl+V、Ctrl+Z 和 Ctrl+Y 键盘快捷键,而且其他元素也很容易参与这些操作。
以下独立 XAML 示例演示了这些内置命令绑定的强大功能:
<StackPanel xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Orientation="Horizontal" Height="25">
<Button Command="Cut" CommandTarget="{Binding ElementName=textBox}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<Button Command="Copy" CommandTarget="{Binding ElementName=textBox}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<Button Command="Paste" CommandTarget="{Binding ElementName=textBox}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<Button Command="Undo" CommandTarget="{Binding ElementName=textBox}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<Button Command="Redo" CommandTarget="{Binding ElementName=textBox}"
Content="{Binding RelativeSource={RelativeSource Self}, Path=Command.Text}"/>
<TextBox x:Name="textBox" Width="200"/>
</StackPanel>
您可以将此内容粘贴到 XamlPad 中,或将其保存为 .xaml 文件在 Internet Explorer 中查看,因为不需要过程代码。五个 Buttons 中的每一个都与一个命令相关联,并将其 Content 设置为每个命令的 Text 属性返回的字符串。唯一的新内容是设置每个 Button 的 CommandTarget
属性为 TextBox 的实例(再次使用第九章讨论的数据绑定功能)。这会导致命令从 TextBox 而不是 Button 执行,这对于它响应命令是必需的。
此 XAML 产生图 3.8 中的结果。当 TextBox
中没有选中文本时,前两个 Button
会自动禁用,当有选中文本时会自动启用。类似地,当剪贴板中有文本内容时,Paste Button
会自动启用,否则禁用。

得益于
TextBox
的内置绑定,五个 Button
在没有过程代码的情况下按预期工作。
Button
和 TextBox
彼此之间没有直接了解,但通过命令,它们可以实现丰富的交互。这就是为什么 WPF 冗长的内置命令列表如此重要的原因。第三方控件标准化 WPF 内置命令的越多,彼此之间没有直接了解的控件之间的交互就越无缝(和声明式)。
类层次结构之旅
WPF 的类具有非常深的继承层次结构,因此很难理解各种类及其关系的意义。本书的内封面包含这些类的映射图,以帮助您在遇到新类时将其置于透视图中。由于空间限制,它不完整,但涵盖了主要类。
有几个类是 WPF 内部工作的基础,在本书的进一步介绍之前值得简要解释。其中一些在前面已经提到过。图 3.9 显示了这些重要的类及其关系,没有内封面的所有额外杂乱。

WPF Presentation Framework 的核心类。
这十个类具有以下意义:
- Object——所有 .NET 类的基类。
- DispatcherObject——任何希望仅在其创建线程上被访问的对象的基础类。大多数 WPF 类都派生自
DispatcherObject
,因此本质上是线程不安全的。名称中的Dispatcher
部分指的是 WPF 版的类 Win32 消息循环,将在第七章“应用程序结构和部署”中进一步讨论。 - DependencyObject——任何支持依赖属性的对象的基础类。
DependencyObject
定义了GetValue
和SetValue
方法,这些方法是依赖属性操作的核心。 - Freezable——可以为了性能原因而被“冻结”为只读状态的对象的基础类。
Freezable
一旦被冻结,甚至可以安全地在多个线程之间共享,这与其他所有DispatcherObject
都不同。冻结的对象永远无法解冻,但您可以克隆它们来创建未冻结的副本。 - Visual——所有具有自身视觉表示的对象的基础类。
Visual
类将在第十一章中深入讨论。 - UIElement——所有具有路由事件、命令绑定、布局和焦点支持的视觉对象的基础类。
- ContentElement——一个类似于
UIElement
的基类,但用于没有自身渲染行为的内容片段。相反,ContentElement
被托管在一个派生自Visual
的类中以在屏幕上渲染。 - FrameworkElement——添加对样式、数据绑定、资源以及 Windows 控件的一些常见机制(如工具提示和上下文菜单)支持的基类。
- FrameworkContentElement——
FrameworkElement
内容的对应物。第十四章将研究 WPF 中的FrameworkContentElement
。 - Control—熟悉控件(如
Button
、ListBox
和StatusBar
)的基类。Control
为其FrameworkElement
基类添加了许多属性,如Foreground
、Background
和FontSize
。Control
还支持模板,可让您完全替换其视觉树,这在第 10 章中进行了讨论。下一章将深入探讨 WPF 的Control
。
在本书中,我们将简单地使用“元素”一词来指代派生自 UIElement
或 FrameworkElement
,有时也指代 ContentElement
或 FrameworkContentElement
的对象。UIElement
与 FrameworkElement
或 ContentElement
与 FrameworkContentElement
之间的区别并不重要,因为 WPF 不会提供 UIElement
和 ContentElement
的任何其他公共子类。
结论
在本章和前两章中,您已经了解了 WPF 构建在 .NET Framework 基础之上的所有主要方式。WPF 团队可以通过类似于 Windows Forms 的典型 .NET API 来公开其功能,并且仍然是一项有趣的技术。相反,该团队添加了几种基本概念,使广泛的功能能够以一种为开发人员和设计人员提供高生产力的方式进行公开。
事实上,当您专注于这些核心概念时(正如本章所做的那样),您会发现情况并不像以前那么简单:有多种属性类型、多种事件类型、多棵树,以及多种实现相同结果的方法(例如,编写声明式代码与过程式代码)!希望您现在能够体会到这些新机制的一些价值。在本书的其余部分,随着我们专注于完成特定的开发任务,这些概念通常会淡出背景。
由于本章中使用的(原始)示例,您现在应该对 WPF 的某些控件以及 WPF 用户界面的排列方式有所了解。接下来的三章将在此基础上,正式介绍 WPF 的控件和布局机制。
版权所有 © 2007 Pearson Education。保留所有权利。