Essential Windows Presentation Foundation (WPF) 摘录






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

53567
本章将阐述WPF的一些基本原则,并快速概述整个平台。你可以将本章视为本书其余部分的预览。
引言
Windows Presentation Foundation (WPF) 代表着用户界面技术向前迈出的重要一步。本章将阐述WPF的一些基本原则,并快速概述整个平台。你可以将本章视为本书其余部分的预览。
WPF 作为新的 GUI
在我们正式深入研究 WPF 之前,回顾一下我们所处的环境是很有趣的。
User32,查尔斯·佩佐尔德(Charles Petzold)版
任何使用 User32 编程的人,都曾在某个时刻读过佩佐尔德的《Windows 编程》系列书籍。它们都以一个类似的例子开始
#include <windows.h>
LRESULT CALLBACK WndProc(HWND hwnd,
UINT msg,
WPARAM wparam,
LPARAM lparam);
INT WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
LPSTR cmdline, int cmdshow) {
MSG msg;
HWND hwnd;
WNDCLASSEX wndclass = { 0 };
wndclass.cbSize = sizeof(WNDCLASSEX);
wndclass.style = CS_HREDRAW | CS_VREDRAW;
wndclass.lpfnWndProc = WndProc;
wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
wndclass.hbrBackground = (HBRUSH)GetStockObject(WHITE_BRUSH);
wndclass.lpszClassName = TEXT("Window1");
wndclass.hInstance = hInstance;
wndclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION);
RegisterClassEx(&wndclass);
hwnd = CreateWindow(TEXT("Window1"),
TEXT("Hello World"),
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT,
0,
CW_USEDEFAULT,
0,
NULL,
NULL,
hInstance,
NULL);
if( !hwnd )
return 0;
ShowWindow(hwnd, SW_SHOWNORMAL);
UpdateWindow(hwnd);
while( GetMessage(&msg, NULL, 0, 0) ) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg,
WPARAM wparam, LPARAM lparam) {
switch(msg) {
case WM_DESTROY:
PostQuitMessage(WM_QUIT);
break;
default:
return DefWindowProc(hwnd, msg, wparam, lparam);
}
return 0;
}
这相当于与 User32 对话时的“Hello World”。其中发生了一些非常有趣的事情。首先通过调用 RegisterClassEx
定义一个专用类型 (Window1
),然后实例化 (CreateWindow
) 并显示 (ShowWindow
)。最后,运行一个消息循环,让窗口接收用户输入和系统事件 (GetMessage
、TranslateMessage
和 DispatchMessage
)。这个程序自 Windows 1.0 时代 User 最初引入以来,基本没有改变。
Windows Forms 采用了这种复杂的编程模型,并在系统之上生成了一个干净的、托管的、对象模型,使其编程变得简单得多。Hello World 在 Windows Forms 中只需十行代码即可编写
using System.Windows.Forms;
using System;
class Program {
[STAThread]
static void Main() {
Form f = new Form();
f.Text = "Hello World";
Application.Run(f);
}
}
WPF 的主要目标是尽可能多地保留开发人员的知识。尽管 WPF 是一个与 Windows Forms 完全不同的新演示系统,但我们可以用非常相似的代码1编写等效的 WPF 程序(更改部分以粗体显示)
using System.Windows;
using System;
class Program {
[STAThread]
static void Main() {
Window f = new Window();
f.Title = "Hello World";
new Application().Run(f);
}
}
在这两种情况下,对 Application 对象的 Run 调用都替代了消息循环,并且标准 CLR(公共语言运行时)类型系统用于定义实例和类型。Windows Forms 实际上是 User32 之上的一个托管层,因此它仅限于 User32 提供的基本功能。
User32 是一个出色的 2D 控件平台。它基于按需、基于裁剪的绘图系统;也就是说,当需要显示一个控件时,系统会回调用户代码(按需)在它保护的边界框内(带裁剪)进行绘图。基于裁剪的绘图系统的优点在于它们速度快;不会浪费内存来缓冲控件内容,也不会浪费任何周期来绘制除已更改控件之外的任何内容。
按需、基于剪辑的绘图系统的缺点主要与响应性和组合有关。在第一种情况下,因为系统必须回调用户代码来绘制任何内容,所以通常一个组件可能会阻止其他组件进行绘制。当应用程序挂起并变白,或者停止正确绘制时,这个问题在 Windows 中很明显。在第二种情况下,要使单个像素受两个组件影响极其困难,然而这种能力在许多场景中是可取的——例如,部分不透明度、抗锯齿和阴影。
当 Windows Forms 控件重叠时,这个系统的缺点就变得清晰可见(图 1.1)。当控件重叠时,系统需要剪辑每个控件。请注意 图 1.1 中“linkLabel1”一词周围的灰色区域。

Windows Forms 控件重叠。请注意,每个控件都会遮挡其他控件。
WPF 基于保留模式合成系统。对于每个组件,都会维护一个绘图指令列表,允许系统自动渲染任何控件的内容,而无需与用户代码交互。此外,该系统使用**画家算法**实现,确保重叠的控件从后往前绘制,允许它们相互叠加。该模型允许系统管理图形资源,其方式与 CLR 管理内存的方式大致相同,以实现一些出色的效果。系统可以执行高速动画,将绘图指令发送到另一台机器,甚至将显示投影到 3D 表面——所有这些都无需控件了解其复杂性。
要查看这些效果,请比较 图 1.1 和 1.2。在 图 1.2 中,所有 WPF 控件的不透明度都设置为部分透明,甚至对背景图像也是如此。
WPF 的合成系统,其核心是一个基于矢量的系统,这意味着所有绘图都是通过一系列线条完成的。图 1.3 展示了矢量图形与传统栅格图形的比较。
该系统还支持完整的变换模型,包括缩放、旋转和倾斜。正如 图 1.4 所示,任何变换都可以应用于任何控件,即使在保持控件可交互和可用状态下,也能产生奇特的效果。
请注意,在 User32 和 GDI32 开发时,并没有容器嵌套的概念。设计原则是,一个扁平的子元素列表存在于一个父窗口之下。这个概念对于 1990 年代的简单对话框来说运行良好,但当今复杂的 UX 需要嵌套。这个问题的最简单例子是 GroupBox 控件。在 User32 设计中,GroupBox 位于控件后面,但不包含它们。Windows Forms 确实支持嵌套,但该功能揭示了底层 User32 控件模型存在的许多问题。

