1、深入浅出 WPF 全系列教程及源代码前言:WPF 之 What。在 XAML中做同样的事情也需要添加对程序集的引用,然后再在根元素的起始标签中写上一句:xmlns:c=“clr-namespace:System.Windows.Control;assembly=PresentationFramework“。c 是映射的前缀,换成其它字符串也可以。因为 button 来自前缀为 c 的 命名空间,所以在使用butto n 的时候就要使用。xmlns:c=“clr-namespace:System.Windows.Control;assemble=PresentationFramework“,这么
2、长的一串字符串看上去的确有点恐怖,但不用担心,VS2008 中有自动提示功能。在 VS2008 自动提 示的顶部,你会看到几 个像网站地址的几个名称空间,其中就包含例子代码中的那两行。为什么名称空间看上去想一个网站地址呢。其实把它 copy 到浏览器地址栏尝试跳转也不会打开网页。这里只是 XAML 解释器的一个硬 性编码(hard-codin g),只要见到这些固定的字符串,就会把一系列的程序集和程序集中包含的名称空间引入进来。默认引入的这两个名称空间格外的重要,它们对应的程序集和.net 名称空间如下:http:/ 对应:System.Windows;System.Windows.Autom
3、ation;System.Windows.Control;System.Windows.Control.Primitives;System.Windows.Data;System.Windows.Document;System.Windows.Forms.Intergration;System.Windows.Ink;System.Windows.Input;System.Windows.Media;System.Windows.Media.Animation;System.Windows.Media.Effects;System.Windows.Media.Imaging;System.Wi
4、ndows.Media.Media3D;System.Windows.Media.TextFormmatting;System.Windows.Navigation;System.Windows.Shapes;也就是说你可以在 XAML 中可以直接使用这些 CLR 名称空间下的 类型(因为默认 XML 名称空间前没有前缀)。http:/ 则对应一些与XAML 语法和编译相关的CLR名称空间, 使用这些名称空间中的类型需要加上前缀 x,因为它们被映射到 x 的XML名称空间中。从这两个名称空间的名字和它所对应的.NET 程序集上,这个不难看出,第一个空间名称对应的是绘制 UI相关的程序集,是表示
5、(Presentation)层面上的东西;第二个名称空间则对应着 XAML解析处理相关的程序集,是语言层面上的东西。还剩下 x:Class=“WpfApplication2.Window2“这个 Attribute。x 前缀说明这个 Attribute 来着于 x映射的名称空间-前面我们解释过,这个名称空间对应 XAML 解析功能的。x:Class,顾名思义他与类有一些关系,是何种关系呢,让我们做一个有趣的实验:首先,x:Class=“WpfApplication2.Window2“这个 Attribute 删掉,再到 Windows.xaml.cs 文件里,把构造中对InitalizeCom
6、ponent 方法的调用也删掉。编译程序,你会发现,程序依然可以运行,为什么呢?打开 App.xaml 这个文件,你会发现这 样一个 Attribute-StartupUri=“Window1.xaml“,是它告诉编译器把 Window1.xaml 作为程序启动的主窗体。也就是说,只要 Windows1.Xaml 能够被解析为一个窗体,程序就能够运行。然后只恢复x:Class=“WpfApplication2.Window2“这个 Attribute(不恢复InitalizeComponent 方法的调用)。编译之后仍然可以运行,这是使用 IL Disassembler(中间语言凡编译器)打开
7、项目的 编译结果,你会发现在由项目编译生成的程序集里面包含一个名为 Window2 的类,如下图所示这说明,XAML 这个 Attribute的作用是当XAML 解析器将它的标签解析成C#类之后,这个类的类名是什么。这里已经触碰到 XAML 的本质。前面我们已 经看到,事例代码的结构就是使用 XAML语言直观的告诉我们,当前的窗体是一个里面嵌入了一个。如果是使用C# 完成同样的设计呢?显然,我们不可能去更改 Window 这个类,我们能做的是从 Window 派生一个类,再为这个类添加一个 Grid 类型的字段,然后把这个字段初始化的时候赋值给派生类的类容属性。代码看起来大概是这样:cshar
8、p view plaincopyprint?1.using System; 2.using System.Collections.Generic; 3.using System.Linq; 4.using System.Text; 5.using System.Windows; 6.using System.Windows.Controls; 7.using System.Windows.Data; 8.using System.Windows.Documents; 9.using System.Windows.Input; 10. using System.Windows.Media; 11
9、. using System.Windows.Media.Imaging; 12. using System.Windows.Shapes; 13.14. namespace WpfApplication2 15. 16. / 17. / Window2.xaml 的交互逻辑 18. / 19. public partial class Window2 : Window 20. 21. private Grid grid; 22. public Window2() 23. 24. grid = new Grid(); 25. this.Content = grid; 26. 27. 28. 最
10、后让让我回到最初的代码。你可能会问:在 XAML里面有x:Class=“WpfApplication2.Window2“,在 Windows2.xaml.cs 里面也声明了 Window2这个类,难道他们不会冲突吗?仔细看看 Windows2.xaml.cs 中 Window2的声明就知道了-在声明的时候使用的是partial 关键字 ,这样,这样由 XAML 中解析成的类和 C#文件里面定义的部分就合二为 1 了,正是由于这种 partial 机制,我们可以把逻辑代码留在.cs 文件里,用 C#语言来实现,而把那些声明及布局 UI 元素的代码分离出去,实现 UI 和逻辑分离,并且,用于绘制
11、UI 的代码(如声明控件 类型的字段,设置它们的外观和布局等) 也不必再使用C#语言,使用 XAML 和 XAML 编辑工具就可以轻松搞定。至此,你应该对这个简单的 XAML 程序了然于胸了。WPF 学习 -系统的学习 XAML 语法1,XAML 文档的树形结 构:UI 在用户眼里面是个平面结构。如下图所示,在用户眼里看来,这个界面就是一个窗体里面平铺了4 个文本框和一个按钮的界面。在传统的 Visual C+、Delphi 、Visua l Basic6.0 和 Windows Form 程序员的思维里,UI 也是一个平面的结构。因此,程序员要做的事情就是根据美工给的给定的 UI 布局把控件
12、安置在窗体的表面,并用使用长度,宽度和间距把控件对齐。与传统的设计思维不同,XAML 使用树形逻辑结构来描述UI,下面是用来描述界面布局的XAML 代码:html view plaincopyprint? 1. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 因为代码中有许多对 Attribute 的属性,所以结构看起来并不是那么清晰。如果我们把对Attribute的赋值都去掉,那么上面的代码就显现了它的树形框架结构。 html view plaincopyprint? 1. 2. 3. 4. 5. 6. 7. 8. 9. 10. 11. 12. 13.
13、如果用一张图来表示上面的那段代码,它会是下面这个样子 有意思的是,针对一个“看上去一样”的 UI 布局,XAML 代码不一定是唯一的。拿上面的UI 代码布局来说,我们还可以使用不同的XAML 代码来描述它。html view plaincopyprint? 1. 5. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 精简后的代码是: html view plaincopyprint? 1. 2. 3.4. 5. 6. 7. 8. 9. 10. 11. 12. 框架变成了如图所示的样子: 虽然两段代码
14、对 UI 的实现方式不同,但是框架都是树形的,以对象为根节点,一层一层向下包含。这种树形结构对于 WPF 整个体系都具有非常重要的意义,它不但影响着 UI 的布局设计,还深刻的影响着 WPF的属性(Property)子系统和事件(Event)子系统等方方面面。在实际的编程过程中,我们经常要在这棵树上进行按名称查找元素,获取父/子节点等操作,为了方便操作这棵树,WPF 基本类库为程序员准备了 VisualTreeHelper和 LogicTreeHelper 两个助手类(Helpe r Class),同时还在一些重要的基类里封装了一些专门用于操作这棵树的方法。你也许可能会问:既然有这么多方法可以
15、实现同一个UI,到底应该选择哪一种方式来实现UI 呢?实际上,设计师给出的 UI 布局是软件的一个静态快照(Static Snap),这个静态快照加上用户有可能动态操作才能够构成选择实现布局形式的完整依据,拿上面两段代码来说,如果你希望用户在改变窗体大小后需要等比例缩小自己内部控件的尺寸,那么你选择第二种,如果只希望控件在界面上做一个简单的排列,第一种足矣。2,XAML 中为对象赋值 的方法XAML 是一种声明性语言,XAML 会为每一个标签创建一个 与之对于的对象,对象创建之后要对它的属性进行必要的初始化之后才有使用意义。因为 XAML 语言不能够编写程序的运行逻辑,所以一份 XAML 文档
16、除了使用标签声明对象就是初始化对象属性了。注意:XAML 中对对象赋值总共有两种 方法:A:使用字符串 进行简单赋 值。B:使用属性元 素 (Property Element)进行复杂赋值。我用一个标签的 Fill 为例来介绍这两种方法:2.1 使用标签的Attribute 为 对象属性赋值前面我们已经知道,一个标签的Attribute 有一部分与对 象的 Property对应,标签里面的Fill 这个 Attribute就是这样,他与 Rectangle 类对象的Fill 属性对应,在 MSDN文档库里可以查询到,Retangle 类的Fill 类型是一个 Brush。Brush是一个抽象类
17、,凡是已 Brush 为基类的类都可以成为 Fill 的属性值。Brush的派生类有很多: SolidColorBrush:单色画刷。 LinearGradientBrush:线性渐变画刷。 RadialGradientBrush:径向渐变画刷。 ImageBrush:位图画刷。 DrawingBrush:矢量图画刷 。 VisualBrush:可视元素画刷。下面这个例子是使用 SolidColorBrsh 和 LinearGradientBrush 两种。我们先学习使用字符串对 Attribute的简单赋值,假设我们的 Rectangle 只需要填充成单一的蓝色,那么我们只需要简单的写成:h
18、tml view plaincopyprint? 1. 5. 6. 7. 8. 运行效果如下图: 我们看到,blue 这个字符串最终被翻译成了 SolidcolorBrush 并赋值给了 Rectangle对象。换成 C#代码是这样。csharp view plaincopyprint? 1. SolidColorBrush brush = new SolidColorBrush(); 2. brush.Color = Colors.Blue; 3. this.rectangle1.Fill = brush; 需要注意的是,这种 Attribute=Value的赋值时,由于 XAML 语法有限
19、,Value 只能是一个字符串值,这就引发了下面两个问题: A,如果一个类可以使用 XAML 类来进行声明,并允许它的 的 Property 可以和它的Attribute 互相映射,那就需要为这些 Property准备适当的转换机制。B,由于 Value 是个字符串,所以其格式 复杂程度有限,尽管可以在转换机制里面包含一定的按格式解析字符串的功能以便转换成较复杂的目标对象,但这会让最终的 XAML 使用者头疼不已,因为他们不得不在一个没有编码辅助的情况下手写一个格式复杂的字符串来满足需求。第一个问题的解决方式是使用 TypeConverter 类的派生 类,在派生类里面重新TypeConver
20、ter 的一些方法,第二个问题 的解决办法就是使用属 性元素(Property Element)。2.2 使用 TypeConverter 类将 XAML 标签的Attribute 与对象的 Property 进行映射注意本小节的例子对于初学者来说理解起来比较困难而且实用性不大,主要是为喜欢刨根问底的 WPF 开发人员准备的,初学者可以跳过这一节。首先我们准备一个类:csharp view plaincopyprint? 1. public class Human 2. 3. public string Nameget;set; 4. public Human Child get; set;
21、5. 这个类具有连个属性: String 类型的 Name;Human类型的 Child;现在我们期望的是,如果我们在XAML 里面这样写:html view plaincopyprint? 1. 2. 3. 则能够为 Human 实例的 Child 属性赋一个 Human 类型的值,并且 Child.Name 就是这个字符串的值。 我们点看看直接写行不行。在 UI 上添加一个 Button,并在 Click 事件里面写上:csharp view plaincopyprint? 1. Human human = (Human)this.FindResource(“human“); 2. Mes
22、sageBox.Show(human.Child.Name); 编译没有问题,但是在单击按钮的时候会弹出异常,告诉 Child 不存在,为什么 Child 不存在呢?原因很简单,Human 的 Child 属性是 Human 类型,而 XAML代码中的 ABC 是个字符串,编译器不知道如何将一个字符串转换成一个 Human 实例。那我们应该怎么办呢?办法是使用TypeConverter 和 TypeConvertAttribute 这两个类。 首先,我们要从 TypeConvert派生出自己的类,并重写它的ConverFrom方法。这个方法有一个参数名叫做 Value,这个值就是XAML 文档
23、里面为其设置的值。我们要做的就是将这个值翻译成合适对象的值并赋值给对象的属性:csharp view plaincopyprint? 1. public class StringToHumanTypeConverter:TypeConverter 2. 3. public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value) 4. 5. if(value is string) 6. 7. Human human = n
24、ew Human(); 8. human.Name = value as string; 9. return human; 10. 11.return base.ConvertFrom(context, culture, value); 12. 13. 有了这个类还不够,你还需要使用 TypeConvertAtrribute 这个特征类把StringToHumanTypeConverter 这个类“粘贴“ 到作为目标 的 Human 类上。 csharp view plaincopyprint? 1. TypeConverterAttribute(typeof(StringToHumanTyp
25、eConverter) 2. public class Human 3. 4. public string Nameget;set; 5. public Human Child get; set; 6. 因为特征类在书写的时候可以省略Attri bute 这个词,所以也可以写作: csharp view plaincopyprint? 1. TypeConverter(typeof(StringToHumanTypeConverter) 2. public class Human 3. 4. public string Nameget;set; 5. public Human Child ge
26、t; set; 6. 但这样写,我们需要认清括号里是TypeConverterAt tribute 而不是TypeConverter。 完成之后我们再单击按钮,我们想要的结果出来了,如下图:注意:TypeConverter 类的使用远远不 只是重载一个 ConvertFrom 方法这么简单,为了配合这个方法的运行,还需要重载其它几个方法。详细的使用方法请参见TypeConver t 类库文档。2.3 属性元素在 XAML中,非空标签均具有自己的 内容(Conte nt)。标签的内容就是指夹在起始标签和结束标签中的一些子级标签,每个子级标签都是父级标签中的一个元素(Element),简称为父级标
27、签的一个元素。顾名思义,属性元素指的就是某个标签的一个元素对应这个标签的一个属性,即已元素的形式来表达一个实例的属性。代码描述为:html view plaincopyprint?1. 2. 3. 4. 这样,这个标签的内部就可以使用对象(而不仅限于字符串)进行赋值了。 如果把上面的例子用标签语法改写一下,XAML 将是这样 :html view plaincopyprint?1. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 效果和之前的效果一样,对于简单的赋值而言,属性标签体现不出来什么优势,反而让代码看起来有点冗长。但遇到属性复杂的对象这种语法的
28、优势就体现出来了,如使用线性渐变来填充这个矩形: html view plaincopyprint?1. 6. 7. 8. 9. 10. 11. 12.13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 23. 24. 25. 26. 27. 28. 29. 30. LinearGradientBrush 的GradientStops 属性是一个 GradientStop的集合(GradientStopCollection),即一些列的矢量渐变填充点。在这些填充点之间,系统会自己进行插值计算,计算出过渡色彩。填充矢量方向是 StartPoint 和 EndPoint
29、 两个属性(类型为 Point)的连线方向 ,矩形的左上脚为 0,0,右下角为 1,1。这段代码中,针对这三个属性都是使用属性标签的赋值方法。古语道,“过犹不及 ”。上面的代码 为了突出属性元素我将 所有的属性都使用属性元素的方法进行赋值,结果代码的可读性一落千丈。经过优化,代码变成了如下,少了 1/3。html view plaincopyprint?1. 6. 7. 8. 9. 10. 11. 12. 13. 14. 15. 16. 17. 18. 19. 20. 21. 22. 这里几个简化 XAML的技巧: 能用 Atrribute=value方式进行赋值的就不要使用属性元素。充分利用
30、默认值,除去冗余。充分利用 XAML 的简写方式: XAML 的简写方式很多,需要在工作 中慢慢积累。WPF 学习 之 X 名称空间详解X 名称空间里面的成员(如 X:Name,X:Class)都是写给 XAML 编译器看的、用来引导XAML 代码将 XAML代码编译为CLR 代码。4.1X 名称空间里面到底都有些什么?x 名称空间映射的是:http:/ XAML 语言相关,所以亦称之为“XAML 名称空间 ”。与 C#语言一样,XAML 也有自己的编译器。XAML 语言被解析并编译,最终形成微软中间语言保存在程序集中。在解析和编译 XAML的过程中,我们经常要 告诉编译器一些重要的信息,如
31、XAML编译的结果应该和哪个C#代码编译的结果合并、使用 XAML 声明的元素是public 还是 private 访问级别等等。这些让程序 员能够与 XAML 编译器沟通的工具就存在 X:名称空间中。我们注意到,它分为 Attribute、标签扩展、XAML 指令元素三个 种类。下面我们讲讲它们的具体用法:4.2 X 名称空间中的 Attribute前面我们已经讲过,Attribute 和Property 是两个层面上的东西,Attribute 是语言层面上的东西, 是给编译器看的,Prope rty 是面向对象层面上的东西, 是给编程逻辑看。而且一个标签中的Attribute 大部分 对应
32、对象的Property。在使用 XAML 编程的时候,如果你想给它加一点特殊的标记来改变 XAML 对它的解析,这时候就需要额外的给 它添加一些Attri bute 了。比如,你想告诉 XAML 编译器将哪个编译结果和那个 C#编译的类合并,这时候就必须为这个标签添加 X:Class Attribute 来告诉编译器。X:Class并不是对象成员,而是重 X 空间硬贴上去的。让我们浏览一下常用的Attri bute。4.2.1 x:Class这个 Attribute 是告诉 XAML 编译器将 XAML 编译器编译的结果和 后台编译结果的哪一个类进行合并,使用 x:Class有以下几点要求:
33、这个 Attribute 只能用于根节点。 使用 x:Class的根节点的类型要与 x:Class的值所指示的一致。 x:Class的值所指示的类型在声明的时候必须使用 partial关键字。 x:Class已经在剖析最简单的 XAML 的时候已经讲过,在这就不多讲了。4.2.2 X:ClassModiffier这段代码是告诉 XAML 编译器有标签编译成的类具有什么样的访问 级别。使用这个 Attribute 的时候需要注意 的是: 标签必须具有 x:Class Attribute。 X:ClassModiffier 的值必须与 X:Class所指定类的访问权限一致。 X:ClassModi
34、ffier 的值随后台代码编译语言的不同而有所不同。4.2.3 X:Name我们之前已经提过 XAML 是一种声明式语言,但你是否想 过 XAML标签声明的是什么呢?其实,XAML 标签声明的是对象,一个 XAML 对应着一个对象,这个对象一般是一个控件类的实例。在.NET 平台上,类是引用类 型。引用类型的实例一般都是以“引用者-实例”的形式成对出现的,而且我们只能通过引用者来访问实例。当一个实例不在被任何引用者引用的时候,它将作为内存垃圾被销毁。常见的引用者是引用变量,但不是唯一的。比如下面这段 XAML代码:html view plaincopyprint?1. 5. 6. 7. 8.
35、9. 10. 11. 这篇代码中通篇没有出现一次名字,但是我们可以通过引用者的层级关系来找到我们最终想要的控件,我们在 Button 的Click下写如下代码:csharp view plaincopyprint?1.private void Button_Click(object sender, RoutedEventArgs e) 2. 3.StackPanel panel = this.Content as StackPanel; 4.TextBox textBox = panel.Children0 as TextBox; 5.if (!string.IsNullOrEmpty(tex
36、tBox.Name) 6. 7.textBox.Text = textBox.Text; 8. 9.else 10. 11. textBox.Text = “NULL“; 12. 13. this.Content 引用着 StackPanel的实例,Stack Panel.Children0又引用着 TextBox 的实例。知道了这个关系,就可以一路顺着查找下来并同时进行类型转换,最终 TextBox 中显示的值是NULL。理论上我们可以用上面的方法访问到 UI 上的所有元素,但这毕竟太麻烦了。换句话说:XAML 这 种对象声明语言只负责声明对象而不负责为这些对象声明 引用变量。如果我们需要为
37、对象准备一个引用变量以便在C#中直接访问就必须显示的告诉 XAML 编译器- 为这个对象声明引用变量,这时候,X:Name 就派上用场了。注意:X:Name 的作用有两个:(1)告诉编译器,当一个标签 带有 x:Name 时,除了为这个标这个还真不能确定!签生成实例还要给这个标签声明一个引用变量,变量名就是x:Name的值。(2)将 XAML标签所对应 的 Name属性( 如果有)也设置为 x:Name 值,并把这个值注册到 UI树上,以方便查找。4.2.4 x:FieldModifier使用了 x:Name 后,XAML 标签对应的实例就具有了自己的引用 变量,而且这些引用变量都是类的字段,
38、既然这样就不免要关注一下它的访问级别。默认情况下这些字段的级别都被设置成了 Internal。在编程的时 候,有的时候需要用一个程序集里的一个窗体元素访问到另一个程序集的窗体元素,那么就需要使用 x:FieldModifier 来改变变量的访问级别!html view plaincopyprint?1. 2. 3. 4. 因为 x:FidleModifier 是应用变量的访问级别,所以要配合x:Name 一起使用。否则没有引用变量,何来引用变量访问级别。4.2.5 x:Key最自然的检索方式莫过于”key-value “的形式了。在 XAML文件中,我们可以把需要多次使用的类容提取出来放在资源字典中,需要使用的时候就用这个资源的 key 将这个资源检索出来。