前言

本来没打算写的,但是考虑到我的开源项目,我决定写一下当作复习了。

XAML(EXtensible Application Markup Language)是微软推出的对标 HTML 的标记语言,其本质就是XML的变体,微软在设计 WPF 的时候就学习了 HTML 的前后端分离的模式,这样就类比过来,WPF 技术,使用 XAML 作为前端,C#作为后端,分离了前后端,方便了团队合作和项目开发不必过于耦合。

理解XAML

XAML编译

WPF 创建者知道,XAML 不仅要能仅仅设计协作的问题,它还需要快速运行。尽管 XML 格式可以很灵活的迁移到其他工具和平台,但是它们并不是最有效的选择。XML 的设计目标是逻辑性,易读性而且简单,且没有被压缩。

WPF 使用 BAML(Binary Application Markup Language,是一种二进制应用程序标记语言)来克服这个缺点。BAML 并不是新事物,它实际上就是 XAML 的二进制表示形式。在 Visual Studio 中编译 WPF 应用程序时,所有的 XAML 文件都被转换为 BAML,这些 BAML 然后作为资源嵌入到最终的 DLL 或者 EXE 程序集中。 BAML 是标记化的,这意味着较长的 XAML 被较短的标记代替。BAML 不仅明显小一些,还对其进行了优化,从而使得它更快的被解析。

大多数情况下,我们并不需要关心 XAML 相 BAML 的转换,因为编译器会在后台进行这些工作。但也可以使用未经编译的 XAML ,这对于需要即时提供一些用户界面的情况可能有意义的。

XAML 基础