WPF 控件重叠,不透明度设置为半透明。请注意,所有组合在一起的控件,包括背景图像,都是可见的。

矢量图形和栅格图形的比较。请注意,放大矢量图形不会降低其清晰度。

应用了各种转换的 WPF 控件。尽管进行了转换,这些控件仍然功能齐全。
在 WPF 的合成引擎中,所有控件都被包含、分组和合成。WPF 中的一个按钮实际上是由几个较小的控件组成的。这种拥抱合成的举动,加上基于矢量的方法,实现了任何级别的包含(图 1.5)。

WPF 控件通过组合和包含构建。此处显示的按钮同时包含文本和图像。
要真正领略这种组合的力量,请查看 图 1.6。在所示的最大缩放比例下,整个圆形在原始按钮上仅占不到一个像素。该按钮实际上包含一个矢量图像,该图像又包含一个完整的文本文档,该文档又包含一个按钮,该按钮又包含另一个图像。
除了解决 User32 和 GDI32 的局限性外,WPF 的目标之一是将 Web 编程模型中的许多最佳功能引入 Windows 开发人员。

组合的力量,通过放大图 1.5中所示的复合按钮得以体现。
HTML,又名 Web
Web 开发最大的优势之一是创建内容的简单入口。最基本的 HTML“程序”实际上不过是文本文件中的几个 HTML 标签
<html>
<head>
<title>Hello World</title>
</head>
<body>
<p>Welcome to my document!</p>
</body>
</html>
事实上,所有这些标签都可以省略,我们只需创建一个包含文本“Welcome to my document!”的文件,将其命名为 *<something>.html*,并在浏览器中查看(图 1.7)。这个极低的入门门槛使数百万从未想过自己能编程的人成为了开发者。
在 WPF 中,我们可以使用一种名为 **XAML**(可扩展应用程序标记语言,发音为“zammel”)的新标记格式来实现相同的功能。由于 XAML 是 XML 的一种方言,它需要稍微严格的语法。最明显的要求可能是必须使用 xmlns 指令将命名空间与每个标签关联起来
<FlowDocument
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<Paragraph>Welcome to my document!</Paragraph>
</FlowDocument>

在 Internet Explorer 中显示一个简单的 HTML 文档

在 Internet Explorer 中显示 WPF 文档
您可以通过双击 <something>.xaml
来查看文件(图 1.8)。
当然,我们可以在这个简单的标记中充分利用 WPF 的所有强大功能。我们可以使用标记轻松实现 图 1.5 中的按钮显示,并在浏览器中显示它(图 1.9)。
HTML 模型的一大局限性在于它实际上只适用于创建托管在浏览器中的应用程序。使用 XAML 标记,我们可以将其用作松散的标记格式并托管在浏览器中,正如我们刚刚看到的,或者我们可以将其编译成应用程序,并使用标记创建标准 Windows 应用程序(图 1.10)。

在 Internet Explorer 中使用 WPF 控件和布局显示 WPF 文档

