第2章:XAML
本章目标
- 了解Windows图形演化
- 了解WPF高级API
- 了解分辨率无关性概念
- 了解WPF体系结构
- 了解WPF 4.5
XAML 概述
XAML(Extensible Application Markup Language 的简写,发音为 “zammel”)是用于实例化.NET对象的标记语言。尽管 XAML是一种可应用于诸多不同问题领域的技术,但其主要作用是构造WPF用户界面。换言之,XAML 文档定义了在WPF应用程序中组成窗口的面板、按钮以及各种控件的布局。
不必再手动编写 XAML,您将使用工具生成所需的XAML。如果您是一位图形设计人员,该工具可能是图形设计程序,如Expression Blend。如果您是一名开发人员,您开始时可能使用Microsof Visual Studio。这两个工具在生成 XANL 时本质上是相同的,因此可使用 Visual Studio
创建一个基本用户界面,然后将该界面移交给一个出色的设计团队,由设计团队在 ExpressionBlend 中使用自定义图形润色这个界面。实际上,将开发人员和设计人员的工作流程集成起来的能力,是 Microsoft 推出 XANL 的重要原因之一。
本章将详细介绍 XAML,分析 XANL 的作用、宏观体系结构以及语法。一旦理解了XAML的一般性规则,就可以了解在WPF 用户界面中什么是可能的、什么是不可能的,并了解在必要时如何手动修改用户界面。更重要的是,通过分析 WPF XANL 文档中的标签,可学习一些支持WPF 用户界面的对象模型,从而为进一步深入分析 WPF 用户界面做好准备。
理解XAML
WPF之前的图形用户界面
使用传统的显示技术,从代码中分离出图形内容并不容易。对于 Windows 窗体应用程序而言,关键问题是创建的每个窗体完全都是由C#代码定义的。在将控件拖动到设计视图控件时,Visual Studio 将在相应的窗体类中自动调整代码。但图形设计人员没有任何可以使用C#代码的工具。
相反,美工人员必须将他们的工作内容导出为位图。然后可使用这些位图确定窗体、技钮以及其他控件的外观。对于简单的固定用户界面而言,这种方法效果不错,但在其他一些情况下会受到很大的限制。这种方法存在以下几个问题:
• 每个图形元素(背景和技钮等)需要导出为单独的位图。这限制了组合位图的能力和使用动态效果的能力,如反锯齿、透明和阴影效果。
• 相当多的用户界面逻辑都需要开发人员嵌入到代码中,包括技钮的大小、位置、鼠标悬停效果以及动画。图形设计人员无法控制其中的任何细节。
• 在不同的图形元素之间没有固有的连接,所以最后经常会使用不匹配的图像集合。跟踪所有这些项会增加复杂性。
• 在调整图形大小时必然会损失质量。因此,一个基于位图的用户界面是依赖于分辨率的。这意味着它不能适应大显示器以及高分辨率显示设置,而这严重背离了WPF的设计初衷。
如果曾经有过在一个团队中使用自定义图形来设计 Windows 窗体应用程序的经历,肯定遇到过不少挫折。即使用户界面是由图形设计人员从头开始设计的,也需要使用C#代码重新创建它。通常,图形设计人员只是准备一个模拟界面,然后需要开发人员再不辞辛劳地将它转换到应用程序中。
WPF通过 XAML 解决了该问题。当在Visual Studio中设计WPF 应用程序时,当前设计的窗口不被转换为代码。相反,它被串行化到一系列XANL 标签中。当运行应用程序时,这些标签用于生成构成用户界面的对象。注意:
XAML对于 WPF不是必需的,理解这一点是很重要的.Visual Studio 当然可使用 Windows
窗体方法,通过语句代码来构造 WPF 窗口.但如果这样的话,窗口将被限制在 Visual Studio开发环境之内,只能由编程人员使用。换句话说,WPF 不见得使用 XAML。但XAML为协作提供了可能,因为其他设计工具理解 XANL 格式。例如,聪明的设计人员可使用 Microsoft Expression Design 等工具精细修改 WPF应用程序的图形界面,或使用 Expression Blend 等工具为WPF应用程序构建精美动画。当学完本章后,您可能希望阅读位于 http://windowsclient.net/wpf/white-papers/thenewiteration.aspx的白皮书,该白皮书对 XAML 进行了评论,并且分析了开发人员和设计人员协作开发序的一些方法。>>
XMAL 变体
实际上术语“XAML”有多种含义。到目前为止,我们使用 XAML 表示整个 XAML.语言,它是一种基于通用XIMIL 语法、专门用于表示一棵,NET 对象树的语言(这些对象可以是窗口中的按钮、文本框,或是您已经定义好的自定义类。实际上,XAML 甚至可用于其他平台来表示非.NET 对象)。
XAML 还包含如下几个子集:
- WPF XAML包含描述WPF 内容的元素,如矢量图形、控件以及文档。目前,它是最重要的 XAML 应用,也是本书将要分析的一个子集。
- XPS XAML 是 WPF XAML 的一部分,它为格式化的电子文档定义了一种XML. 表示方式。XPS XAML 已作为单独的 XML. 页面规范(XML Paper Specification,xPs)标准发布。
- Silverlight XAML 是一个用于 Microsoft Silverlight 应用程序的 WPF XAML 子集。Silverlight是一个跨平台的浏览器插件,通过它可创建具有二维图形、动画、音频和视频的富 Web 内容。第1 章介绍了关于 Silverlight的更多内容,您也可以访问http://silverlight.net 来了解详情。
- WF XAML包括描述WF(Work Flow,工作流)内容的元素,可访问http://tinyurl.com/d9xc2nv
来了解有关 WF的更多内容。
XAML 编译
WPF的创建者知道,XANL 不仅要能够解决设计协作问题,它还需要快速运行。尽管基于XMIL 的格式(如XAML)可以很灵活并且很容易地迁移到其他工具和平台,但它们未必是最有效的选择。XINL的设计目标是具有逻辑性、易读而且简单,没有被压缩。
WPF使用BAML(Binary Application Markup Language, 二进制应用程序标记语言)来克服这个缺点。BAML 并非新事物,它实际上就是 XAML的二进制表示。当在 Visual Studio 中编译WPF 应用程序时,所有 XAML文件都被转换为 BAMNL,这些BAML 然后作为资源被嵌入到最终的 DLL 或EXE 程序集中。BAML 是标记化的,这意味着较长的XAML. 被较短的标记替代。BAML 不仅明显小一些,还对其进行了优化,从而使它在运行时能够更快地解析。
大多数开发人员不必考虑 XANL 向 BANL 的转换,因为编译器会在后台执行这项工作。但也可以使用未经编译的 XANL,这对于需要即时提供一些用户界面的情况可能是有意义的(例如,将从某个数据库中提取的内容作为一块 XAML 标签)。
XAML基础
- XAML文档中的每个元素都映射为.NET 类的一个实例。元素的名称也完全对应于类名。例如,元素<Button>指示 WPF 创建 Button 对象。
- 与所有 XMIL文档一样,可在一个元素中 套另一个元素。您在后面将看到,XAML让每个类灵活地决定如何处理嵌套。但嵌套通常是一种表示“包含”的方法—换句话说,如果在一个Grid 元素中发现一个 Button 元素,那么用户界面可能包括一个在其内部包含一个 Button 元素的Grid 元素。
- 可通过特性(attribute)设置每个类的属性(property)。但在某些情况下,特性不足以完成这项工作。对于此类情况,需要通过特殊的语法使用嵌套的标签(tag)。
XAML文档框架
<Window x:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid></Grid>
</Window>
文档仅含两个元素——顶级的 Window 元素以及一个 Grid 元素,Window 元素代表整个窗口,在Grid 元素中可以放置所有控件。尽管可使用任何顶级元素,但是WPF 应用程序只使用以下几个元素作为顶级元素:
Window 元素
Page元素(该元素和 Window 元素类似,但它用于可导航的应用程序)
Application 元素(该元素定义应用程序资源和启动设置)
与在所有 XML 文档中一样,在 XAMIL 文档中只能有一个顶级元素。在上例中,这意味着只要使用\<window\>标签关闭了Window 元素,文档就结束了。在后面不能再有任何内容了。
XAML 名称空间
显然,只提供类名是不够的。XAML 解析器还需要知道类位于哪个,NET名称空间。例如,在许多名称空间中可能都有 Window 类,Window类可能是指 System.Windows.Window类,也可能是指位于第三方组件中的Window 类,或您自己在应用程序中定义的Window类等。为了弄清实际上希望使用哪个类,XAML 解析器会检查应用于元素的 XML名称空间。
上面显示的示例文档定义了5个名称空间.
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp"
注意:
使用特性声明 XML 名称空间。这些特性能被放入任何元素的开始标签中。但约定要求,在文档中需要使用的所有名称空间应在第一个标签中声明,正如在这个示例中所做的那样.一旦声明一个名称空间,在文档中的任何地方都可以使用该名称空间。
xmnlns特性是 XML 中的一个特殊特性,它专门用来声明名称空间。这段标记声明了5个名称空间,在创建的所有 WPF XAML 文档中都会使用这5个名称空间,其中核心的2个名称空间如下:
- http://schemas.nicrosoft.com/wintx/2006/xaml/presentation 是 WPF 核心名称空间。它包•
含了所有WPF类,包括用来构建用户界面的控件。在该例中,该名称空间的声明没有使用名称空间前缀,所以它成为整个文档的默认名称空间。换句话说,除非另行指明。
每个元素自动位于这个名称空间。- http://schemas.microsoft.com/winfx/2006/xaml 是 XAML 名称空间。它包含各种 XAML实用特性,这些特性可影响文档的解释方式。该名称空间被映射为前缀x。这意味着可通过在元素名称之前放置名称空间前缀x来使用该名称空间(例如<x:ElementName>)。
代码隐藏类
可通过XAML构造用户界面,但为了使应用程序具有一定的功能,就需要用于连接包含应用程序代码的事件处理程序的方法。XAML. 通过使用如下所示的 Class 特性简化了这个问题:
<Window x:Class=“WpfApp.MainWindow”
在 XAML 名称空间的 Class 特性之前放置了名称空间前缀x,这意味着这是XAML语言中更通用的部分。实际上,Class 特性告诉 XANL解析器用指定的名称生成一个新类。该类继承自由XIML 元素命名的类。换句话说,该例创建了一个名为MainWindow的新类,该类继承自Window基类。
MainWindow类是编译时自动生成的。这正是令人感兴趣之处。您可以提供 MainWindow的部分类,该部分类会与自动生成的那部分合并在一起。您提供的部分类正是包含事件处理程序代码的理想容器。
注意:
这个过程是使用 C#语言的部分类(partial class)特征实现的。部分类允许在开发阶段把一个类分成两个或更多独立的部分,并在编译过的程序集中把这些独立的部分融合到一起,部分类可用于各种代码管理情形,但在此类情况下是最有用的,在此编写的代码需要和设计工具生成的文件融合到一起。
Visual Studio 会自动帮助您创建可以放置事件处理代码的部分类。例如,如果创建一个名为WindowsApplicationl的应用程序,该应用程序包含名为 Windowl 的窗口(就像上面的示例那样),Visual Studio 将首先提供基本的类框架:
namespace WpfApp {/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}} }
在编译应用程序时,定义用户界面的 XAMIL(如 MainWindow.xaml)被转换为 CLR 类型声明,这些类型声明与代码隐藏类文件(如MainWindow.xaml.cs)中的逻辑代码融合到一起,形成单一的单元。
1.InitializeComponent()方法
现在,Window1类尚不具有任何真正的功能。然而,它确实包含了一个非常重要的细节—默认构造函数,当创建类的一个实例时,该构造函数调用 InitializeComponent()方法。注意:
InitializeComponent()方法在WPF 应用程序中扮演着重要角色。因此,永远不要删除窗口构造函数中的 InitializeComponent()调用.同样,如果为窗口类添加另一个构造函数,也要确保调用 InitializeComponent()方法。
InitializeComponent()方法在源代码中不可见,因为它是在编译应用程序时自动生成的。本质上,LnitializeComponent()方法的所有工作就是调用 System.Windows.Application 类LoadComponent()方法。LoadComponent()方法从程序集中提取 BAML(编译过的 XAML),并用它来构建用户界面。当解析 BAML 时,它会创建每个控件对象,设置其属性,并关联所有事件处理程序。
2.命名元素
还有一个需要考虑的细节。在代码隐藏类中,经常希望通过代码来操作控件。例如,可能需要读取或修改属性,或自由地关联以及断开事件处理程序。为达到此目的,控件必须包含XANIL Name特性。在上面的示例中,Grid控件没有包含 Nae特性,所以不能在代码隐藏文件中对其进行操作。
下面的标记演示了如何为Grid 控件关联名称:
<Grid x:Name="grid1"></Grid>
可在 XAML 文档中手动执行这个修改,也可在Visual Shudio设计器中选择该网格,并通过Properties 窗口设置其 Name 属性。
无论使用哪种方法,Name 特性都会告诉XANLL 解析器将这样一个字段添加到为 MainWindow类自动生成的部分:
internal System.Windows.Controls.Grid grid1;
现在可以在MainWindow类的代码中,通过 gridl 名称与网格元素进行交互了:
提示:
在传统的 Windows 窗体应用程序中,每个控件都有名称。而在 WPF 应用程序中,没有这一要求。在本书的示例中,当不需要元素名称时通常会省略,这样可以使标记更加简洁。
到现在为止,应当对如何解释定义窗口的XAML 文档,以及XAML文档是如何被转换为最终编译过的类(包括编写的其他所有代码)有了基本的理解。下一节将介绍有关属性语法的更多细节,并将学习如何关联事件处理程序。
XAML中的属性和事件
下图展示一个简单的问答交互功能示例。
xaml代码:
<Window x:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid x:Name="grid1" ShowGridLines="True"><!--设置行列--><Grid.RowDefinitions><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions><!--控件--><TextBox Name="txtQuestion" Grid.Row="0" Margin="5"VerticalAlignment="Stretch" HorizontalAlignment="Stretch"FontFamily="Verdana"FontSize="24"Foreground="Green"></TextBox><Button Name="btnAnswer" Grid.Row="1" Width="200" Height="50" Click="btnAnswer_Click">咨询</Button><TextBox Name="txtAnswer" Grid.Row="2" Margin="5"VerticalAlignment="Stretch" HorizontalAlignment="Stretch"FontFamily="Verdana"FontSize="24"Foreground="Red"></TextBox></Grid>
</Window>
cs代码:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;namespace WpfApp
{/// <summary>/// MainWindow.xaml 的交互逻辑/// </summary>public partial class MainWindow : Window{public MainWindow(){InitializeComponent();}/// <summary>/// 按钮单击事件/// </summary>/// <param name="sender"></param>/// <param name="e"></param>private void btnAnswer_Click(object sender, RoutedEventArgs e){if (this.txtQuestion.Text=="who are you"){this.txtAnswer.Text = "i'am kaige!";}}}
}
简单属性与类型转换器
前面已经介绍过,元素的特性设置相应对象的属性。例如,我们为上面示例中的文本框设置了对齐方式、页边距和字体:
<TextBox Name="txtQuestion" Grid.Row="0" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"FontFamily="Verdana"FontSize="24"Foreground="Green">
为使上面的设置起作用,System.Windows.Controls.TextBox 类必须提供以下属性:
VerticalAlignment、HorizontalAligament、FontFamily、FontSize 和Foreground。后面几章将介绍这些属性的具体含义。 为使这个系统能够工作,XAML 解析器需要执行比表面上看起来更多的工作。XMLL 特性中的值总是纯文本字符串。但对象的属性可以是任何.NET 类型。在上面的示例中,有两个属性为枚举类型(VerticalAligament 属性和 HorizontalAlignment属性)、一个为字符申类型(FontFamily属性)、一个为整型(FontSize 属性),还有一个为 Brush 对象(Foreground 属性)。
为了关联字符串值和非字符串属性,XAML 解析器需要执行转换。由类型转换器执行转换,类型转换器是从.NET 1.0 起就已经引入的.NET 基础结构的一个基本组成部分。
实际上,类型转换器在这个过程中扮演着重要角色— 提供了实用的方法,这些方法可将特定的.NET 数据类型转换为任何其他.NET类型,或将其他任何.NET类型转换为特定的数据类型,比如这种情况下的字符串类型。XAMIL 解析器通过以下两个步骤来查找类型转换器:
(I) 检查属性声明,查找 TypeConverter 特性(如果提供了 TypeConverter 特性,该特性将指定哪个类可执行转换)。例如,当使用诸如 Foreground这样的属性时,NET 将检查 Foreground属性的声明。
(2) 如果在属性声明中没有 TypeConverter 特性,XAWL 解析器将检查对应数据类型的类声明。
例如,Foreground 属性使用一个 Brush 对象。由于 Brush 类使用 TypeConverter (typeof(BrushConverter))特性声明进行了修饰,因此 Brush类及其子类使用 BrushConverter 类型转换器。
注意:
与所有基于 XML的语言一样,XAML 也区分大小写。这意味着不能用<button>替代<Button>,然而,类型转换器通常不区分大小写,这意味着 Foreground=“White” 和
Foreground="white"具有相同的效果
复杂属性
虽然类型转换器便于使用,但它们不能解决所有的实际问题。例如,有些属性是完备的对象,这些对象具有自己的一组属性。尽管创建供类型转换器使用的字符串表示形式是可能的,但使用这种方法时语法可能十分复杂,并且容易出错。
幸运的是,XANIL 提供了另一种选择:属性元素语法(poroperty-element syntax)。使用属性元素语法,可添加名称形式为 Parent PropertyName 的子元素。例如,Grid 控件有一个 Background 属性,该属性允许提供用于绘制控件背景区域的画刷。如果希望使用更复杂的画刷—比单一固定颜色填充更高级的画刷—就需要添加名为 Grid.Background 的子标签,如下所示:
<!--设置背景--> <Grid.Background><LinearGradientBrush><LinearGradientBrush.GradientStops><GradientStop Offset="0.00" Color="Red"/><GradientStop Offset="0.50" Color="Indigo"/><GradientStop Offset="1.00" Color="Violet"/></LinearGradientBrush.GradientStops></LinearGradientBrush> </Grid.Background>
任何 XANL 标签集合都可以用一系列执行相同任务的代码语句代替。上面显示的使用所选的渐变颜色填充背景的标签,与以下代码是等价的:
LinearGradientBrush brush = new LinearGradientBrush();GradientStop stop1=new GradientStop();stop1.Offset = 0;stop1.Color = Colors.Red;brush.GradientStops.Add(stop1);GradientStop stop2 = new GradientStop();stop2.Offset = 0.5;stop2.Color = Colors.Indigo;brush.GradientStops.Add(stop2);GradientStop stop3 = new GradientStop();stop3.Offset = 1;stop3.Color = Colors.Violet;brush.GradientStops.Add(stop3);
标记扩展
对大多数属性而言,XAMIL 属性语法可以工作得非常好。但有些情况下,不可能硬编码属性值。例如,可能希望将属性值设置为一个已经存在的对象,或者可能希望通过将一个属性绑定到另一个控件来动态地设置属性值。这两种情况都罱要使用标记扩展——种以非常规的方式设置属性的专门语法。
标记扩展可用于嵌套标签或 XMLL 特性中(用于 XNL 特性的情况更常见)。当用在特性中时,它们总是被花括号仔包围起来。例如,下面的标记演示了如何使用标记扩展,它允许引用另一个类中的静态属性:
<Button Foreground="{x:Static SystemColors.ActiveCaptionBrush}"></Button>
标记扩展使用(标记扩展类 参数}语法。在上面的示例中,标记扩展是 StaticExtension 类(根据约定,在引用扩展类时可以省略最后一个单词 Extension)。x 前缀指示在 XAML 名称空间中查找 StaticExtension 类。还有一些标记扩展是WPF 名称空间的一部分,它们不需要x前缀。
所有标记扩展都由继承自 System.Windows.Markup.MarkupExtension 基类的类实现。
MarkupExtension 基类十分简单—它提供了一个简单的 ProvideValue()方法来获取所期望的数值。换句话说,当 XAML 解析器遇到上述语句时,它将创建 StaticExtension 类的一个实例(传递字符串 SystemColors.ActiveCaptionBrush 作为构造函数的参数),然后调用 Provide Value()方法获取 SystemColors.ActiveCaption.Brush 静态属性返回的对象。 因为标记扩展映射为类,所以它们也可用作嵌套属性,与上一节中学过的一样。例如,可以像下面这样为 Button.Foreground 属性使用 StaticExtension 标记扩展:
‹Button> ‹Button.Foreground> ‹x:Static Member="SystemColors.ActiveCaptionBrush">/<x:Static> </Button.Foreground> </Button>
根据标记扩展的复杂程度,以及想要设置的属性数量,这种语法有时更筒单。
和大多数标记扩展一样,StaticBxtension 需要在运行时赋值,因为只有在运行时才能确定当前的系统颜色。一些标记扩展可在编译时评估。这些扩展包括 NulExteision(该扩展构造表示.NET 类型的对象)。
附加属性
除普通属性外,XAML 还包括附加属性(attached property)的概念—附加属性是可用于多个控件但在另一个类中定义的属性。在WPF 中,附加属性常用于控件布局。
下面解释附加属性的工作原理。每个控件都有各自固有的属性(例如,文本框有其特定的字体、文本颜色和文本内容,这些是通过 Fontfamily、Foreground 和Text 属性指定的)。当在容器中放置控件时,根据容器的类型控件会获得额外特征(例如,如果在网格中放置一个文本框,就需要选择文本框放在网格控件中的哪个单元格中)。使用附加属性设置这些附加的细节。
附加属性始终使用包含两个部分的命名形式:定义类型.属性名。这种包含两个部分的命名语法使 XANL解析器能够区分开普通属性和附加属性。
在上面的示例中,通过附加属性在网格的每一行中放置各个控件:
<TextBox Name="txtQuestion" Grid.Row="0"></TextBox><Button Name="btnAnswer" Grid.Row="1">咨询</Button><TextBox Name="txtAnswer" Grid.Row="2"></TextBox>
附加属性根本不是真正的属性。它们实际上被转换为方法调用。XAML. 解析器采用以下形式调用静态方法:Defining Bype.SetPropertName()。例如,在上面的XAMIL 代码段中,定义类型是Grid类,并且属性是 Row,所以解析器调用 Grid.SetRow()方法。
当调用 SetPropertyName()方法时,解析器传递两个参数:被修改的对象以及指定的属性值。例如,当为 TextBox 控件设置 Grid.Row 属性时,XAML 解析器执行以下代码:
Grid.SetRow(txtQuestion,0)
这种方式(调用定义类型的一个静态方法)隐藏了实际发生的操作,使用起来非常方便。乍一看,这些代码好像将行号保存在Grid 对象中。但行号实际上保存在应用它的对象中—对于上面的示例,就是TextBox 对象。
这种技巧之所以能够奏效,是因为与其他所有WPF控件一样,TextBox 控件继承自
DependencyObject 基类。从第4章将可以了解到,DependencyObject 类旨在存储实际上没有限制的依赖项属性的集合(前面讨论的附加属性是特殊类型的依赖项属性)。 实际上,Grid.SetRow()方法是和 DependencyObject.SetValue()方法调用等价的简化操作,如下所示:
txtQuestion. SetValue(Grid.Rowproperty, 0)
附加属性是 WPF 的核心要素。它们充当通用的可扩展系统。例如,通过将 Row 属性定义为附加属性,可确保任何控件都可以使用它。另一个选择是将该属性作为基类的一部分,例如,作为 FrameworkElement 类的一部分,但这样做很复杂。因为只有在特定情况下(在这个示例中,是当在Grid 内部使用元素的时候)有些属性才有意义,如果将它们作为基类的一部分,不仅会使公共接口变得十分杂乱,而且也不能添加需要新属性的新类型的容器。
嵌套元素
正如您所看到的,XANIL文档被排列成一棵巨大的嵌套的元素树。在当前示例中,Window元素包含Grid 元素,Grid 元素又包含 TextBox 元素和Button 元素。
XAML 让每个元素决定如何处理嵌套的元素。这种交互使用下面三种机制中的一种进行中转,而且求值的顺序也是下面列出这三种机制的顺序:
- 如果父元素实现了 IList接口,解析器将调用 IList.Add()方法,并且为该方法传入子元素作为参数。
- 如果父元素实现了 IDictionary 接口,解析器将调用IDictionary.Add()方法,并且为该方法传递子元素作为参数。当使用字典集合时,还必须设置 x:Key 特性以便为每个条目指定键名。
- 如果父元素使用 ContentProperty特性进行修饰,解析器将使用子元素设置对应的属性。
例如,您已经在本章前面的示例中看到过 LinearGradientBrush 画刷如何使用如下所示的语法,从而包含 GradientStop 对象集合;
<LinearGradientBrush><LinearGradientBrush.GradientStops><GradientStop Offset="0.00" Color="Red"/><GradientStop Offset="0.50" Color="Indigo"/><GradientStop Offset="1.00" Color="Violet"/></LinearGradientBrush.GradientStops> </LinearGradientBrush>
特殊字符与空白
XAMIL 受到XMIL 规则的限制。例如,XMIL 特别关注一些特殊字符,如&、<和>。如果试图使用这些字符设置元素的内容,将遇到麻烦,因为 XAMIL 解析器认为您正在处理其他事情—例如,创建嵌套的元素。
例如,假设需要创建一个包含<Click Me~文本的技钮。下面的标记是无法奏效的:
‹Button><Click Me> </Button>
XAML字符实体:
特殊字符 字符实体 小于号(<) & lt; 大于号(>) & gt; &符号 & amp; 双引号(") & quot; 特味字符并非使用 XAMIL.的唯一障碍。另一个问题是空白的处理。默认情况下,XAML 折叠所有空白,这意味着包含空格、Tab键以及硬回车的长字符串将被转换为单个空格。而且,如果在元素内容之前或之后添加空白,将完全忽略这个空格。
有时这并不是所期望的结果。例如,可能希望在按钮文本中包含一系列空格。在这种情况下,需要为元素使用 xml:space="preserve"特性。
xml:space 特性是 XML 标准的一部分,是一个要么包括全部、要么什么都不包括的设置。
一旦使用了该设置,元素内的所有空白字符都将被保留。比如下面的标记:<TextBox Name="txtQuestion" xml: space="preserve" ...> (There is a lot of space inside these quotation marks " "•) </TextBox>
事件
到目前为止介绍的所有特性都被映射为属性。然而,特性也可用于关联事件处理程序。用于关联事件处理程序的语法为:事件名=”事件处理程序方法名”。
例如,Button 控件提供了 Click 事件。可使用如下所示的标记关联事件处理程序:
<Button Name="btnAnswer" Grid.Row="1" Click="btnAnswer_Click" Width="200" Height="50">咨询</Button>
上面的标记假定在代码隐藏类中有名为 cmmdAnswer_ Click 的方法。事件处理程序必须具有正确的签名(也就是说,必须匹配 Click 事件的委托)。下面是一个符合要求的方法:
private void btnAnswer_Click(object sender, RoutedEventArgs e) {if (this.txtQuestion.Text=="who are you"){this.txtAnswer.Text = "i'am kaige!"; } }
完整示例
<Window x:Class="WpfApp.MainWindow"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp"mc:Ignorable="d"Title="MainWindow" Height="450" Width="800"><Grid x:Name="grid1" ShowGridLines="True"><!--设置背景--><Grid.Background><LinearGradientBrush><LinearGradientBrush.GradientStops><GradientStop Offset="0.00" Color="Red"/><GradientStop Offset="0.50" Color="Indigo"/><GradientStop Offset="1.00" Color="Violet"/></LinearGradientBrush.GradientStops></LinearGradientBrush></Grid.Background><!--设置行列--><Grid.RowDefinitions><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions><!--文本框--><TextBox Name="txtQuestion" Grid.Row="0" Margin="5"VerticalAlignment="Stretch" HorizontalAlignment="Stretch"FontFamily="Verdana"FontSize="24"Foreground="Green"></TextBox><!--按钮--><Button Name="btnAnswer" Grid.Row="1" Click="btnAnswer_Click" Width="200" Height="50">咨询</Button><TextBox Name="txtAnswer" Grid.Row="2" Margin="5"VerticalAlignment="Stretch" HorizontalAlignment="Stretch"FontFamily="Verdana"FontSize="24"Foreground="Red"></TextBox><!--文本框--><Button Foreground="{x:Static SystemColors.ActiveCaptionBrush}"></Button></Grid> </Window>
请记住,可能不会为整个用户界面手动编写 XAMLL-这样做将是非常单调乏味的。但可能会编辑 XAML 标记,对界面进行某些修改,而在设计器中完成这些修改可能是很笨拙的。您可能还会发现通过分析 XAML 可以很好地理解窗口的工作原理。
使用其他命名空间中的类型
为使用未在 WPF名称空间中定义的类,需要将NET名称空间映射到 XML,名称空间。
XAML 有一种特殊的语法可用于完成这一工作,该语法如下所示:xmIns: Prefix="clr-namespace: Namespace; assembly=AssemblyName"
通常,在 XAML 文档的根元素中,在紧随声明WPF 和XAML名称空间的特性之后放置这个名称空间。还需要使用适当的信息填充三个斜体部分,这三部分的含义如下:
- Prefix 是希望在 XAML 标记中用于指示名称空间的 XMIL 前缀。例如,XAML语言使用x前缀。
- Namespace 是完全限定的.NET名称空间的名称。
- AssemblyName是声明类型的程序集,没有,dll 扩展名。这个程序集必须在项目中引用。如果希望使用项目程序集,可忽略这一部分。
<Window x:Class="WpfApp.Window1"xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"xmlns:d="http://schemas.microsoft.com/expression/blend/2008"xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"xmlns:local="clr-namespace:WpfApp"xmlns:sys="clr-namespace:System;assembly=mscorlib"mc:Ignorable="d"Title="Window1" Height="450" Width="800"><ListBox><ListBoxItem><sys:DateTime>10/13/2024 5:20 PM</sys:DateTime></ListBoxItem><ListBoxItem><sys:DateTime>10/15/2024 10:20 PM</sys:DateTime></ListBoxItem><ListBoxItem><sys:DateTime>10/18/2024 12:20 PM</sys:DateTime></ListBoxItem></ListBox> </Window>
加载和编译 XAML
只使用代码
以下代码用于生成一个普通窗口,该窗口包含一个按钮和一个事件处理程序(下图)。在创建窗口时,构造函数调用 InitializeComponent()方法,该方法实例化并配置这个按钮和窗体,并连接(hook up)事件处理程序。
注意:
要创建该示例,必须从头编写 Window1类(右击 Solution Explorer 中的项目,然后从上下文莱单中选择 Add | Class 菜单项)。不能选择 Add|Window 菜单项,因沟这将为窗口添加一个代码文件和一个 XANL 模板,并带有自动生成的 InitializeComponent()方法。
Window2.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Markup; using System.Windows.Controls;namespace WpfApp {internal class Windows2:Window{private Button button1;public Windows2(){InitializeComponent();}private void InitializeComponent(){//windowthis.Width = this.Height = 800;this.Left = this.Top = 100;this.Title = "Code-Only Window";//panelDockPanel panel = new DockPanel();//buttonbutton1= new Button();button1.Content = "Click Me";button1.Margin = new Thickness(30);button1.Click += Button1_Click;IAddChild container = panel;container.AddChild(button1);container = this;container.AddChild(panel);}private void Button1_Click(object sender, RoutedEventArgs e){button1.Content = "小伙子,真听话!";}} }
Program.cs:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows;namespace WpfApp {internal class Program:Application{[STAThread()]static void Main(){Program p = new Program();p.MainWindow = new Windows2();p.MainWindow.ShowDialog();}} }
注意:
删除app.xaml
添加Program.cs类
使用代码和未经编译的 XAML
下面就是对代码进行修改,让窗口加载我们的XAML文件从而达到添加组件的目的,我们首先对主窗口MainWindow的构造函数进行修改,设置一些窗口的基本属性。 然后接着在重载的构造函数里面加载XAML文件 。
public MainWindow() {InitializeComponent();string xamlFile = "Window1.xaml";//设置窗体的属性this.Width = this.Height = 600;this.Left = this.Top = 100;this.Title = "动态加载 XAML";//从外部文件获取xaml的内容DependencyObject rootElement;//定义一个依赖来接收xaml文件读取的内容using (FileStream fs = new FileStream(xamlFile, FileMode.Open)){rootElement = (DependencyObject)XamlReader.Load(fs);}this.Content = rootElement;//将依赖的内容赋值给窗体的content属性//从xaml的内容中找到button1名字的按钮,并赋值给mybutton对象button1 = (Button)LogicalTreeHelper.FindLogicalNode(rootElement, "button1");//添加事件处理程序button1.Click += Btn_Click; }
加载主要用到了XamlReader.Load这个函数,需要的参数就是我们构造函数传进来的XAML文件的名字 .
如果需要对XAML里面的组件进行操作,比如加一些响应函数,那么我们需要在主窗口类里面添加一个对应组件的私有变量,然后利用LogicalTreeHelper.FindLogicalNode()这个函数,第一个参数为xaml读取的依赖变量,第二个参数是要操作的组件名字。
//添加事件处理程序button1.Click += Btn_Click;
然后在主窗口类中对这个响应函数进行声明并实现 :
private void Btn_Click(object sender, RoutedEventArgs e) {button1.Content = "Thank You"; }
xaml的文件的内容主要如下:(并且该文件需要放到工程的DEBUG目录下)
下面是结果(左图为单击之前,右图为单击后):
使用代码和编译过的 XAML
优点:
有些内容可以自动生成。不必使用LogicalTreeHielper类进行ID 查找,也不需要在代码中关联事件处理程序。
在运行时读取 BAML 比读取 XAML 的速度要快。
部署更简单。因为BAML 作为一个或多个资源嵌入到程序集中,不会丢失。
可在其他程序中编辑 XAML文件,例如设计工具。这为程序编程人员和设计人员之间更好地开展协作提供了可能(当使用未编译的XAML 时,也能获得这个好处,如上一节所述)。
当编译 WPF 应用程序时,Visual Studio 使用分为两个阶段的编译处理过程。第一阶段将XAML 文件编译为 BANL。例如,如果项目中包含名为Windowl.xaml 的文件,编译器将创建名为 Window1.baml 的临时文件,并将该文件放在项目文件夹的 obj/Debug子文件夹中。同时,使用选择的语言为窗口创建部分类。例如,如果使用 C#语言,编译器将在 objDebug 文件夹中创建名为 Windowl.g.cs 的文件。g代表生成的(generated)。部分类包括如下三部分内容:
窗口中所有控件的字段。
从程序集中加载 BAML 的代码,由此创建对象树。当构造函数调用 InitializeComponent()方法时将发生这种情况。
将恰当的控件对象指定给各个字段以及连接所有事件处理程序的代码。该过程是在名为 Connect()的方法中完成的,BAML 解析器在每次发现一个已经命名的对象时调用该方法一次。
部分类不包含实例化和初始化控件的代码,因为这项任务由 WPF 引擎在使用Application.LoadComponent()方法处理 BAML 时执行。下面的Window1.g.cs 文件中显示了Window1.xaml 自动生成的设计文件:
#pragma checksum "..\..\Window1.xaml" "{8829d00f-11b8-4213-878b-770e8597ac16}" "B05650BB07F587FFFA0A74D79F7D16BE36340DC8A7549CBED83BE7C1E6CED4F1" //------------------------------------------------------------------------------ // <auto-generated> // 此代码由工具生成。 // 运行时版本:4.0.30319.42000 // // 对此文件的更改可能会导致不正确的行为,并且如果 // 重新生成代码,这些更改将会丢失。 // </auto-generated> //------------------------------------------------------------------------------using System; using System.Diagnostics; using System.Windows; using System.Windows.Automation; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Ink; using System.Windows.Input; using System.Windows.Markup; using System.Windows.Media; using System.Windows.Media.Animation; using System.Windows.Media.Effects; using System.Windows.Media.Imaging; using System.Windows.Media.Media3D; using System.Windows.Media.TextFormatting; using System.Windows.Navigation; using System.Windows.Shapes; using System.Windows.Shell; using WpfApp1;namespace WpfApp1 {/// <summary>/// Window1/// </summary>public partial class Window1 : System.Windows.Window, System.Windows.Markup.IComponentConnector {#line 10 "..\..\Window1.xaml"[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields")]internal System.Windows.Controls.Button btn1;#line default#line hiddenprivate bool _contentLoaded;/// <summary>/// InitializeComponent/// </summary>[System.Diagnostics.DebuggerNonUserCodeAttribute()][System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")]public void InitializeComponent() {if (_contentLoaded) {return;}_contentLoaded = true;System.Uri resourceLocater = new System.Uri("/WpfApp1;component/window1.xaml", System.UriKind.Relative);#line 1 "..\..\Window1.xaml"System.Windows.Application.LoadComponent(this, resourceLocater);#line default#line hidden}[System.Diagnostics.DebuggerNonUserCodeAttribute()][System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "4.0.0.0")][System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)][System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes")][System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")][System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1800:DoNotCastUnnecessarily")]void System.Windows.Markup.IComponentConnector.Connect(int connectionId, object target) {switch (connectionId){case 1:this.btn1 = ((System.Windows.Controls.Button)(target));#line 10 "..\..\Window1.xaml"this.btn1.Click += new System.Windows.RoutedEventHandler(this.Button_Click);#line default#line hiddenreturn;}this._contentLoaded = true;}} }
本章小结
本章分析了一个简单的 XAML 文件,同时分析了 XAMIL 的语法。下面列出本章中介绍的
内容:
- 介绍了 XANL 的主要组成部分,如类型转换器、标记扩展和附加属性。
- 学习了如何连接可以处理由控件触发的事件的代码隐藏类。
- 介绍了将标准的WPF 应用程序编译成可执行文件的编译过程。
课后作业
无