XAML 的标准非常简单:

  • XAML 文档中的每个元素都被映射为.NET 类的一个实例。元素名称也完全对应。例如元素<Button>对应 WPF的 Button对象
  • 与所有的 XML 文档一样,可以在一个元素中嵌套另一个元素
  • 可以通过特性(attribute)设置每个类的属性(property

在继续学习之前,现在来看一看下面的 XAML 文档基本框架:

1
2
3
4
5
6
7
8
<Window x:Class="WpfApp.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="300" Width="300">
<Grid>

</Grid>
</Window>

该框架仅含有两个元素——顶级的Window元素以及一个Grid元素,Window元素代表整个窗口,在Grid元素中可以放置所有控件。尽管可以使用任何元素作为顶级元素,但是 WPF 应用程序只使用如下几个元素作为顶级元素:

  • Window元素
  • Page元素(该元素定义应用程序与Window元素类型,但是它用于可导航的应用程序)
  • Application元素(该元素定义应用程序资源和启动设置)

与所有 XML 文档一样,在 XAML 文档中只能有一个顶级元素。在上述例子中,这意味着只要使用</Window>标签关闭了Window元素,文档就结束了,在后面不能再有任何内容了。

查看Windows元素的开始标签,将会发现几个有趣的特性,包括一个类名和两个 XML 名称空间,还会发现三个属性,如下所示:

1
Title="MainWindow" Height="300" Width="300"

这个特性对应Window类的一个单独的属性。其告诉 WPF 创建标题为 MainWindow 的窗口,窗口的大小为 $300 \times 300$ 个大小。

注:$300 \times 300$并不是指的像素,而是 WPF 的屏幕计量单位

XAML 名称空间

显然,只提供类名是不够的。XAML 解析器还需要知道类位于哪个 .NET 名称空间。例如,在许多名称空间中都有可能存在 Window类,或者说存在自定义的Window类等。为了区分实际上要使用哪个类,XAML 解析器还会检查应用于元素的 XML 名称空间。

下面是该机制的工作原理。上面的示例文档中存在如下两个名称空间:

1
2
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

xmlns特性是 XML 中的一个特殊特性,它专门用来声明名称空间。这段标记声明了两个名称空间,在创建的所有 WPF XAML 文档中都会有使用这两个名词空间。

  • https://schemas.microsoft.com/winfx/2006/xaml/presentation是 WPF 核心名词空间。它包含了所有 WPF 类,包括用来构建用户界面的控件。在该示例中,该名称空间的声明没有使用名称空间前缀,所以它成为整个文档的默认名称空间。换句话说,除非另行指明,每个元素默认自动位于这个名称空间。
  • https://schemas.microsoft.com/winfx/2006/xaml是 XAML 名称空间。它包含各种 XAML实用特性,这些特性可影响文档的解释方式。该名称空间被映射为前缀x。这意味着可以通过元素名称之前放置名称空间前缀x来使用该名称空间,例如x:ElementName

正如前面看到的一样,XML 名称空间的名称和任何特定的 .NET 名称空间都不匹配。XAML的创建者选择这种设计有两个原因。按照约定,XML 名称空间通常是 URI。这种 URI 看起来像是指明 Web 上的位置,但是实际上不是。

另一个原因是 XAML 中使用 XML 名称空间和 .NET 名称空间不是一一对应的,如果一一对应的话,会明显增加 XAML 文档的复杂程度。所以,WPF 创建人员选择了这种方法,将所有的 .NET 名称空间组合到单个 XML 名称空间中。

代码隐藏类

可以通过 XAML 构造用户界面,但为了使应用程序具有一定的功能,就需要用于链接包含应用程序代码的事件处理程序的方法。XAML 通过使用如下所示的 Class特性简化了这个问题:

1
x:Class="WpfApp.MainWindow"

在 XAML 名称空间的Class特性之前放置了名称空间前缀x,这意味着这是 XAML 语言中更通用的部分。实际上,Class特性告诉 XAML 解析器用指定的名称生成一个新类。

MainWindow类是编译时自动生成的。我们也可以提供MainWindow的部分类,该部分类会自动和 XAML 的部分类结合在一起,这样这个部分类就完美的成为了事件处理程序代码的理想容器。

Visual Studio会自动的帮助我们创建可以放置事件处理程序代码的部分类。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace WpfApp
{
/// <summary>
/// MainWindow.xaml 的交互逻辑
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

在编译应用程序的时候,定义用户界面的 XAML 被转换为 CLR 类型声明,这些类型声明与代码隐藏类文件(如MainWindow.xaml.cs)中的逻辑融合在一起,形成单一的单元。

InitializeComponent()方法

现在,MainWindow类尚不具有任何真正的功能。然而它确实包含一个重要的细节——默认构造函数,当创建类的一个实例的时候,该构造函数调用InitializeComponent()方法。

InitializeComponent()方法在源代码中不可见,因为它是在编译程序的时候自动生成的。本质上,InitializeComponent()方法的所有工作就是调用System.Windows.Application类的LoadComponent()方法。LoadComponent()方法从程序集中提取 BAML ,并用它来构建用户界面。当解析 BAML 时,它会创建每个控件对象,设置其属性,并关联所有事件处理程序。

命名元素

还需要注意的一个细节,在代码隐藏类中,经常希望通过代码来操作控件。为了达到这个目的,控件必须包含 XAML Name特性。在上面的示例中,Grid控件没有包含Name特性,所以不能在代码隐藏类中对其进行操作。

下面的标记演示了如何为Grid控件关联名称:

1
2
<Grid x:Name="grid1">
</Grid>

可以直接在 XAML 文档中进行设置,也可以选择控件,在其属性面板中修改。

无论哪种方法,Name特性都会告诉 XAML 解析器将这样一个字段添加到 MainWindow类自动生成的部分。

1
private System.Windows.Controls.Grid grid1;

现在就可以在MainWindow类的代码中,通过grid1名称和元素进行交互了。

XAML中的属性和事件

简单属性与类型转换器

现在如下一个文本示例,并为其设置了对齐方式,字体大小等属性:

1
<TextBlock x:Name="block1" Text="这是一行文本" VerticalAlignment="Center" FontSize="24" HorizontalAlignment="Center"></TextBlock>

为了使得上面的属性设置起作用,对应的TextBlock类也必须有对应的属性,例如FontSizeText等。

为了使这个系统顺利工作,XAML 解析器需要执行比表面看上去更多的工作。XML 特性中的值总是纯文本字符串。但是对应的属性可以是任何 .NET 类型。在上面的实例中,有字符串类型的Text,整型的FontSize等。

为了关联字符串的值和非字符串的属性,XAML 解析器需要进行转换。由类型转换器执行类型转换。在这个过程中,类型转换器起到了很重要的作用,它负责将特定的 .NET 类型转换为任意其他 .NET 类型,或者将任何其他 .NET 类型转换为特定的 .NET 类型。

XAML 区分大小写,这意味着<Button>不等同于<button>,但是类型转换器并不区分大小写,这就意味着Foreground="White"等同于Foreground="white"

复杂属性

虽然类型转换器便于使用,但是它并不能解决所有问题。例如:有些属性是完备的对象,这些对象具有自己的一组属性。尽管创建供类型转换器使用的字符串表示的形式是可能的,但是语法可能十分复杂,且容易出错。

幸运的是,XAML 提供了另一种选择:属性元素语法。使用属性元素语法,可以添加名称为Parent.PropertyName的子元素。例如:Grid控件有一个Background属性,该属性允许提供用于绘制背景的画刷。如果希望使用复杂的画刷——就需要添加名称为Grid.Background的子标签。例如:

1
2
3
4
<Grid>
<Grid.Background>
</Grid.Background>
</Grid>

还有一个细节就是,一旦识别出来要配置的复杂属性,该如何设置的问题。这里有一个技巧就是可以在嵌套元素内部添加其他标签来实例化特定的类。例如在上述示例中,为了定义需要的渐变颜色,需要创建LinearGradientBrush对象,代码示例:

1
2
3
4
5
<Grid>
<Grid.Background>
<LinearGradientBrush></LinearGradientBrush>
</Grid.Background>
</Grid>

但是,只创建一个LinearGradientBrush对象还不够,还需要指定其他的渐变色。需要通过GradientStop对象的集合填充属性LinearGradientBrush.GradientStops来完成这个工作。代码示例:

1
2
3
4
5
6
7
8
9
<Grid>
<Grid.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>

</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
</Grid>

最后可以使用一系列的GradientStop对象填充GradientStops集合。

1
2
3
4
5
6
7
8
9
10
11
<Grid>
<Grid.Background>
<LinearGradientBrush>
<LinearGradientBrush.GradientStops>
<GradientStop Offset="0" Color="Red"/>
<GradientStop Offset="0.5" Color="Black"/>
<GradientStop Offset="1" Color="Yellow"/>
</LinearGradientBrush.GradientStops>
</LinearGradientBrush>
</Grid.Background>
</Grid>

当然,任何的 XAML 标签都可以直接使用C#代码来直接实现,上述代码等价于下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LinearGradientBrush linearGradientBrush = new LinearGradientBrush();

//创建对象GradientStop,并定义属性加入到集合中
GradientStop gradientStop1 = new GradientStop();
gradientStop1.Offset = 0;
gradientStop1.Color = Colors.Red;
linearGradientBrush.GradientStops.Add(gradientStop1);

GradientStop gradientStop2 = new GradientStop();
gradientStop2.Offset = 0.5;
gradientStop2.Color = Colors.Black;
linearGradientBrush.GradientStops.Add(gradientStop2);

GradientStop gradientStop3 = new GradientStop();
gradientStop3.Offset = 1;
gradientStop3.Color = Colors.Yellow;
linearGradientBrush.GradientStops.Add(gradientStop3);

//设置Grid元素的背景属性
grid1.Background = linearGradientBrush;

标记扩展

对大多数属性而言,XAML 属性语法已经可以很好的工作了。但是有有些情况下,不能硬编码属性值。例如,可能希望将属性值设置为一个已经存在的对象,或者可能希望通过将一个属性绑定到另一个控件上来动态的控制属性值。这种情况都需要使用标记扩展——一种以非常规的方法设置属性的专门语法。

标记扩展可以用于嵌套标签或者 XML 特性中。当用在特性中是采用花括号包围起来。例如,下面的标记,它允许引用另一个类中的静态属性。

1
<TextBlock Foreground="{x:Static SystemColors.ActiveBorderBrush}"/>

标记扩展的语法:{标记扩展类 参数}。上述实例标记扩展时StaticExtension类,根据约定,在引用扩展类时可以省略最后一个单词Extension

因为标记扩展映射为类,所以它们也可以作嵌套属性,例如:

1
2
3
<TextBlock.Foreground>
<x:Static Member="SystemColors.ActiveBorderBrush"></x:Static>
</TextBlock.Foreground>

和大多数标记扩展一样,StaticExtension需要在运行时赋值,因为只有在运行时才能缺点当前系统的颜色。

附加属性

除了普通属性外,XAML 还包括附加属性——附加属性是可以用于多个控件但是在另一个类中定义的属性。在 WPF 中,附加属性常用于控件布局。

此处省略附加属性的详细说明,一般是Grid属性的排列位置需要附加指定,本质是调用Grid类的静态方法

嵌套元素

XAML 通 HTML 整个就是一个嵌套的元素树,具体嵌套是根据元素所继承的不同接口或者方法来实现的,详细自行查询,此处也不做过多赘述。

特殊字符与空白

XAML 受制于 XML 规则的限制。例如:XML 关注一些特殊字符,例如 &,< 和 > 。如果试图使用这些字符设置元素的内容,会遇到一定的麻烦,因为解析器可能会认为你在创建其他事情,例如嵌套元素。例如:

1
2
3
<Button> 
<这是一行字>
</Button>

这个地方当你想要创建一个Button含有文字<这是一行字>解决问题的方法就是使用实体引用代替特殊字符,实体引用是 XAML 解析器能够正确解释特定字符的编码。如下:

特殊字符 字符实体
小于号(<) &lt;
大于号(>) &gt;
& &amp;
引号(”) &quot;

下面是正确使用实体引用实例:

1
<Button> &lt;</Button>

特殊字符并非使用 XAML 的唯一障碍。另一个问题是空白的处理。默认情况下,XAML 折叠所有空白,这就意味着空格,Tab 键以及硬回车的长字符都会被转换为单个空格。而且,如果在元素内容之前或者之后添加空白,将会被完全忽略这个空格。

有时候这并不是我们所期望的。例如:我们可能希望文本内容含有一系列的空格。在这种情况下,就需要为元素使用xml:space="preserve"。代码示例:

1
<Button xml:space="preserve">这是       很多空格</Button>

需要注意的是,这个问题只会在 XAML 标记中存在。如果通过代码来设置文本则不会出现这种情况。

事件

到目前为止,介绍的所有特性都被映射为属性。然而,特性也可以用于关联事件处理程序,语法为:事件名称=事件处理程序方法名

注:关于事件详细不再赘述,如果你了解过微软的相关开发技术相信都了解一二

使用其他名称空间中的类型

前面介绍了如何在 XAML 中使用 WPF 中的类来创建基本的用户界面。但是 XAML 是实例化 .NET 对象的通用方法,包括那些位于其他非 WPF 名称空间以及自己创建的名称空间的对象。

为使用未在 WPF 名称空间中定义的类,需要将 .NET 名称空间映射到 XML 名称空间,XAML 有一种特殊的语法可用于完成这一工作,语法示例如下:

1
xmlns:Prefix="clr-namespace:Namespace;assembly=AssemblyName"

通常,在 XAML 文档的根元素中,在紧随声明 WPF 和 XAML 名称空间的特性之后放置这个名称空间。还需要使用适当的信息填充这三个部分,其含义如下:

  • **Prefix**:是希望在 XAML 标记中用于指示名称空间的 XML 前缀。例如:XAML 语言使用x作为前缀
  • **Namespace**:是完全限定的 .NET 名称空间的名称
  • **AssemblyName**:是声明类型的程序集,没有.dll扩展名。这个程序集必须在项目中引用。如果希望使用项目程序集,则可以忽略这一部分。

例如,下面的标记演示了如何访问System名称空间中的基本类型,并将其映射到前缀sys

1
xmlns:sys="clr-namespace:System;assembly=mscorlib"

下面标记演示了如何访问当前项目在 MyProkect 名称空间中声明的类型,并将它们映射到local前缀中:

1
xmlns:local="clr-namespace:MyObject"

End

介绍了 XAML 的基本语法和一些特性,还有一些深入的底层原理和内容需要自行研究。