运行用 XAML 编写的应用程序。该程序可以在顶级窗口中运行,也可以托管在浏览器中。
<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World!'>
<Button>Hello World!</Button>
</Window>
HTML 中的编程能力有三种形式:声明式、脚本和服务器端。**声明式编程**是许多人认为不属于编程的范畴。我们可以使用简单的标记标签(如 <form />
)在 HTML 中定义行为,这些标签允许我们执行操作(通常是将数据回传到服务器)。**脚本编程**允许我们使用 JavaScript 对 HTML 文档对象模型 (DOM) 进行编程。脚本编程变得越来越流行,因为现在有足够多的浏览器支持通用的脚本模型,使得脚本可以在任何地方运行。**服务器端编程**允许我们在服务器上编写与用户交互的逻辑(在 Microsoft 平台中,这意味着 ASP.NET 编程)。
ASP.NET 提供了一种非常好的方式来生成 HTML 内容。通过使用 Repeater、数据绑定和事件处理程序,我们可以编写简单的服务器端代码来创建简单的应用程序。一个更简单的例子是简单的标记注入
<%@ Page %>
<html>
<body>
<p><%=DateTime.Now().ToString()%></p>
</body>
</html>
ASP.NET 的真正强大之处在于其丰富的服务器控件和服务库。使用像 DataGrid 这样的单个控件,我们可以生成大量的 HTML 内容;通过会员资格等服务,我们可以轻松创建具有身份验证的网站。
这种模型的主要限制是需要在线。现代应用程序预计可以在离线或偶尔连接的场景中运行。WPF 吸收了 ASP.NET 的许多功能——例如,repeater 和数据绑定——并将其提供给 Windows 开发人员,同时还增加了离线运行的能力。
WPF 的主要目标之一是将 Windows 开发和 Web 模型的最佳功能结合起来。在我们查看 WPF 的功能之前,了解 .NET Framework 3.0 中的新编程模型:XAML 至关重要。
XAML 编程模型简述
.NET 3.0 的主要(且经常被误解的)特性之一是新的 XAML 编程模型。XAML 在原始 XML 之上提供了一组语义,从而实现了共同的解释。稍微简化一下,XAML 是用于 CLR 对象的基于 XML 的实例化脚本。存在从 XML 标签到 CLR 类型以及从 XML 属性到 CLR 属性和事件的映射。以下示例展示了在 XAML 和 C# 中创建对象并设置属性
<!-- XAML version -->
<MyObject
SomeProperty='1' />
// C# version
MyObject obj = new MyObject();
obj.SomeProperty = 1;
XML 标签始终在命名空间的上下文中定义。该命名空间决定了哪些标签有效。在 XAML 中,我们将 XML 命名空间映射到 CLR 命名空间和程序集的集合。要使刚刚说明的简单示例生效,我们需要映射所需的命名空间。在 XML 中,我们使用 xmlns 属性来定义新的命名空间
<!-- XAML version --> <MyObject xmlns='clr-namespace:Samples' SomeProperty='1' />
// C# version
using Samples;
MyObject obj = new MyObject();
obj.SomeProperty = 1;
在 C# 中,找到类型的程序集列表总是由项目文件或 `csc.exe` 的命令行参数决定。在 XAML 中,我们可以为每个命名空间指定源程序集的位置
<!-- XAML version -->
<MyObject
xmlns='clr-namespace:Samples;assembly=samples.dll'
SomeProperty='1' />
// C# version
csc /r:samples.dll test.cs
using Samples;
MyObject obj = new MyObject();
obj.SomeProperty = 1;
在 XML 中,世界分为两个空间:元素和属性。就对象、属性和事件而言,XAML 模型与 CLR 更紧密地对齐。属性值编码为属性或子元素是灵活的。我们可以使用子元素重写前面的示例
<MyObject
xmlns='clr-namespace:Samples;assembly=samples.dll'>
<MyObject.SomeProperty>
1
</MyObject.SomeProperty>
</MyObject>
每个属性元素都由定义该属性的类型限定,允许属性包含任意复杂的结构化数据。例如,假设我们有第二个属性,它接受一个具有 FirstName 和 LastName 属性的 Person 对象。我们可以使用属性元素语法轻松地在 XAML 中编写代码
<MyObject
xmlns='clr-namespace:Samples;assembly=samples.dll'>
<MyObject.Owner>
<Person FirstName='Chris' LastName='Anderson' />
</MyObject.Owner>
</MyObject>
XAML 的创建是为了成为一种与 CLR 良好集成并提供丰富工具支持的标记语言。次要目标是创建一种易于阅读和编写的标记格式。将平台的功能首先为工具优化,然后为人类优化,这可能听起来有点粗鲁,但 WPF 团队坚信 WPF 应用程序通常会在 Microsoft Visual Studio 或 Microsoft Expression 等可视化设计工具的帮助下编写。为了在工具和人类之间取得平衡,WPF 允许类型作者定义一个属性作为内容属性。2
在我们的例子中,如果我们将 `MyObject` 的 Owner 属性设为内容属性,3 那么标记可以更改为省略属性元素标签
<MyObject
xmlns='clr-namespace:Samples;assembly=samples.dll'>
<Person FirstName='Megan' LastName='Anderson' />
</MyObject>
为了进一步提高可读性,XAML 有一个称为**标记扩展**的功能。这是一种通用方法,用于扩展标记解析器以生成更简单的标记。标记扩展作为 CLR 类型实现,它们的工作方式几乎与 CLR 属性定义完全相同。标记扩展用花括号 `{ }` 括起来。例如,要将属性值设置为特殊值 null,我们可以使用内置的 Null 标记扩展
<MyObject
xmlns='clr-namespace:Samples;assembly=samples.dll'>
<Person FirstName='Megan' LastName='{x:Null}' />
</MyObject>
表 1.1 列出了所有内置的 XAML 功能。
表 1.1:内置 XAML 功能
XAML 命名空间指令 | 含义 | 示例 |
x:Array |
创建 CLR 数组。 |
<x:Array Type='{x:Type Button}'>
<Button />
<Button />
</x:Array>
|
x:Class
|
指定要定义的类型名称(仅用于标记编译)。 |
<Window
x:Class='MyNamespace.MyClass'>...
</Window>
|
x:ClassModifier
|
指定要定义的类型的修饰符(“public”、“internal”等)(仅用于标记编译)。 |
<Window x:Class='...'
x:ClassModifier='Public'>
...
</Window>
|
x:Code
|
划定一个内联代码块(仅用于标记编译)。 |
<Window x:Class='...'>
<x:Code>
public void DoSomething() {
...
}
</x:Code>
...
</Window>
|
x:Key
|
指定用于元素的键(仅支持字典中包含的元素)。 |
<Button>
<Button.Resources>
<Style x:Key='Hi'>...</Style>
</Button.Resources>
</Button>
|
x:Name
|
指定元素的编程名称(通常用于元素没有内置名称属性时)。 |
<sys:Int32
xmlns:sys='clr-namespace:
System;...'
x:Name='_myIntegerValue'>
5</sys:Int32>
|
x:Null
|
创建空值。 |
<Button Content='{x:Null}' />
|
x:Static
|
通过访问类型的静态字段或属性来创建值。 |
<Button
Command='{x:Static
ApplicationCommands.Close}' />
|
x:Subclass
|
为不支持部分类型的语言提供标记编译的基类型。 | |
x:Type
|
提供 CLR 类型(等同于 Type.GetType)。 |
<ControlTemplate
TargetType='{x:Type Button}'>
...
</ControlTemplate>
|
x:TypeArguments
|
指定实例化泛型类型的泛型类型参数。 |
<gc:List
xmlns:gc='clr-
namespace:System.Collections.
Generic;...'
x:TypeArguments='{x:Type Button}' />
|
x:XData
|
划定一个内联 XML 块;只能用于 IXmlSerializable 类型的属性。 |
<XmlDataSource>
<x:XData>
<Book xmlns='' Title='...' />
</x:XData>
</XmlDataSource>
|
标记扩展的解析方式与对象标签完全相同,这意味着我们必须声明“x”XML 前缀才能解析此标记。XAML 为处理解析器内置类型定义了一个特殊命名空间
<MyObject
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='clr-namespace:Samples;assembly=samples.dll'>
<Person FirstName='Megan' LastName='{x:Null}' />
</MyObject>
任何 CLR 程序集(或程序集集合)也可以为 CLR 命名空间和程序集的集合定义基于 URI 的名称。这相当于 C/C++ 开发人员所知的旧式 `#include 'windows.h'` 语句。WPF 程序集使用此机制,因此我们可以使用任何一种格式将 WPF 导入 XAML 文件
<!-- option 1: import by CLR namespace -->
<Window
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns=
'clr-namespace:System.Windows;assembly=presentationframework.dll'>
</Window>
<!-- option 2: import by URI -->
<Window
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
</Window>
基于 URI 的方法的好处在于它导入了多个 CLR 命名空间和程序集,这意味着您的标记更紧凑,更容易使用。
XAML 的最后一个功能是能够使用其他类型提供的属性来扩展类型;我们将此功能称为**附加属性**。实际上,附加属性只是 JavaScript 扩展属性的类型安全版本。在 WPF 版本的 XAML 中,附加属性仅在定义属性的类型和其属性正在设置的类型都派生自 `DependencyObject` 类型时才起作用,但 XAML 规范没有此要求。
在下面的示例中,属性 `Dock` 由类型 `DockPanel` 定义。附加属性总是以提供属性的类型名称为前缀,即使它们作为属性出现也是如此
<Window
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<DockPanel>
<Button DockPanel.Dock='Top'>Top</Button>
<Button>
<DockPanel.Dock>Left</DockPanel.Dock>
Left
</Button>
<Button>Fill</Button>
</DockPanel>
</Window>
XAML 是一种相当简单的语言,规则相对较少。在 .NET Framework 3.0 版本的 XAML 中,所有 XAML 标签的定义都在 CLR 类型中——目标是确保我们可以在标记中做的任何事情也可以在代码中实现。在本书中,我将根据哪种方式更能轻松演示特定概念,在标记4和代码之间来回切换。
现在我们已经掌握了 XAML 的基础知识,我们可以开始研究 WPF 本身的主要部分。
WPF 概述
当我开始写这本书时,我想让它尽可能短,但不能再短(向爱因斯坦博士致歉)。即使秉持这种理念,我仍然想为您,读者,提供一个平台的快速概述,为您提供入门所需的所有基本概念。
启动并运行
有很多方法可以接触 WPF:通过浏览器,通过标记,或者通过代码。我编程太久了,所以忍不住从一个简单的 C# 程序开始。每个 WPF 应用程序都从创建一个 Application 对象开始。Application 对象控制应用程序的生命周期,并负责向运行中的程序传递事件和消息。
除了 Application 对象,大多数程序都希望向人显示一些东西。在 WPF 中,这意味着创建一个窗口。5 我们已经看到了基本的 WPF 应用程序源代码,所以这应该不足为奇
using System.Windows;
using System;
class Program {
[STAThread]
static void Main() {
Application app = new Application();
Window w = new Window();
w.Title = "Hello World";
app.Run(w);
}
}
要编译此代码,我们需要调用 C# 编译器。我们有两个选项;第一个是直接在命令行上调用 C# 编译器。我们必须包含三个引用程序集才能针对 WPF 进行编译。用于构建 WPF 应用程序的工具的位置取决于它们的安装方式。以下示例展示了如果安装了 .NET Framework 3.0 SDK 并且我们在提供的构建窗口中运行,如何编译此程序
csc /r:"%ReferenceAssemblies%"\WindowsBase.dll
/r:"%ReferenceAssemblies%"\PresentationCore.dll
/r:"%ReferenceAssemblies%"\PresentationFramework.dll
/t:winexe
/out:bin\debug\tour.exe
program.cs
直接使用 C# 编译对于单个文件和少量引用非常有效。然而,更好的选择是使用 .NET Framework 3.0 SDK 和 Visual Studio 2005 中包含的新构建引擎:MSBuild。创建 MSBuild 项目文件相对简单。这里我们将命令行转换为项目文件
<Project
DefaultTargets='Build'
xmlns='http://schemas.microsoft.com/developer/msbuild/2003'>
<PropertyGroup>
<Configuration>Debug</Configuration>
<Platform>AnyCPU</Platform>
<RootNamespace>Tour</RootNamespace>
<AssemblyName>Tour</AssemblyName>
<OutputType>winexe</OutputType>
<OutputPath>.\bin\Debug\</OutputPath>
</PropertyGroup>
<ItemGroup>
<Reference Include='System' />
<Reference Include='WindowsBase' />
<Reference Include='PresentationCore' />
<Reference Include='PresentationFramework' />
</ItemGroup>
<ItemGroup>
<Compile Include='program.cs' />
</ItemGroup>
<Import Project='$(MSBuildBinPath)\Microsoft.CSharp.targets' />
<Import Project='$(MSBuildBinPath)\Microsoft.WinFX.targets' />
</Project>
要编译应用程序,我们现在可以在命令行调用 MSBuild
msbuild tour.csproj
运行应用程序将显示 图 1.11 所示的窗口。
随着我们的程序启动并运行,我们可以思考如何构建一些有趣的东西。WPF 中最明显的变化之一(至少对于开发人员社区而言)是标记在平台中的深度集成。使用 XAML 构建应用程序通常要简单得多。
转向标记
要使用标记构建我们的程序,我们将首先定义 Application 对象。我们可以创建一个名为 *App.xaml* 的新 XAML 文件,内容如下
<Application xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' />

