2.1 新建 WPF 项目
在新建项目之前,先让我们看看什么是“项目模板”。在 Visual Studio 2008 中,当你使用 File->New->Project 菜单命令时,会弹出如图2-1所示的窗口。
这个窗口就是项目模板窗口(也有人称之为“新建项目向导”)。项目模板,意思就是说你选择使用哪个模板,写出来的就是哪种程序。为什么要使用项目模板呢?大家知道,为了满足用户的各种需求,能在 Windows 上运行的程序种类能达到数十种之多。想要得到一个程序,首先要由程序员使用编程语言编写出源代码,然后再使用编译器将源代码编译成成品程序。
编译器也是一个程序,它的职责就是把源代码编译成目标程序。在编译过程中,编译器会根据它获得的指令,把源代码编译成相应种类的程序。就拿 C# 语言的编译器来说,同样一段代码,如果在编译时使用了 /t:exe 参数,那么将编译出一个命令行程序(Console Application),如果把 /t:exe 换成 /t:winexe,则编译结果是一个图形用户界面程序(GUI Application),如果把 /t:winexe 换成 /t:library,则编译结果是一个动态链接库(Dynamic Link Library,DLL)。
C# 的编译器有几十个参数,每种应用程序都有相应的编译参数,这还不算有些种类的应用程序需要在源代码中进行相应的配置(如需要哪些文件盒文件夹、代码的基本格式是什么样)。如果每次写程序都让程序员手动配置这些参数和初始设置,那开销就太大了,因此,VS 2008 准备了对应各种应用程序的模板。所以,当你选择了哪个模板,实际上就是 VS 2008 自动配置好了编译器的参数并准备了一套基本的源代码。
了解了什么是项目模板,就可以动手写第一个 WPF 程序了。从项目模板里选择 WPF Application,并在窗口下部的 Name 文本框里填写 MyFirstWpfApplication,然后单击 OK 按钮,一个基本的 WPF 项目就创建好了。执行 Debug->Start Debugging 菜单命令或使用工具栏的执行图标,就可以变异者程序并在调试模板启动它,如图2-2所示。
在 Solution Explorer 窗口中可以看到,VS 2008 的 WPF 项目模板为我们准备了一系列源代码,如图2-3所示。
下面来介绍一下这些分支都是做什么的:
- Properties 分支:里面的的内容主要是程序要用到的一些资源(如图标、图片、静态的字符串)和配置信息
- References 分支:标记了当前这个项目需要引用哪些其他的项目。目前里面列出来的条目都是 .NET Framework 中的类库,有时候还需要添加其他的 .NET Framework 类库或其他程序员编写的项目或类库。
- App.xaml 分支:程序的主体。大家知道,在 Windows 系统里,一个程序就是一个进程。Windows 还规定,一个 GUI 进程需要有一个窗体(Window)作为“主窗体”。App.xaml 文件的作用就是声明了程序的进程会是谁,同时指定了程序的主窗体是谁。在这个分支里还有一个文件——App.xaml.cs,它是 App.xaml 的后台代码。
- Window1.xaml 分支:程序的主窗体。上面我们看到的图 2-2 所示的那个窗口就是由它声明的。它也具有自己的后台代码 Window1.xaml.cs,VS 2008 还具有可视化编辑能力。
2.2 剖析最简单的 XAML 代码
分析的重点是 Window1.xaml 和它的后台代码。在 Window1.xaml 文件里能看到如下代码:
<Window x:Class="MyFirstWpfApplication.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="3000">
<Grid>
</Grid>
</Window>
XAML 是一种由 XML 派生而来的语言,所以很多 XML 中的概念在 XAML 是通用的。
为了表示同类标签中的某个标签与众不同,可以给它的特征(Attribute)赋值。为特征赋值的语法如下:
- 非空标签:
<Tag Attribute1=Value1 Attribute2=Value2>Content</Tag> - 空标签:
<Tag Attribute1=Value1 Attribute2=Value2/>
在这里有必要把 Attribute 和 Property 这两个词仔细地辨别一下。
这两个词的混淆由来已久。混淆的主要原因就是大多数中文译本里既把 Attribute 译为“属性”,也把 Property 译为“属性”。其实,这两个词所表达的不是一个层面上的东西。
Property 属于面向对象理论的范畴。在使用面向对象思想编程的时候,常常需要对客观事物进行抽象,再把抽象出来的结果封装成类,类中用来表达事物状态的成员就是 Property。比如,Car.Length、Car.Height、Car.Speed 就是 Property 的典型代表。总结一句话就是:Property(属性)是针对对象而言的。
Attribute 则是编程语言文法层面的东西。比如有两个同类的语法元素 A 和 B,为了表示 A 与 B 不完全相同或者 A 与 B 在用法上有些区别,这时候就要针对 A 和 B 加一些 Attribute。
明白了 XAML 的格式以及 Attribute 与 Property 的关系,对上面的代码即可一目了然。它的总体结构是一个 <Window> 标签内部包含着一个 <Grid> 标签,代表的含义是一个窗体对象内嵌着一个 Grid 对象。
XAML 对象是一种“声明”式语言,当你见到一个标签,就意味着声明了一个对象,对象之间的层级关系要么是并列、要么是包含,全都体现在标签的关系上。
下面这些代码就是 <Window> 标签的 Attribute。
x:Class="MyFirstWpfApplication.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="3000"
其中,Title、Height 和 Width 一看就知道是与 Window 对象的 Property相对应的。中间两行(即两个 xmlns)是在声明命名空间。最上面一行是在使用名为 Class 的 Attribute,这个 Attribute 来自于 x: 前缀所对应的命名空间。
前面已经说过,XAML 语言是从 XML语言派生出来的。XML 语言有一个功能就是可以在 XML 文档上使用 xmlns 特征来定义命名空间(namespace),xmlns 也就是 XML-namespace 的缩写了。定义名称空间的好处就是,当来源不同的类重名时,可以使用名称空间加以区分。xmlns特征的语法格式如下:
xmlns[:可选的映射前缀]="名称空间"
xmlns 后可以跟一个可选的映射前缀。如果没有写可选映射前缀,那就意味着所有来自于这个名称空间的标签前都不用加前缀,这个没有映射前缀的名称空间称为“默认名称空间”。上面的例子中,<Window> 和 <Grid> 都来自第二行生命的默认命名空间。而第一行中的 Class 特征则来自于第三行中 x: 前缀对应的名称空间。这里可以做一个有趣的小实验:如果给第二行生命的名称空间加上一个前缀,比如 n,那么代码就必须改成这样才能编译通过:
<n:Window x:Class="MyFirstWpfApplication.Window1"
xmlns:n="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="300" Width="3000">
<n:Grid>
</n:Grid>
</n:Window>
XAML 中引用外来程序集和其中 .NET 名称空间的语法与 C# 是不一样的。在 C# 中,如果想使用 System.Windows.Controls 名称空间里的 Button 类,需要先把包含 System.Windows.Controls 名称空间的程序集 PresentationFramework.dll 通过添加引用的方式引用到项目中,然后再在 C# 代码的顶部写上 using System.Windwos.Controls;。在 XAML 中做同样的事情也需要先添加对程序集的引用,然后再在根元素的起始标签中写上一句 xmlns:c="clr-namespace:System.Windows.Controls;assembly=PresentationFramework"。c 是映射前缀,换成其他的字符串也可以。因为 Button 来自前缀 c 对应的名称空间,所以在使用 Button 的时候就要写成 <c:Button>...</c:Button>。
在 VS 2008 自动提示的顶部,你会看到几个看上去像网页地址的名称空间,如图 2-6 所示,其中就包含例子代码中的两行。为什么名称空间看上去像是一个主页地址呢?其实把它 copy 到浏览器地址栏里尝试跳转也不会打开网页。这里只是 XAML 解析器的一个硬性编码(hard-coding),只要见到这些固定的字符串,就会把一系列必要的程序集(Assembly)和程序集中包含的 .NET 名称空间引用进来。
默认引用进来的两个名称空间格外重要,它们所对应的程序集和 .NET 名称空间如下:
http://schemas.microsoft.com/winfx/2006/xaml/presentation 对应:
- System.Windwos
- System.Windwos.Automation
- System.Windwos.Controls
- System.Windwos.Controls.Primitives
- System.Windwos.Data
- System.Windwos.Documents
- System.Windwos.Forms.Intergration
- System.Windwos.Ink
- System.Windwos.Input
- System.Windwos.Media
- System.Windwos.Media.Animation
- System.Windwos.Media.Effects
- System.Windwos.Media.Imaging
- System.Windwos.Media.Media3D
- System.Windwos.Media.TextFormatting
- System.Windwos.Shapes
也就是说,你在 XAML 代码中可以直接使用这些 CLR 名称空间中的类型(因为默认 XML 名称空间没有前缀)。
HTTP://schemas.microsoft.com/winfx/2006/xaml 则对应一些与 XAML 语法和编译相关的 CLR 名称空间。使用这些名称空间中的类型时需要加上 x 前缀,因为它们背影收到了名为 x 的 XML 名称空间中。
从这两个名称空间的名字和它们所对应的 .NET 程序集上,我们不难看出,第一个名称空间对应的是与绘制 UI 相关的程序集,是表示层上面的东西;第二个名称空间则对应 XAML 语言解析处理相关的程序集,是语言层面的东西。
还剩下 x:Class="MyFirstWpfApplication.Window1" 这个 Attribute。x: 前缀说明这个 Attribute 来自于 x 映射的名称空间——前面我们分析过,这个名称空间是对应 XAML 解析功能的。x:Class,顾名思义它与类有些关系,是何种关系呢?让门做个有趣的实验:
首先,把 x:Class="MyFirstWpfApplication.Window1" 这个 Attribute 删掉,再到 Window1.xaml.cs 文件里,把构造器中对 InitializeComponent 方法的调用也删掉。编译程序,你会发现程序仍然可以运行。为什么呢?打开 App.xaml 这个文件,你能发现这样一个 Attribute——StartUri="Window1.xaml",它是在告诉编译器把 Window1.xaml 解析后生成的窗体作为程序启动时的主窗体。也就是说,只要 Window1.xaml 文件能够被正确解析成一个窗体,程序就可以正常运行。
然后,只恢复 x:Class 这个 Attribute (不恢复对 InitializeComponent 方法的调用),并更改它的值为 x:Class="MyFirstWpfApplication.WindowABC"。编译之后,仍然可以正常运行。这时,使用 IL Disassembler(中间语言反编译器,如图 2-7 所示)打开项目的编译结果,你会发现在由项目编译生成的程序集里面包含一个 WindowABC 的类,如图 2.8 所示。
这说明,x:Class 这个 Attribute 的作用是当 XAML 解析器将包含它的标签解析成 c# 类后,这个类的类名是什么。这里,已经触及到 XAML 的本质。前面我们已经看到,示例代码的结构就是使用 XAML 语言直观地告诉我们,当前被设计的窗体是在一个 <Window> 里嵌套一个 <Grid>。如果是使用 C# 来完成同样的设计呢?显然,我们不可能去更改 Window 这个类,我们能做的事从 Window 类派生出一个类(比如叫 WindowABC),再为这个类添加一个 Grid 类型的字段,然后把这个字段在初始化的时候赋值给派生类的内容属性。代码看起来大概是这样:
(图 2-8)
using System.Windows;
using System.Windows.Controls;
class WindowABC: Window
{
private Grid grid;
public WindowABC()
{
grid = new Grid();
this.Contetnt = grid;
}
}
最后,让我们回到最初的代码。你可能会问:在 XAML 里有 x:Class="MyFirstWpfApplication.Window1",在 Window1.xaml.cs 里面也声明了 Window1 这个类,难道他们不冲突吗?仔细看看 Window1.xaml.cs 中 Window1 类的生命就知道了——在声明时使用了 partial 这个关键字。显然,由 XAML 解析器生成的 Window1 类在声明时也是用了 partial 关键字,这样,由 XAML 解析成的类和 C# 文件里定义的部分就合二为一。
正是由于这种 partial 机制,我们可以把类的逻辑代码留在 .cs 文件里,用 C# 语言来实现,而把那些与声明及布局 UI 相关的代码分离出去,实现 UI 与逻辑分离。并且,用于绘制 UI 的代码也不必再使用 C# 语言,使用 XAML和 XAML 编辑工具工具就能轻松搞定了!