应用程序中创建的空窗口
和以前一样,运行它并没有什么特别有趣的地方。我们可以使用 Application 的 `MainWindow` 属性来定义一个窗口
<Application
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'>
<Application.MainWindow>
<Window Title='Hello World' Visibility='Visible' />
</Application.MainWindow>
</Application>
要编译此代码,我们需要更新项目文件以包含应用程序定义
<Project ...>
...
<ItemGroup>
<ApplicationDefinition Include='app.xaml' />
</ItemGroup>
...
</Project>
如果我们现在构建,我们会得到一个错误,因为通过包含我们的应用程序定义,我们自动定义了一个与现有 *program.cs* 冲突的“Main”函数。因此,我们可以从项目项列表中删除 *program.cs*,只剩下应用程序定义。此时,运行应用程序会产生与 图 1.11 所示完全相同的结果。
通常,我们会在单独的 XAML 文件中定义新的类型,而不是在应用程序定义中定义窗口。我们可以将窗口定义移动到一个单独的文件 `MyWindow.xaml` 中
<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
</Window>
然后我们可以更新应用程序定义以引用此标记
<Application
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
StartupUri='MyWindow.xaml'
/>
最后,我们需要将窗口添加到项目文件中。对于任何已编译的标记(应用程序定义除外),我们都使用 Page 构建类型
<Project ...>
...
<ItemGroup>
<Page Include='mywindow.xaml' />
<ApplicationDefinition Include='app.xaml' />
</ItemGroup>
...
</Project>
现在我们有了一个基本程序,运行良好,结构合理,准备好探索 WPF。
基础知识
WPF 中的应用程序由许多组合在一起的控件组成。我们已经看到的 Window 对象就是其中一个控件的第一个例子。其中一个更熟悉的控件是 Button
<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<Button>Howdy!</Button>
</Window>
运行这段代码会产生类似 图 1.12 的结果。这里首先要注意的有趣之处在于,按钮会自动填充窗口的整个区域。如果窗口大小调整,按钮会继续填充该空间。

窗口中的一个简单按钮
WPF 中的所有控件都具有某种类型的布局。在窗口的布局中,单个子控件会填充窗口。要在窗口中放置多个控件,我们需要使用某种类型的容器控件。WPF 中一种非常常见的容器控件是**布局面板**。
布局面板接受多个子元素并强制执行某种布局策略。最简单的布局可能是堆栈
<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<StackPanel>
<Button>Howdy!</Button>
<Button>A second button</Button>
</StackPanel>
</Window>
StackPanel
的工作方式是将控件一个接一个地堆叠起来(如 图 1.13 所示)。
WPF 中包含了更多控件和更多布局(当然,您也可以构建新的)。要查看其他一些控件,我们可以将它们添加到我们的标记中

<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<StackPanel>
<Button>Howdy!</Button>
<Button>A second button</Button>
<TextBox>An editable text box</TextBox>
<CheckBox>A check box</CheckBox>
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</StackPanel>
</Window>
运行此代码显示您可以与所有控件进行交互(图 1.14)。
为了查看不同的布局,我们可以替换 `StackPanel`。这里我们换成 `WrapPanel`
<Window
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<WrapPanel>
<Button>Howdy!</Button>
<Button>A second button</Button>
<TextBox>An editable text box</TextBox>
<CheckBox>A check box</CheckBox>
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
</Window>

窗口中添加了更多控件
运行此代码会发现控件的布局有显著差异(图 1.15)。

折叠面板中的多个控件
既然我们已经看到了一些控件,那么让我们编写一些与控件交互的代码。将标记文件与代码关联需要几个步骤。首先我们必须为标记文件提供一个类名
<Window
x:Class='EssentialWPF.MyWindow'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<WrapPanel>
<Button>Howdy!</Button>
<Button>A second button</Button>
<TextBox>An editable text box</TextBox>
<CheckBox>A check box </CheckBox>
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
</Window>
通常还会使用 C# 2.0 的部分类型功能,将一些额外的代码与标记文件关联起来。要定义代码隐藏文件,我们需要创建一个与我们在标记文件中指定的名称6相同的 C# 类。我们还必须从类的构造函数中调用 InitializeComponent
:7
using System;
using System.Windows.Controls;
using System.Windows;
namespace EssentialWPF {
public partial class MyWindow : Window {
public MyWindow() {
InitializeComponent();
}
}
}
为了完成我们的代码与标记的关联,我们需要更新项目文件以包含新定义的 C# 文件
<Project ...>
...
<ItemGroup>
<Compile Include='mywindow.xaml.cs' />
<Page Include='mywindow.xaml' />
<ApplicationDefinition Include='app.xaml' />
</ItemGroup>
...
</Project>
因为我们的代码没有做任何有趣的事情,所以运行程序时没什么可看的。代码隐藏文件和标记文件之间最常见的链接是事件处理程序。控件通常公开一个或多个事件,可以在代码中处理这些事件。处理事件只需在标记文件中指定事件处理程序方法名称
<Window
x:Class='EssentialWPF.MyWindow'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<WrapPanel>
<Button Click='HowdyClicked'>Howdy!</Button>
<Button>A second button</Button>
<TextBox>An editable text box</TextBox>
<CheckBox>A check box </CheckBox>
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
</Window>
然后我们可以在代码隐藏文件中实现该方法
using System;
using System.Windows.Controls;
using System.Windows;
namespace EssentialWPF {
public partial class MyWindow : Window {
public MyWindow() {
InitializeComponent();
}
void HowdyClicked(object sender, RoutedEventArgs e) {
}
}
}
要从代码隐藏文件访问任何控件,我们必须为该控件提供一个名称
<Window
x:Class='EssentialWPF.MyWindow'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<WrapPanel>
<Button Click='HowdyClicked'>Howdy!</Button>
<Button>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox>A check box </CheckBox>
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
</Window>
然后我们可以在代码隐藏文件中使用指定的名称
using System;
using System.Windows.Controls;
using System.Windows;
namespace EssentialWPF {
public partial class MyWindow : Window {
public MyWindow() {
InitializeComponent();
}
void HowdyClicked(object sender, RoutedEventArgs e) {
_text1.Text = "Hello from C#";
}
}
}
运行此应用程序并单击**Howdy!**按钮,将显示类似 图 1.16 的内容。
除了控件、布局和事件的基础知识之外,最常见的事情可能是让应用程序与数据交互。
处理数据
WPF 对数据和数据绑定有很深的依赖。看看最基本的控件之一,会发现多种类型的绑定
Button b = new Button();
b.Content = "Hello World";
这里至少发生了三种类型的绑定。首先,按钮的显示方式由一种绑定类型决定。每个控件都有一个 Resources 属性,它是一个字典,可以包含样式、模板或任何其他类型的数据。然后控件可以绑定到这些资源。
其次,按钮内容的类型是 System.Object
。按钮可以接受任何数据并显示它。WPF 中的大多数控件都利用了所谓的**内容模型**,其核心是实现丰富的内容和数据呈现。例如,我们可以创建包含几乎任何内容的按钮,而不仅仅是字符串。
第三,按钮显示和核心内容模型的基本实现都使用数据绑定来连接控件属性与显示元素。

点击按钮导致另一个元素发生变化
为了了解 WPF 中绑定是如何工作的,我们可以看几个场景。首先,让我们考虑设置按钮的背景
<Button
Background='Red' />
如果我们希望在多个按钮之间共享此背景,最简单的方法是将颜色定义放在一个公共位置,并将所有按钮连接到该位置。这就是 Resources 属性的设计目的。
要定义资源,我们在控件的 Resources 属性中声明对象,并为该对象分配 x:Key
<Window
x:Class='EssentialWPF.ResourceSample'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
</Window.Resources>
<!-- ... rest of window ... -->
</Window>
然后,我们可以使用 `DynamicResource` 或 `StaticResource` 标记扩展来引用命名资源(在第 6 章详细介绍)
<Window x:Class='EssentialWPF.ResourceSample' xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation' Title='Hello World' > <Window.Resources> <SolidColorBrush x:Key='bg' Color='Red' /> </Window.Resources> <WrapPanel> <Button Background='{StaticResource bg}' Click='HowdyClicked'>Howdy!</Button> <Button Background='{StaticResource bg}'>A second button</Button> <TextBox x:Name='_text1'>An editable text box</TextBox> <CheckBox>A check box </CheckBox> <Slider Width='75' Minimum='0' Maximum='100' Value='50' /> </WrapPanel> </Window>
运行此程序会发现两个按钮颜色相同(图 1.17)。

绑定到资源
资源绑定是一种相对简单的绑定类型。我们还可以使用数据绑定系统在控件(和数据对象)之间绑定属性。例如,我们可以将 `TextBox` 的文本绑定到 `CheckBox` 的内容
<Window
x:Class='EssentialWPF.ResourceSample'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
Title='Hello World'
>
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
</Window.Resources>
<WrapPanel>
<Button Background='{StaticResource bg}'
Click='HowdyClicked'>Howdy!</Button>
<Button Background='{StaticResource bg}'>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox Content='{Binding ElementName=_text1,Path=Text}' />
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
</Window>
当我们运行此代码时,我们可以在文本框中输入,复选框的内容将自动更新(图 1.18)。

两个控件之间的数据绑定
与控件的深度数据集成实现了强大的数据可视化。除了传统的控件,WPF 还提供了对文档、媒体和图形的无缝访问。
整合的力量
WPF 中的可视化系统支持 2D 矢量图形、栅格图像、文本、动画、视频、音频和 3D 图形。所有这些功能都集成到一个基于 DirectX 的单一合成引擎中,允许许多功能在现代显卡上通过硬件加速实现。
为了开始了解这种集成,让我们创建一个矩形。我们将不使用纯色填充矩形,而是创建一个渐变(从一种颜色混合到另一种颜色——在本例中,从红色到白色再到蓝色)
<Window ... >
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
</Window.Resources>
<DockPanel>
<WrapPanel DockPanel.Dock='Top'>
<Button Background='{StaticResource bg}'
Click='HowdyClicked'>Howdy!</Button>
<Button Background='{StaticResource bg}'>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox Content='{Binding ElementName=_text1,Path=Text}' />
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
<Rectangle Margin='5'>
<Rectangle.Fill>
<LinearGradientBrush>
<GradientStop Offset='0' Color='Red' />
<GradientStop Offset='.5' Color='White' />
<GradientStop Offset='1' Color='Blue' />
</LinearGradientBrush>
</Rectangle.Fill>
</Rectangle>
</DockPanel>
</Window>
图 1.19 显示了结果。调整窗口大小会显示矩形改变大小,并且渐变旋转,使其始于并止于矩形的角落。显然,2D 图形与布局引擎集成在一起。

填充渐变的矩形
我们可以将这种集成更进一步,使用一组控件作为画刷,而不是用彩色画刷填充矩形。在下面的示例中,我们将为我们的 WrapPanel 添加一个名称,并使用 `VisualBrush` 填充矩形。`VisualBrush` 接受一个控件并将其显示复制为填充。通过使用 `Viewport` 和 `TileMode` 属性,我们可以使内容复制多次
<Window ... >
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
</Window.Resources>
<DockPanel>
<WrapPanel x:Name='panel' DockPanel.Dock='Top'>
<Button Background='{StaticResource bg}'
Click='HowdyClicked'>Howdy!</Button>
<Button Background='{StaticResource bg}'>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox Content='{Binding ElementName=_text1,Path=Text}' />
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
<Rectangle Margin='5'>
<Rectangle.Fill>
<VisualBrush
Visual='{Binding ElementName=panel}'
Viewport='0,0,.5,.2'
TileMode='Tile' />
</Rectangle.Fill>
</Rectangle>
</DockPanel>
</Window>
运行这段代码会显示,如果我们在顶部编辑控件,矩形中的显示会更新(图 1.20)。我们可以看到,我们不仅可以使用 2D 绘图与控件,而且还可以将控件本身用作 2D 绘图。事实上,所有控件的实现都被描述为一组 2D 绘图。

使用可视画刷填充矩形
我们可以将这种集成推向更深层次。WPF 也提供了基本的 3D 支持。我们可以使用相同的视觉画刷作为 3D 绘图中的纹理。创建一个 3D 场景需要五样东西:模型(形状)、材质(覆盖形状的材质)、相机(从哪里看)、光源(以便我们能看到)和视口(渲染场景的地方)。在第 5 章中,我们将详细介绍 3D 场景,但现在重要的是要注意,作为模型的材质,我们使用了与之前相同的视觉画刷
<Window ... >
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
</Window.Resources>
<DockPanel>
<WrapPanel x:Name='panel' DockPanel.Dock='Top'>
<Button Background='{StaticResource bg}'
Click='HowdyClicked'>Howdy!</Button>
<Button Background='{StaticResource bg}'>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox Content='{Binding ElementName=_text1,Path=Text}' />
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
<Viewport3D>
<Viewport3D.Camera>
<PerspectiveCamera
LookDirection='-.7,-.8,-1'
Position='3.8,4,4'
FieldOfView='17'
UpDirection='0,1,0' />
</Viewport3D.Camera>
<ModelVisual3D>
<ModelVisual3D.Content>
<Model3DGroup>
<PointLight
Position='3.8,4,4'
Color='White'
Range='7'
ConstantAttenuation='1.0' />
<GeometryModel3D>
<GeometryModel3D.Geometry>
<MeshGeometry3D
TextureCoordinates=
'0,0 1,0 0,-1 1,-1 0,0 1,0 0,-1 0,0'
Positions=
'0,0,0 1,0,0 0,1,0 1,1,0 0,1,-1 1,1,-1 1,1,-1 1,0,-1'
TriangleIndices='0,1,2 3,2,1 4,2,3 5,4,3 6,3,1 7,6,1'
/>
</GeometryModel3D.Geometry>
<GeometryModel3D.Material>
<DiffuseMaterial>
<DiffuseMaterial.Brush>
<VisualBrush
Viewport='0,0,.5,.25'
TileMode='Tile'
Visual='{Binding ElementName=panel}' />
</DiffuseMaterial.Brush>
</DiffuseMaterial>
</GeometryModel3D.Material>
</GeometryModel3D>
</Model3DGroup>
</ModelVisual3D.Content>
</ModelVisual3D>
</Viewport3D>
</DockPanel>
</Window>
图 1.21 展示了其外观。就像形状是 2D 矩形时一样,更改控件会反映在 3D 对象上。

用作 3D 形状材质的控件
如前一个示例所示,创建 3D 场景需要大量标记。如果您打算玩 3D,我强烈建议使用 3D 创作工具。
我们查看集成方面的最后一站是动画。到目前为止,一切基本上都是静态的。与 2D、3D、文本和控件集成的方式相同,WPF 中的所有内容都本质上支持动画。
WPF 中的动画允许我们随着时间改变属性值。为了动画我们的 3D 场景,我们将首先添加一个旋转变换。旋转将允许我们通过调整角度来旋转我们的 3D 模型。然后,我们将能够通过随时间调整角度属性来动画显示
<!-- ...rest of scene... -->
<GeometryModel3D>
<GeometryModel3D.Transform>
<RotateTransform3D
CenterX='.5'
CenterY='.5'
CenterZ='-.5'>
<RotateTransform3D.Rotation>
<AxisAngleRotation3D
x:Name='rotation'
Axis='0,1,0'
Angle='0' />
</RotateTransform3D.Rotation>
</RotateTransform3D>
</GeometryModel3D.Transform>
<!-- ...rest of scene... -->
现在我们可以定义我们的动画了。这里有很多细节,但重要的是 `DoubleAnimation`,它允许我们随时间改变双精度值。(`ColorAnimation` 将允许我们动画颜色值。)我们正在动画旋转的角度,从 -25 到 25。它将自动反转,并且每次旋转需要 2.5 秒才能完成。
<Window ...>
<!-- ...rest of scene... -->
<Window.Triggers>
<EventTrigger RoutedEvent='FrameworkElement.Loaded'>
<EventTrigger.Actions>
<BeginStoryboard>
<BeginStoryboard.Storyboard>
<Storyboard>
<DoubleAnimation
From='-25'
To='25'
Storyboard.TargetName='rotation'
Storyboard.TargetProperty='Angle'
AutoReverse='True'
Duration='0:0:2.5'
RepeatBehavior='Forever'
/>
</Storyboard>
</BeginStoryboard.Storyboard>
</BeginStoryboard>
</EventTrigger.Actions>
</EventTrigger>
</Window.Triggers>
<!-- ...rest of scene... -->
运行这段代码会产生类似 图 1.22 的结果,但它是动画的。(我曾试图让出版商在本书的每本副本中都附赠一台笔记本电脑,以便您能看到动画,但他们认为这不划算。)

为我们的 3D 场景添加旋转动画
UI、文档和媒体的集成在 WPF 中深入人心。我们可以用 3D 为按钮添加纹理,我们可以使用视频作为文本的填充——几乎一切皆有可能。这种灵活性非常强大,但它也可能导致非常难以使用的体验。我们可以用来获得丰富而一致的显示效果的工具之一是 WPF 样式系统。
获得一些风格
样式提供了一种机制,用于将一组属性应用于一个或多个控件。由于 WPF 中几乎所有自定义都使用属性,因此我们可以自定义应用程序的几乎所有方面。使用样式,我们可以在应用程序之间创建一致的主题。
为了了解样式的工作原理,让我们修改那两个红色按钮。首先,我们不再让每个按钮都引用资源,而是将背景设置移到样式定义中。通过将样式的键设置为 Button 类型,我们确保该类型将自动应用于此窗口中的所有按钮
<Window ... >
<Window.Resources>
<SolidColorBrush x:Key='bg' Color='Red' />
<Style x:Key='{x:Type Button}' TargetType='{x:Type Button}'>
<Setter Property='Background' Value='{StaticResource bg}' />
</Style>
</Window.Resources>
<!-- ... rest of window ... -->
<WrapPanel x:Name='panel' DockPanel.Dock='Top'>
<Button Click='HowdyClicked'>Howdy!</Button>
<Button>A second button</Button>
<TextBox x:Name='_text1'>An editable text box</TextBox>
<CheckBox Content='{Binding ElementName=_text1,Path=Text}' />
<Slider Width='75' Minimum='0' Maximum='100' Value='50' />
</WrapPanel>
<!-- ... rest of window ... -->
</Window>
运行这段代码会产生与 图 1.22 无法区分的结果。为了使其更有趣,让我们尝试自定义按钮的 `Template` 属性。WPF 中的大多数控件都支持模板,这意味着控件的渲染可以声明性地更改。这里我们将用样式化的椭圆替换按钮的默认外观。
ContentPresenter
告诉模板按钮的内容放置在哪里。在这里,我们使用布局、控件和 2D 图形来实现单个按钮的显示
<Style x:Key='{x:Type Button}' TargetType='{x:Type Button}'>
<Setter Property='Background' Value='{StaticResource bg}' />
<Setter Property='Template'>
<Setter.Value>
<ControlTemplate TargetType='{x:Type Button}'>
<Grid>
<Ellipse StrokeThickness='4'>
<Ellipse.Stroke>
<LinearGradientBrush>
<GradientStop Offset='0' Color='White' />
<GradientStop Offset='1' Color='Black' />
</LinearGradientBrush>
</Ellipse.Stroke>
<Ellipse.Fill>
<LinearGradientBrush>
<GradientStop Offset='0' Color='Silver' />
<GradientStop Offset='1' Color='White' />
</LinearGradientBrush>
</Ellipse.Fill>
</Ellipse>
<ContentPresenter
Margin='10'
HorizontalAlignment='Center'
VerticalAlignment='Center' />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
图 1.23 展示了我们运行此代码时得到的结果。按钮仍然处于活动状态;实际上,单击**Howdy!**按钮仍然会更新文本框(请记住,我们在此前的巡览中编写了这段代码)。

带有自定义模板的按钮,由样式提供
我们现在已经浏览了 WPF 的大部分领域,但我们才刚刚开始触及这个平台中的概念和功能的表面。在结束介绍之前,我们应该讨论如何配置您的计算机来构建和运行我们正在创建的所有这些精彩程序。
构建应用程序的工具
要编译和运行本书中的任何代码,您需要一套基本的工具以及对它们工作原理的一些了解。您只需一个互联网连接即可构建一个完整的开发环境,因为新的 Visual Studio Express 产品为您提供了一个免费的绝佳开发环境!
.NET Framework 3.08
此外,您可以选择获取适用于 Visual Studio 的 .NET Framework 3.0 扩展(当前代号为“Orcas”),它目前以 Visual Studio 下一个版本的社区技术预览 (CTP) 形式提供。不过,随着时间的推移,此软件包将被新发布的 Visual Studio 取代,后者将原生支持 .NET Framework 3.0 开发。
在我们早期的 WPF 巡览中,我们讲解了创建用于编译 WPF 应用程序的项目文件的基本知识。安装 Visual Studio 扩展后,所有项目文件维护都可以由 Visual Studio 处理。另外,Microsoft 的 Expression Blend(代号“Sparkle”)也可以用于构建项目。
两个最有用的 API 文档来源是 Windows SDK 文档和像 Reflector 这样的程序集浏览器工具。11
我们现在在哪里?
在本章中,我们了解了微软构建 WPF 的原因,并简要介绍了该平台的主要领域。我们学习了构建 WPF 应用程序所需的工具,并获得了一些关于在哪里可以找到所需软件以开始使用的提示。
脚注
-
我们可以通过向类型添加
System.Windows.Markup.ContentPropertyAttribute
来实现此目的。 -
本章之后,我将在标记示例中省略“. . ./xaml/presentation”和“. . ./xaml”命名空间。我将始终把 presentation (WPF) 命名空间映射为默认 XML 命名空间,并将“x”作为 XAML 命名空间的前缀。
-
代码隐藏文件的命名约定是“
<markupfile>.cs
”,所以对于 `mywindow.xaml`,我们会创建一个名为 `mywindow.xaml.cs` 的文件。 -
.NET Framework 3.0 可再发行组件可在此处下载。
-
Windows SDK 可在此处下载。
-
Visual C# Express 可在此处下载。
-
Reflector 可在此处下载。
版权所有 © 2007 Pearson Education。保留所有权利。