WPF/Silverlight编程:控件定制
概述
本文聚焦在如何定制WPF/Silverlight UI控件上,通过一个具体的UI样式实例,说明XAML的写法,解释控件模型和动画模型。
“让我们乐于创造出个性的UI界面吧!”---- 在WPF/Silverlight中定制界面真的很容易。
文中纰漏,敬请不吝赐教,一同讨论。
理解XAML
XAML是基础,所以我们先来看看它。另外,对于本文涉及的代码,可以使用Visual Stidio 2008,当然还需要安装silverlight的sdk和toolkit,才能在VS2008中新建Silverlight的项目。
在看过WPF/Silverlight编程:理解应用程序编程模型中对XAML产生过程的介绍,了解到XAML是基于XML的,它的全称是“eXtensible Application Markup Language”,对于其基础,我推荐w2school xml,当然与XML相关的也可以在其上找到,尤其是xhtml,css,这对理解XAML以及WPF/Silverlight的界面布局方式都有好处。有一句话是这样的“HTML用来显示数据,XML用来描述数据”,所以XAML也只是用来描述界面的构成,显示界面的事由WPF/Silverlight运行时库来做了。
功能
总的来说,XAML用来做两件事情:定义界面结构布局、定义界面所使用的资源。
所以,在XAML的产生中提到的XAML的功能就可以分成这两部分:
1,定义界面所使用的资源:
定义资源集合;
定义控件模板;
定义动画;
2,定义界面结构布局:
定义界面整体的Layout;
装备事件处理器;
编写数据绑定表达式;
但实际上,也可以完全将c#代码写在XAML文件中,但这种应用不提倡,也几乎没有看到实际的应用。
分类
XAML根据使用的用途,可以分为以下几类,或者说是XAML的子集:
1,WPF XAML:这也是XAML最初被发明时适用的框架集,适用于描述WPF程序界面。
2,Silverlight XAML:WPF XAML的子集,适用于描述silverlight程序界面。
3,XPS XAML:WPF XAML的一部分,用来描述电子格式的文档。现在已经属于XPS规格的一部分。
4,WF XAML:适用于描述Windows Workflow Fundation框架的内容,和WPF XAML的功能有较大差异。
我们这里当然只关心前两者。
解析
WPF/Silverlight框架使用XAML文件有两种方式:适用XamlReader直接读取XAML文件、将XAML编译成为BAML。WPF使用前者,Silverlight使用后者。
另外,有一种格式的XAML可以被IE直接打开,这种叫loose XAML,表现得就如一个HTML。
基本规则
根据XML的惯例,XAML上的每个元素(Element)指的是从(且包括)开始标签直到(且包括)结束标签的部分。元素可包含其他元素、文本或者两者的混合物。元素也可以拥有属性。元素是可扩展的。
三条基本的规则:
1,每个Element映射为一个.NET的类的实例。元素的名称要和类的名称完全一致,是区分大小的。元素的属性,对应为类的属性。如下:
<Button FontSize="20">Test</Button> <!-- 这个XAML元素映射为System.Windows.Controls.Button的一个实例对象,FontSize是类Button的属性。-->
2,对于任何Element,可以内嵌其它的element。这体现了控件的扩展性。也是体现界面布局的“父 - 子 - 兄弟”关系的要求,这其实也是依赖项属性和路由事件的根源。如下代码:
<StackPanel> <Button Content="Test." /> <Button Content="Test 2." /> </StackPanel>
3,可以为元素,也就是类对象,设置属性。有时还可以使用特殊的语法,内嵌tags。上面的代码中就给Button元素设置了Content属性。
另外,每个XAML文件的根元素,都要定义XML名称空间到WPF专门的域上,具体的内容参考:MSDN WPF XAML。
隐藏代码
工具可以从XAML文件自动生成C#代码,这部分代码叫隐藏代码。这里说明一下简单的映射关系。
当在代码中,写到<Window x:Class="WindowsApplication1.Window1"之类的代码,隐藏代码会是这样的:
namespace WindowsApplication1 { /// <summary> /// Interaction logic for Window1.xaml /// </summary> /// 注意c#语法关键字partial 为代码自动生成提供了直接支持,这样开发人员就可以不用修改工具生成的这个类Window1,而又能新建类Window1的另一部分了。 public partial class Window1 : Window { public Window1() { InitializeComponent(); //不细说是什么了,具体在VS2008中跟踪下去就知道了。自己写的代码不要放在这一行的上面。 } } }
重点在于XAML的x:Class特性(attribute),前面的x表示了XAML中定义的专有域名,这个Window元素对应了类Window,而在x:Class中描述的,就是它的一个子类。
而在XAML中类似<Grid x:Name="grid1"> </Grid>,就会被生成为private System.Windows.Controls.Grid grid1;
像上面的grid1在XAML中,叫做Naming Element,具名元素。只有具名元素才会被生成到代码中,因为它们会被其它元素引用,这样在代码中,就可以直接使用grid1这个Grid对象的引用了。
殊字符和空格
对于一些特殊字符,如 & < > 还有空格,需要特殊对待,在XAML中要使用转换字符串来代替它们。
< -- & lt; // 去掉&后的空格,见下面的代码,如果没有空格,wiki直接显示出<
> -- & gt;
& -- & amp;
" -- & quot;
很好记,lt就是less that的意思,所以表示<,gt就是great than。还要注意不要缺少最后的;号。比如:
<Button Content="<"Click & Me">" /> <!-- Button上显示<"Click & Me"> -->
对于空格,要复杂一点,要使用xml:space="preserve",比如:
<TextBox Name="txtQuestion" xml:space="preserve"> [There is a lot of space inside these quotation marks " ".] <!--这样,双引号中间的空格将会被保留。--> </TextBox>
理解资源(Resources)
我们可以把用户界面定义为视觉元素的组合,这些视觉元素又是由界面资源构成的。定义界面和控件定制,其实就是对界面使用资源的定义和组合。而通 过用户操作表现出来的界面动态效果,如,颜色的动态变化、字体的大小变化、图片的缩放、文字倒影、窗体的渐隐渐现等,都是针对颜色、字体、图片、字符串、 窗体布局等资源进行的,这是一种行为,属于动画的范畴。
资源具有不可扩展性,比如颜色只能是一种具体的颜色,不可能颜色之中还有字体资源。但是资源之间可以相互使用,比如字体颜色。这也说明,资源具有不能同程度的原子性,有具体的资源,也有抽象的资源。抽象的资源往往聚合了其它类型的资源。
使用资源,首要的目的就是复用性,随之而来的就是统一,那当然修改和管理起来也就很容易了。
类型
WPF/Silverlight中的资源可以分成两大类:
1,Assembly Resources:是二进制格式的,比如图片文件、声音文件和视频文件等等。
2,Object Resources:是.NET对象,比如字符串、颜色、尺寸、形状(shape)、画笔,图形 (geometry)、坐标、间距、填充、字体(大小,样式, etc)等等。更广义的说,类库中定义的控件也是object resources。不过,WPF/Silvelight中的控件被设计定义为lookless的,后文再说。
前者独立存在,并且有对应的后者引用。
上面的分类方法还是从实现的角度来说的,其实对我们没有多大的益处,只是适合软件进行处理。
资源还可以根据可视性来分成:
1,不可视资源:如坐标、间距、填充、字符串。其实前三个也是通过界面元素的相对可视位置体现出来的。
2,可视资源:如颜色,字体,图片,画笔,形状,尺寸,图形。
请注意这里的区别,同样是一个显示了“Click Me”的按钮,我们要区别Click Me字符串不可视资源,与使用了某个字体让这个内容数据显示出来的可视资源!这是理解WPF/Silverlight lookless控件的一个关键。只有这样区别于传统图形编程框架中将某个控件看成一个整体的思考方法,才为后面WPF/Silverlight控件的视 觉效果可以重新定义(也就是控件定制)提供了新的思路。
我们关注可视资源与不可视资源的分离,相比可视资源,不可视资源具有更好的复用性,体现的是软件的功能性。而可视资源得重点就是给用户提供不同的视觉体验。这也是我们所要的:"数据都不要动,最上面的界面修改掉"。
组织方式
根据资源是否可以在项目之间复用,可以把资源放到Resource Collection和Resource Dictionary中。
每个XAML中的元素都有一个Resource属性,这里叫Resource Collection。另外有一个单独的类ResourceDictionary,用来组织后者。
Resource Dictionary是被定义在一个单独的XAML文件中的,被不同的项目间复用时,被具体的项目当作资源的source,而导入。它也会被编译成为BAML,以提高效率。如下:
<Application x:Class="Resources.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Menu.xaml" > <Application.Resources> <ResourceDictionary> <ResourceDictionary.MergedDictionaries> <ResourceDictionary Source="AppBrushes.xaml"/> <!-- 导入Resource Dictionary中的资源 --> <ResourceDictionary Source="WizardBrushes.xaml"/> </ResourceDictionary.MergedDictionaries> </ResourceDictionary> </Application.Resources> </Application> <!-- AppBrushes.xaml --> <ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <ImageBrush x:Key="TileBrush" TileMode="Tile" <!--ImageBrush专门使用外部图片资源渲染背景或字体--> ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"> </ImageBrush> </ResourceDictionary>
对于定义在Resource Collection中的资源,要看这个Collection定义在什么地方,只有定义了它的元素内部才可以使用。Resource Collection可以定义在应用程序下、界面的根元素下、控件元素下。如下所示:
<Application x:Class="Resources.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="Menu.xaml" > <Application.Resources> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"> </ImageBrush> </Application.Resources> </Application>
上面的Resource Collections定义在应用程序下,可以在这个应用程序的所有界面定义XAML中使用。
<Window x:Class="Resources.TwoResources" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Resources" Height="300" Width="300" > <Window.Resources> <!--这个Resource Collection定义在这个XAML文件的根元素下,可以被这个文件内的所有元素使用--> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"> </ImageBrush> </Window.Resources> <StackPanel Margin="5"> <Button Background="{StaticResource TileBrush}" Padding="5" FontWeight="Bold" FontSize="14" Margin="5" >A Tiled Button </Button> <Button Padding="5" Margin="5" FontWeight="Bold" FontSize="14">A Normal Button </Button> <Button Background="{DynamicResource TileBrush}" Padding="5" Margin="5" <!--这里使用了动态引用方式,使用的就是下面的TileBrush资源--> FontWeight="Bold" FontSize="14"> <Button.Resources> <!--这个Resource Collection直接定义在控件内,就只能被这个控件使用,同时它也覆盖了根元素下的同名资源--> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="sadface.jpg" Opacity="0.3"> </ImageBrush> </Button.Resources> <Button.Content>Another Tiled Button</Button.Content> </Button> </StackPanel> </Window>
使用方式
对于定义在Resource Dictionary中的资源,在导入后,可以和像放在Resource Collection中的资源一样,通过定义的名字来引用。
控件使用资源,分成静态和动态的方式。静态是指在控件引用这个资源之前,该资源就已经在XAML中被定义好了;动态的情况下,就是XAML中控件引用的资源会在后面才会定义。
它们各有目的,静态的方式是常见的。动态的方式也有其应用的场景:1,使用当前系统设置的资源;2,希望使用代码自由控制。定义在XAML中的资源是不能在代码中修改的,代码中只能动态的切换使用哪个静态资源。
上面的代码中演示了动态的使用方式,静态的引用就是把DynamicResource换成StaticResource。
如果不想让某个资源被使用,比如测试需要,过时了等等,可以如下:
<ImageBrush x:Key="TileBrush" x:Shared="False" > <!--使用x:Shared属性让该资源不可用--> </ImageBrush>
[Kristy]有时候系统会自动生成模板,例如新建一个panorama project,系统自动生成了MainPage.xaml,下面是取了xaml开头的一部分。
<phone:PhoneApplicationPage x:Class="HelloPanorama.MainPage" <!--Namespace--> xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" <!--xmlns的第一个声明,将 Silverlight 核心 XAML 命名空间映射为默认命名空间 --> xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" <!--第二个声明为 XAML 定义的语言元素映射一个单独的 XAML 命名空间,通常将它映射为 x: 前缀:--> xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone" <!--clr-namespace: 在包含要对 XAML 用法公开的公共类型的程序集中声明的公共语言运行时 (CLR) 命名空间--> xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone" xmlns:controls="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="800" d:DataContext="{d:DesignData SampleData/MainViewModelSampleData.xaml}" FontFamily="{StaticResource PhoneFontFamilyNormal}" <!--PhoneFontFamilyNormal,PhoneFontSizeNormal,PhoneForegroundBrush 都是系统默认提供的--> FontSize="{StaticResource PhoneFontSizeNormal}" Foreground="{StaticResource PhoneForegroundBrush}" SupportedOrientations="Portrait" Orientation="Portrait" shell:SystemTray.IsVisible="False"> .... </phone:PhoneApplicationPage >
样式(Style)
从上面的代码示例中看到Button的Background属性使用了TileBrush资源,这是一种直接静态的使用方式。样式(Style)为控件使用资源提供了间接静态的使用方式。间接了,就带来了更多的灵活性,这种方式为控件定制提供了直接的支持。
对于与Object Resources,控件中也定义了能够使用它们的对应的属性,自然地,这些属性体现了这个控件的 可视部分,比如Background可以使用画笔资源,FontFamily, FontSize, FontWeight可以使用字体资源,Height, Width可以使用尺寸资源等等。这些都是控件都有的属性,所以它们可以独立于控件的内容而存在,它们就可以被整理在一个对象中,整体被使用,这就是样式 的作用。
样式是一个集合,包含了对属性的设置,这个样式可以被界面元素使用,简化XAML的编写,复用资源。样式概念的来源,是Web中的CSS概念。
看下面的代码:
<Window.Resources> <FontFamily x:Key="ButtonFontFamily">Times New Roman</FontFamily> <sys:Double x:Key="ButtonFontSize">18</s:Double> <FontWeight x:Key="ButtonFontWeight">Bold</FontWeight> </Window.Resources> <Button Padding="5" Margin="5" Name="cmd" FontFamily="{StaticResource ButtonFontFamily}" FontWeight="{StaticResource ButtonFontWeight}" FontSize="{StaticResource ButtonFontSize}"> A Customized Button </Button>
上面是直接静态引用的方式,下面使用样式:
<Window.Resources> <Style x:Key="BigFontButtonStyle"> <!--使用x:Key为样式命名--> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="18" /> <Setter Property="FontWeight" Value="Bold" /> </Style> </Window.Resources> <Button Padding="5" Margin="5" Name="cmd" Style="{StaticResource BigFontButtonStyle}" > A Customized Button </Button>
看看使用样式的优势:
1,想想一个复杂的用户界面,具体的控件描述简洁了,就能够较容易的看清整体的界面布局结构。
2,Button本身的描述更加自然、易读了,因为它的“实现”细节被封装了起来,通过样式的名称BigFontButtonStyle,可以更清楚地明白这是一个看起来是什么样的Button。
3,控件的可视部分使用样式封装起来之后,可以复用,并且可以控制用户界面的整体风格。
这里要说明的是,能都在样式中被定义的属性必须是依赖项属性。控件属性有很多,其中大部分都是依赖项属性。
可以在Style设置很多内容,其中有几个重要的:
1,Setters。用来设置属性集合,或者装备事件处理方法。前者用Setter,后者用EventSetter。
2,Triggers。描述Trigger集合,允许在触发某种条件后的处理,对事件驱动的进一步抽象和封装。
3,Resources。一组资源的集合,这些资源可以被Setters中的setter使用。这其实就是上面所说的资源的Resource Collection方式。
4,BasedOn。实现样式的继承。
5,TargetType。说明样式适用的控件类型。
下面说明样式的更多使用方式:
1,针对不同控件类型的属性定义样式。注解:这种方式不推荐使用,因为实际上Button.FontFamily和 TextBlock.FontFamily实际上是一样的,下面的代码实际上定义了两次FontFamily属性,自然实际上使用的是后定义的 value。如果要指定样式能够被使用的类型,要用Style的TargetType属性。
<Style x:Key="BigFontStyle"> <Setter Property="Button.FontFamily" Value="Times New Roman" /> <!--属性前加类名.--> <Setter Property="Button.FontSize" Value="18" /> <Setter Property="TextBlock.FontFamily" Value="Arial" /> <Setter Property="TextBlock.FontSize" Value="10" /> </Style> <Style x:Key="BigFontButtonStyle" TargetType="Button"> <!--使用TargetType--> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="18" /> <Setter Property="FontWeight" Value="Bold" /> </Style>
2,在样式中定义事件处理。
<Style x:Key="MouseOverHighlightStyle"> <EventSetter Event="TextBlock.MouseEnter" Handler="element_MouseEnter" /> <!--在代码里实现element_MouseEnter--> <EventSetter Event="TextBlock.MouseLeave" Handler="element_MouseLeave" /> <Setter Property="TextBlock.Padding" Value="5"/> </Style> <TextBlock Style="{StaticResource MouseOverHighlightStyle}"> Hover over me. </TextBlock>
3,样式可以定义在界面根元素的Resource Collection中,也可以单独定义在具体控件的Resource Collection中。注解:这样就不能被其他控件使用了。
<Button Padding="5" Margin="5"> <Button.Style> <!--放在控件内部,这时可以不用放到Button.Resource元素中来描述--> <Style> <Setter Property="Control.FontFamily" Value="Times New Roman" /> <Setter Property="Control.FontSize" Value="18" /> <Setter Property="Control.FontWeight" Value="Bold" /> </Style> </Button.Style> <Button.Content>A Customized Button</Button.Content> </Button>
4,控件可以覆盖样式中的定义。
<Window.Resources> <Style x:Key="BigFontButtonStyle"> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="18" /> <Setter Property="FontWeight" Value="Bold" /> </Style> </Window.Resources> <Button Padding="5" Margin="5" Name="cmd" Style="{StaticResource BigFontButtonStyle}" FontSize="20"> <!-- 控件中描述的FontSize会覆盖掉引用的样式中的定义--> A Customized Button </Button>
5,样式可以继承,形成层次。
<Window.Resources> <Style x:Key="BigFontButtonStyle"> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="18" /> <Setter Property="FontWeight" Value="Bold" /> </Style> <Style x:Key="EmphasizedBigFontButtonStyle" BasedOn="{StaticResource BigFontButtonStyle}"> <!-- EmphasizeBigFontButtonStyle继承了BitFontButtonStyle--> <Setter Property="Control.Foreground" Value="White" /> <Setter Property="Control.Background" Value="DarkBlue" /> </Style> </Window.Resources>
6,根据类型自动附加样式到控件上。注解:不要滥用这个特性。小技巧:只在无名称带有TargetType的样式中定义一些公共的属性,比如Margin,Padding。
<Window.Resources> <Style TargetType="Button"> <!--注意这个样式没有名称,所以会自动附加到所有Button的身上--> <Setter Property="FontFamily" Value="Times New Roman" /> <Setter Property="FontSize" Value="18" /> <Setter Property="FontWeight" Value="Bold" /> </Style> </Window.Resources> <StackPanel Margin="5"> <Button Padding="5" Margin="5">Customized Button</Button> <TextBlock Margin="5">Normal Content.</TextBlock> <Button Padding="5" Margin="5" Style="{x:Null}">A Normal Button</Button> <!--使用{x:NULL}去掉附加上的样式--> <TextBlock Margin="5">More normal Content.</TextBlock> <Button Padding="5" Margin="5">Another Customized Button</Button> </StackPanel>
7,样式中可以在静态引用其它资源。
<Application.Resources> <ImageBrush x:Key="TileBrush" TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSource="happyface.jpg" Opacity="0.3"> </ImageBrush> <Style x:Key="BigFontButtonStyle" TargetType="Button"> <Setter Property="Background" Value="{StaticResource TileBrush}" /> </Style> </Application.Resources>
至于Trigers,因为它能为我们带来动画效果,考虑到它与这一节的主题远点,这里不详细说明,放到下面的理解动画模型中。
总结一下。样式提供了另外一种资源的组织方式,区别于资源本身的描述和组织,是从控件使用资源的角度进行组织,同时样式也是控件的可视属性的组织,反过来,被组织成样式的可视属性,可以被不同控件使用,这就是分离了控件的可视化部分,为控件的定制提供了直接的支持。
一点总结
资源的描述和使用是界面定制的基础,样式的描述和使用会是我们开发的一个重点。通过对样式的说明,我们可以看到,实际上XAML中的描述都是围 绕着元素的属性(即对应.NET类的属性)进行的。我们描述它们的值,考虑它们之间依赖、继承、覆盖等关系。可以说在WPF/Silverlight上编 程,不管是XAML还是实际的代码,属性是被看成第一等的,这其实也是来自于WPF/Silverlight的第一等编程方式是描述性的。
描述性编程是说设计方面,属性编程是实现方面。用户的操作,事件的产生和响应、甚至动画的实现,在WPF/Silverlight中都 是以属性为中心。我们不仅可以描述属性的值,描述他们之间依赖、继承、覆盖的关系,还可以让属性根据时间变化其值,这是WPF/Silverlight动 画模型的核心思想。(而这一切都要以CLR/C#语言对类属性的直接支持为基础。)
在后面的文字中,会看到围绕属性的操作,如何展现对用户界面的定制,如何响应用户操作,如何实现动画效果的。
理解界面布局(Layout)结构
上面说明了在XAML中描述和组织资源的内容,下面进入控件定制的主体部分,说明在XAML中描述界面结构和使用控件。
界面布局就是对控件的组织,体现控件间“父 - 子 - 兄弟”关系,在数据结构上,这个叫做“树”。当在XAML中描述应用程序的Layout时,用的是许多的Element,或者叫对象,WPF/Silverlight中称之为“元素数/对象树”。仅在Layout的结构层面上,就仅仅是元素树了,但是结合WPF/Silverlight其它的功能,比如依赖项属性的定位,路由事件的传递,控件的定制等方面,对这个概念做了进一步的分析,进而把它分成两种树:一个是用来体现纯粹逻辑结构上的包含、组织关系的“逻辑树”;另一个专门用来体现可视元素的包含、组织关系的“可视化树”。
这体现了WPF/Silverlight中强调控件的内容和视觉表现分离的思想。逻辑树主要处理依赖项属性、资源查找、元素绑定 (Element Binding)的设置等,这是偏数据与逻辑处理的,但不包括用户自定义的控件内部layout;而可视化树则用来分离界面的可视化部分,包含更多用户自 定义的可视化元素,扩展基本控件的可视效果。路由事件系统也工作在可视化树上面。
另外,WPF/Silverlight的界面布局遵循下面的几个原则:
1,界面元素(指控件)不应该显式的设置大小,应该根据其显示的内容自动适应。
2,界面元素不能指定它们在屏幕上的具体坐标,它们的具体位置应该由放置它们的容器(container)来决定。
3,布局容器(layout container)应该自动处理它的子控件间的空隙。
4,布局容器是可以嵌套的。
第一、二点就是为了让WPF/Silverlight程序能够自适应不同尺寸、不同分辨率的显示设备。这也是这个GUI框架的一个设计优点。
下面代码用来说明这两种树。
<Window xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" Title="About WPF Unleashed" SizeToContent="WidthAndHeight" Background="OrangeRed"> <StackPanel> <Label FontWeight="Bold" FontSize="20" Foreground="White"> WPF Unleashed (Version 3.0) </Label> <Label>© 2006 SAMS Publishing</Label> <Label>Installed Chapters:</Label> <ListBox> <ListBoxItem>Chapter 1</ListBoxItem> <ListBoxItem>Chapter 2</ListBoxItem> </ListBox> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <Button MinWidth="75" Margin="10">Help</Button> <Button MinWidth="75" Margin="10">OK</Button> </StackPanel> <StatusBar>You have successfully registered this product.</StatusBar> </StackPanel> </Window>
逻辑树(Logical Tree)
结合代码,看到逻辑树上的内容基本上和XAML中所写的一致,除了最后的叶子节点String对象。
可视化树(Visual Tree)
再来看逻辑树,增加了很多内容,这些多出来的和视觉相关的对象由WPF/Sivlerlight自动处理,而当我们自己定制控件的时 候,就需要描述清楚它们。比如定制Label控件,它默认其中是一个Border,我们可以修改为Grid,StackPanel,Canvas等等,完 全由我们自己决定。
理解控件模型
现在我们来看构成界面布局的控件。我们把控件也看成一个微观的用户界面,它也有自己的逻辑树和可视化树。这样的新观点来源于对控件的重新分析。
对于控件,我们其实使用是它的功能,比如Button,我们只需要一个显示了功能的字符串内容,这个控件可以接受点击事件并触发一些处 理;不需要用户点击,只是显示信息内容,那就是Label,等等,至于这些控件具体是什么样子的,对于开发人员来说并不是重点,但对于用户来说,是一个重 要的软件使用上的体验方面,那图形编程框架就应该提供这种灵活性,允许自由描述控件的可视样式,这就是WPF/Silverlight中对待控件的设计思 路,并且抽象出对应的可视化树和样式的概念。而对于控件能提供的功能,可以说是控件应该具有的行为,在实现层面上就是对属性和事件的处理,这部分就是逻辑 树的概念。这就是WPF/Silverlight的控件被称为“lookless control”的原因。
长期以来,Button,Label等这样的控件的基本样式被我们看来已经固化,定制它们的思维也是从图形学实现上的角度出发的,WPF/Silverlight的描述性方法是一个革新。在WPF/Silverlight中定制控件的开发活动基本上就是:选择要定制控件的基本控件类型;描述它的样式和视觉状态;绑定它的内容数据;实现它的事件处理方法。
在我看来,WPF/Silverlight目前提供的基本控件在开始具有实际用户需求的软件的时候,基本上不会被直接用到,它们在视觉 效果上太平凡,和传统的图形框架提供的样式是一样的,实际的工作中都会基于它们做相应的定制,但是这个定制过程在WPF/Silverlight上要容易 太多了。
内容模型(ContentControl、ItemsControl)
WPF/Silverlight定义了不同类型的控件能够显示的内容类型,这叫做控件内容模型。回顾下编程模型的构成中的Silvergliht 3 UI Class Tree 2图及其解释。
这里控件(指可以接受用户操作的UI Element)分成两大类,ContentControl和ItemsControl,它们都不是抽象类,但我们不会像使用Button那样直接使用这 两个类来构建用户界面,这是.NET类库的一种层次分类方式,因为它们只是用来描述控件的内容类型。在定制控件的时候,要根据被定制的控件参照哪一种基本 控件,如Button还是ListBox,来选择使用。本文最后由一个实例,参考之。
内容模型是由框架定义的,这从类库的层次上就能看出来。参考Silverlight控件内容模型,其中的列表定义不同控件的内容类型,这是要遵守的规则。
模板模型(Template)
既然控件的内容是独立与它的显示样式而存在的,自然控件的显示样式可以被重用,这就是控件模板存在的意义。从逻辑树和可视化树的关系来说,当在 Layout中使用了控件时,控件模板起到的作用就在于,由控件模板进一步描述Layout的可视化树,告诉图形框架它自己应该如何显示控件,以及在接受 用户操作时,控件应该如何表现。
WPF/Silverlight定义了三种Template,都是从FrameworkTemplate继承的。它们是控件模板(ControlTemplate), 项布局模板(ItemsPanelTemplate)(最后一个的中文翻译是我个人的,仅用来说明概念)和数据模板(DataTemplate)。其中,数据模板伴随其它两个而使用,反映的是数据处理模型。控件模板对应的是ContentControl和ItemsControl控件(即所有响应用户操作的控件),项布局模板对应的是ItemsControl中的ItemsPanel成员。
数据模板为这两种控件提供内容数据,体现为“数据绑定”的概念,它的本义在于描述数据的可视化结构。因为所有WPF/Silverlight中已定义的控件都有它们默认的控件模板(Default Control Template),把它们指向特定的数据模板,可以用来定制数据的可视结构,比如行列的方式,而ControlTemplate和ItemsPanelTemplate则提供更彻底的定制化。DataTemplate和ItemsControl结合使用是现实应用的多数情况。
这样控件定制也有其深度,通过在内建控件的基础上,描述其DataTemplate,可以为其增加一些数据内容,但这是很有限的;完全的定制化,要通过ControlTemplate或ItemsPanelTemplate。DataTemplate重点还是在于数据的供给。
控件(及其内含项布局)的模板,也是一种资源。
控件模板(Control Template)
控件模板资源有几种方式描述:
1,直接描述在控件元素中。如下代码:
<Button Content="A Custom Button Template"> <Button.Template> <ControlTemplate TargetType="Button" > <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="10" Background="Red"> <TextBlock Foreground="White" Text="A Custom Template"></TextBlock> </Border> </ControlTemplate> </Button.Template> </Button>
2,单独的控件模板资源项。如下代码:
<Window.Resources> <ControlTemplate x:Key="ButtonTemplate" TargetType="{x:Type Button}"> <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="2" Background="Red" TextBlock.Foreground="White"> <ContentPresenter RecognizesAccessKey="True" Margin="{TemplateBinding Padding}" > <!--绑定表达式--> </ContentPresenter> </Border> </ControlTemplate> </Window.Resources> <Button Margin="10" Padding="5" Template="{StaticResource ButtonTemplate}"> A Simple Button with a Custom Template </Button>
3,放在样式资源中。如下代码:
<Style x:Key="ButtonStyle" TargetType="Button"> <Setter Property="Background" Value="Red"></Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="Button"> <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="10" Background="{TemplateBinding Background}"> <ContentPresenter Margin="{TemplateBinding Padding}"> </ContentPresenter> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <Button Content="A Custom Button Template" Style="{StaticResource ButtonStyle}">
或者保持ControlTemplate的复用性,这里补充了样式的灵活性:
<ControlTemplate x:Key="ButtonTemplate" TargetType="Button"> <Border BorderBrush="Orange" BorderThickness="3" CornerRadius="10" Background="Red"> <ContentPresenter Margin="{TemplateBinding Padding}"> </ContentPresenter> </Border> </ControlTemplate> <Style x:Key="ButtonStyle" TargetType="Button"> <Setter Property="Background" Value="Red"></Setter> <Setter Property="Template" Value="{StaticResource ButtonTemplate}"></Setter> </Style> <Button Content="A Custom Button Template" Style="{StaticResource ButtonStyle}">
将模板放到样式中,可以借由自动适应控件的样式的方式,达到自动适应控件的效果,但是正如在样式中所说明的,这种应用方式不要滥用。
项布局模板(Items Panel Template)
对于ItemsControl来说,它是用一个ItemsPanel来组织内部可以包含的Items(项)。ItemsControl比 ContentControl要复杂很多。要注意的是ItemsPanelTemplate针对的是ItemsControl中的ItemsPanel, 而不是这个ItemsControl。ItemsPanel的类型就是Panel的那些子类型,比如DockPanel, VirtualizingStackPanel, StackPanel,WrapPanel等等。
下面说明一个ListBox上的示例。
<!--水平显示ListBox的成员,关键在于设置ListBox的ItemsPanelTemplate为Horizontal的Stack Panel.--> <Style TargetType="ListBox"> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"/> </ItemsPanelTemplate> </Setter.Value> </Setter> </Style> <!--这里使用ControlTemplate来设置,这等于重新描述了ListBox的布局结构,即它的可视化树的结构--> <Style TargetType="ListBox"> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="ListBox"> <Border CornerRadius="5" Background="{TemplateBinding ListBox.Background}"> <ScrollViewer HorizontalScrollBarVisibility="Auto"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" IsItemsHost="True"/> <!--说明这个StackPanel是一个ItemsPanel,否则的话,这个新的ListBox不可以单独替换ItemsPanelTemplate,只能使用ControlTemplate的方式重新描述--> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style> <!--同时描述了ItemsPanleTemplate和ControlTemplate--> <Style TargetType="{x:Type ListBox}"> <Setter Property="ItemsPanel"> <Setter.Value> <ItemsPanelTemplate> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"/> </ItemsPanelTemplate> </Setter.Value> </Setter> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="{x:Type ListBox}"> <Border CornerRadius="5" Background="{TemplateBinding ListBox.Background}"> <ScrollViewer HorizontalScrollBarVisibility="Auto"> <ItemsPresenter/> <!--自动使用上面自定义的ItemsPanelTemplate--> </ScrollViewer> </Border> </ControlTemplate> </Setter.Value> </Setter> </Style>
实际上对于不同的ItemsControl,不仅有ItemsPanelTemplate,还有被ItemsPanel包含的Item的 ItemTemplate,这就涉及了这个ItemsControl具体想要显示的数据了,这部分要结合DataTemplate进行描述。比如 ListBox,ListView,TreeView,GridView等控件,想想它们在GTK,QT,BREW,Android等传统图形框架中的使 用方式,往往是运用MVC模式提供了单独的从数据供给到显示的一个完整的微观模型。WPF/Silverlight中也不例外,只不过是新的方式更加易于 理解和维护。
理解数据模型
承接上面对于ItemsPanelTemplate中的描述,这里介绍WPF/Silverlight中的数据模型。
控件的内容模型处理的是控件显示内容的方式和类型,具体如何获得内容的呢?用户操作引起的数据更新又如何被显示在用户界面上的呢?WPF/Silverlight提供了在XAML中直接描述数据来源的方式。这里的数据可以是来自其他控件,也可以是在类中定义的对象数据,你可以自由的实现一个数据类,并将类的各种数据属性绑定到界面控件的内容属性上来,这样控件就可以自动显示它的内容了。
WPF/Silverlight中的数据处理方式是按照很自然的逻辑方式:指定数据的来源;指定目标;设置数据是否双向影响。更多内容参考:WPF数据绑定和Silverlight数据绑定。
数据绑定(Data Binding)
最基本的控件内容数据来源是另一个控件的某个属性值。比如下个例子。
<Slider Name="sliderFontSize" Margin="3" Minimum="1" Maximum="40" Value="10" TickFrequency="1" TickPlacement="TopLeft"> </Slider> <TextBlock Margin="10" Text="Simple Text" Name="lblSampleText" FontSize="{Binding ElementName=sliderFontSize, Path=Value, Mode=TwoWay}" > <!--这里叫绑定表达式--> </TextBlock>
另外,控件的属性内容也可以从自定义的类属性中获得。当属性值发生变化的时候,WPF/Silverlight自动更新界面显示。这来源自我们一个常见的设计方法。通常我们都会把应用程序的数据处理单独封装起来。
// 这里定义了一个数据类,专门用来保存产品的一些数据信息。 public class Product { private string modelNumber; public string ModelNumber { get { return modelNumber; } set { modelNumber = value; } } private string modelName; public string ModelName { get { return modelName; } set { modelName = value; } } private double unitCost; public double UnitCost { get { return unitCost; } set { unitCost = value; } } private string description; public string Description { get { return description; } set { description = value; } } public Product(string modelNumber, string modelName, double unitCost, string description) { ModelNumber = modelNumber; ModelName = modelName; UnitCost = unitCost; Description = description; } }
<Grid Name="gridProductDetails"> <Grid.ColumnDefinitions> <ColumnDefinition Width="Auto"></ColumnDefinition> <ColumnDefinition></ColumnDefinition> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="Auto"></RowDefinition> <RowDefinition Height="*"></RowDefinition> </Grid.RowDefinitions> <!--注意下面的绑定表达式,这些控件的显示内容直接从数据类的实例中获取--> <TextBlock Margin="7">Model Number:</TextBlock> <TextBox Margin="5" Grid.Column="1" Text="{Binding ModelNumber}"></TextBox> <TextBlock Margin="7" Grid.Row="1">Model Name:</TextBlock> <TextBox Margin="5" Grid.Row="1" Grid.Column="1" Text="{Binding ModelName}"></TextBox> <TextBlock Margin="7" Grid.Row="2">Unit Cost:</TextBlock> <TextBox Margin="5" Grid.Row="2" Grid.Column="1" Text="{Binding UnitCost}"></TextBox> <TextBlock Margin="7,7,7,0" Grid.Row="3">Description:</TextBlock> <TextBox Margin="7" Grid.Row="4" Grid.Column="0" Grid.ColumnSpan="2" TextWrapping="Wrap" Text="{Binding Description}"></TextBox> </Grid>
数据模板(Data Template)
数据绑定进行模板化,是控件模板描述的必然要求。
对于ContentControl来说,它支持通过ContentTemplate属性来设置 DataTemplate,DataTemplate里设置的数据来源,就是用来显示在Content属性中。对于从ItemsControl继承的 List Controls,通过ItemTemplate属性来设置DataTemplate,就是用来显示在每个项(Item)上面的数据来源。
实际上,后者也是使用前者的,因为每一个具体的项也是只包含一个内容。
数据模板可以像ControlTemplate那样,单独定义在资源元素中,或者定义在控件元素内部,也可以在样式中定义ControlTemplate的时候被静态引用。示例如下:
<ListBox Name="lstProducts" HorizontalContentAlignment="Stretch"> <ListBox.ItemTemplate> <DataTemplate> <!--ListBox的ItemTemplate属性数据来自这个DataTemplate--> <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <TextBlock FontWeight="Bold" Text="{Binding Path=ModelNumber}"></TextBlock> <!--只是把绑定表达式放到了DataTemplate中描述--> <TextBlock Grid.Row="1" Text="{Binding Path=ModelName}"></TextBlock> </Grid> </Border> </DataTemplate> </ListBox.ItemTemplate> </ListBox> <!--下面说明DataTemplate也可以单独定义在资源中,被控件静态引用--> <UserControl.Resources> <DataTemplate x:Key="ProductDataTemplate"> <Border Margin="5" BorderThickness="1" BorderBrush="SteelBlue" CornerRadius="4"> <Grid Margin="3"> <Grid.RowDefinitions> <RowDefinition></RowDefinition> <RowDefinition></RowDefinition> </Grid.RowDefinitions> <TextBlock FontWeight="Bold" Text="{Binding ModelNumber}"></TextBlock> <TextBlock Grid.Row="1" Text="{Binding ModelName}"></TextBlock> </Grid> </Border> </DataTemplate> </UserControl.Resources> <ListBox Name="lstProducts" HorizontalContentAlignment="Stretch" ItemTemplate="{StaticResource ProductDataTemplate}" SelectionChanged="lstProducts_SelectionChanged"></ListBox>
绑定表达式(Binding Expression)
应该会注意到上面的代码示例中有两种绑定表达式:{Binding}和{TemplateBinding}。比较起来:
1,TemplateBinding是对Binding的优化,只使用在ControlTemplate的描述中,用来绑定控件的依赖项属性。
2,Binding既可以用来绑定控件的依赖项属性,也可以用来帮定数据类对象的属性。但是它在绑定控件的依赖项属性时,仅是简单的数据绑定功能,不用在ControlTemplate中,但可以用在DateTemplate中。
{Binding}参考:Silverlight绑定标记扩展和WPF绑定标记扩展。
{TemplateBinding}参考:Silverlight TemplateBinding 标记扩展和WPF TemplateBinding 标记扩展。
一点总结
上面我们依次说了用户界面的布局模型、组成它的控件模型。在控件模型里,又可以分成两个部分,一个是内容模型,另一个是描述结构的模板,这两者 都涉及的数据处理,引申出对数据模型的描述。可以看出,WPF/Silverlight一直注意显示和数据的分离,同时在XAML中提供粘合的表达式,使 得显示部分对数据的使用方式,表现得自然、易于理解。这体现了一种精巧的设计,各部分的概念独立,而又环环相扣。
这样,WPF/Silverlight中关于控件的静态视觉表现就介绍完了,下面就是关于如何描述控件的动态视觉表现的动画模型。
理解动画模型
首先考虑一下传统应用软件模型中的处理。在事件驱动的这个基本认识下,开发人员能够做事情的最佳地方就是事件处理函数中,在timer的协助下,处理一些绘画逻辑。这是一种Frame-Based的方式,这是相当低级的处理方式,繁琐而又难于维护:
1,处理的是基本的像素点(pixels),而不是控件对象本身。(不是面向对象的)
2,不能同时实现多于一个的动画。
3,动画的帧率是固定的。
4,动画越复杂,实现的代码也越复杂,这和第一点也是关联的。
基于frame的方式,来源于传统上对动画的认识:一幅幅的静态图片以一定的速度播放而展现出来的效果。这对于图形框架来说,实现上是简单了,但是对于做为使用者的开发人员来说,是个噩梦。
在WPF/Silverlight中,动画模型从Frame-Based改为Property-Based。具体含义就是:WPF/Silverlight随着时间的变化,通过修改控件的属性来展现动画的效果。注意与传统方式的区别:在每个时刻上,开发者只是修改控件的属性,WPF/Siverlight就会自动更新整体界面的显示,而不是由开发者负责绘制整体。这就让开发者从界面的低级绘制上解脱出来,专注于考虑当动画发生时,界面上的哪个对象的哪些的属性需要发生变化。这才是真正面向对象的方式。
比如,当我们想让一个按钮伸缩的动态效果,很自然的,只需要在不同时刻上设置按钮的width和height属性就应该可以了,至于这 个按钮显示内容的大小变化,根据WPF/Silverlight的界面布局原则,它会自动伸缩的。再比如按钮有光晕动画的效果,我们可以设计使用合适的画 笔对象绘制这个按钮的边框就可以了,至于画笔是怎么绘制上去的,那是WPF/Silverlight的事情。当然,如果内置的画笔不能满足需求,就需要先 开发出一个合适的画笔,然后再使用它,而开发这个画笔的时候,还是会要求使用低级的绘制方法。尽管还有这样的需求,但也是大大的简化了动画的实现,也降低 了维护的难度。
虽然Property-Based的方式也是和Frame-Bashed一样,都是Timer-Based的,但是在WPF/Silverlight中只需要指定动画的开始时间和持续时间,由图形框架自动计算帧率。
总结起来,WPF/Sivlierlight的动画模型的几条基本设计规则:
1,动画是基于时间变化的。只要制定初始状态、终结状态、初始时间和动画的持续时间,图形框架会计算帧率。
2,动画是通过在不同的时间间隔上修改控件的属性来实现的。这些控件的属性往往表示的就是控件的可视资源,比如尺寸、背景、颜色、转换、像素效果。
3,既然是修改属性,那就根据控件的属性值的数据类型的不同,抽象出不同的动画类来处理动画的实现,比如Width属性的值是 Double数据类型的,就由DoubleAnimation类来处理这一类型的动画;需要修改颜色属性,就由ColorAnimation类来处理。等 等。
4,根据这些动画类修改属性的方式不同,分成两大类:Linear Interpolation和Key-Frame animation。前者平滑的修改属性值,后者是跳跃式的。用数学的话说,前者是连续性的,后者是离散性的。
在编程模型的构成中 的Silverlight 3 UI Class Tree 1图上,System.Windows.Media.Animation.Timeline子树上的动画类。比如DoubleAnimation和 DoubleAnimationUsingKeyFrame就是对第4点的说明,后者用来实现基于关键帧的动画。可以想见,这些 WPF/Silverlight动画类主要封装的是Timer,从它们都是从类Timeline继承下来的,也可以看出这一点。
在WPF/Silverlight中,动画模型可以分成两种:Event Trigger动画模型和VSM(Visual State Manager)动画模型。前者是从事件触发的角度;后者是可视状态的角度,一个硬币的两面,只是前者更遵从传统上的事件驱动的思考方式,后者更面向对象。不过,后者的功能更强大。
我们从基本的动画类说起。
动画类(Animation Class)
一定请注意,动画类是针对数据类型的,不是针对具体控件的,因为WPF/Silverlight的动画模型是基于属性的修改,属性的值类型才是关注的重点。动画类具有如下的属性:
1,From。用来设置这个动画类关注的数据的初始值。
2,To。用来设置这个动画类关注的数据的终结值。
3,By。代替To属性使用。表示关注的数据在初始值的基础上相对增加的数值。
4,Duration。用来设置这个动画类的时间间隔,这是说明动画类封装了timer的直接证明。Duration是一个类,和TimeSpan类似。
如下代码:
<!--这个动画类描述了:针对Double数据类型,设置其值从160变为300,持续时间是5秒的动画过程,图形框架会自己计算帧率--> <!--Duration的格式:[days.]hours:minutes:seconds[.fractionalSeconds] 即天.小时:分:秒.秒的小数部分,或者是“Automatic”、“Forever”--> <DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation>
这里的From和To使用的都是硬编码,可以使用数据绑定表达式来描述它们。比如:
<DoubleAnimation Storyboard.TargetProperty="Width" To="{Binding ElementName=window,Path=Width}" Duration="0:0:5"></DoubleAnimation>
演示图板(StoryBoard)
既然动画类不是直接和控件关联的,那怎么确定动画类作用的是哪一个控件的相关属性上的值呢?WPF/Silverlight发明了演示图板的概念。
演示图板关联了具体的动画类和它处理的数据对应的属性。演示图板提供了两个依赖项属性TargetName和TargetProperty,它们也是附加属性,用来说明动画关联到的对象名称以及该对象的属性名称。这是它的第一个作用。如下:
<!--使用Storyboard关联一个DoubleAnimation到cmdGrow.Width属性。--> <Storyboard x:Name="storyboard" Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="Width"> <DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard> <!--Storyboard的TargetProperty是一个附加属性,需要使用括号--> <Storyboard x:Name="storyboard" Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="(Canvas.Left)"> <DoubleAnimation From="160" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard> <!--直接在DoubleAnimation使用这两个附加属性--> <Storyboard x:Name="storyboard"> <DoubleAnimation Storyboard.TargetName="cmdGrow" Storyboard.TargetProperty="Width" From="160" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard>
StoryBoard的第二个作用,控制动画的播放过程,即控制动画的开始、暂停、前进、停止等功能。如下:
<Storyboard x:Name="fadeStoryboard"> <DoubleAnimation x:Name="fadeAnimation" Storyboard.TargetName="imgDay" Storyboard.TargetProperty="Opacity" From="1" To="0" Duration="0:0:10"> </DoubleAnimation> </Storyboard> <Grid> <Image Source="night.jpg"></Image> <Image Source="day.jpg" Name="imgDay"></Image> </Grid> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" Margin="5"> <Button Name="cmdStart" Click="cmdStart_Click">Start</Button> <Button Name="cmdPause" Click="cmdPause_Click">Pause</Button> <Button Name="cmdResume" Click="cmdResume_Click">Resume</Button> <Button Name="cmdStop" Click="cmdStop_Click">Stop</Button> <Button Name="cmdMiddle" Click="cmdMiddle_Click">Move To Middle</Button> </StackPanel>
对应的事件处理实现:
private void cmdStart_Click(object sender, RoutedEventArgs e) { fadeStoryboard.Begin(); } private void cmdPause_Click(object sender, RoutedEventArgs e) { fadeStoryboard.Pause(); } private void cmdResume_Click(object sender, RoutedEventArgs e) { fadeStoryboard.Resume(); } private void cmdStop_Click(object sender, RoutedEventArgs e) { fadeStoryboard.Stop(); } private void cmdMiddle_Click(object sender, RoutedEventArgs e) { // Start the animation, in case it's not currently underway. fadeStoryboard.Begin(); // Move to the time position that represents the middle of the animation. fadeStoryboard.Seek( TimeSpan.FromSeconds(fadeAnimation.Duration.TimeSpan.TotalSeconds/2)); }
这里看到了Storyboard和Animation Class分离的好处,Animation Class针对具体的数据类型设置动画,Storyboard可以将Animation Class关联到任何对象上,只要这个对象的属性符合Animation Class的数据类型要求。如果没有对应的Animation Class,就需要自己来实现一个了。
Storyboard的播放动画类的功能,不仅可以写在代码里,还可以在XAML中描述。这需要通过Event Trigger来实现。
事件扳机(Event Trigger)
Event Trigger是一种Trigger,这里需要先插入介绍一下Trigger。
Triggers
重回到Style的概念和应用上来。现在我们知道,样式被看成是控件定制的对象,它描述了控件对象的属性值的集合,控件因所其样式不同而产生不 同的显示效果。但在本文前面对Style的介绍中,只是说明了Style组织控件的静态可视资源的功能,控件的动态可视状态也可以被Style来组织。这 样一来,对于一个控件的定制来说,只要描述好使用它的样式,就不仅定制了它的静态可视效果,也定制了动态可视效果,再加上还可以在样式中装备用户事件处理 方法,这样就涵盖了控件定制的所有方面。
Trigger就意为“扳机”,它提供了一种由状态的变化触发而自动修改控件可视属性的一种功能。代码示例如下:
<Style x:Key="BigFontButton"> <Style.Setters> <!--属性集合放在Style的Setters属性中--> <Setter Property="Control.FontFamily" Value="Times New Roman" /> <Setter Property="Control.FontSize" Value="18" /> </Style.Setters> <Style.Triggers> <!--Trigger集合放在Style的Triggers属性中--> <Trigger Property="Control.IsFocused" Value="True"> <!--设置触发条件,属性IsFocused为True时--> <Setter Property="Control.Foreground" Value="DarkRed" /> <!--就把Foreground属性设为DarkRed--> </Trigger> </Style.Triggers> </Style>
上面的例子简单的说明了Triggers的用法。当把这个Style资源付给Button后,只要Button获得焦点,WPF/Silverlight就会自动设置前景色了。
Trigger的机制不仅在于修改控件的属性,主要的,它进一步封装了事件驱动的编程模型,直接提供了在XAML中描述事件处理的内容的描述性方式。
Trigger有很多种,都是从TriggerBase继承的。
1,Trigger。最简单的形式。观察控件属性的变化,然后触发控件的其它属性的设置。
2,MultiTrigger。与Trigger类似,但是可以同时设置多个触发条件。
3,DataTrigger。和数据绑定一起工作,专门用来设置绑定的数据的变化,而非控件的属性。
4,MultiDataTrigger。和DataTrigger类似,但是可以同时设置多个触发条件。
5,EventTrigger。这是功能最强大的Trigger,它用来设置事件发生后的动画效果。
Trigger不仅可以放在Style里,还可以被控件单独使用。因为控件的父类FrameworkElement中有一个集合叫 Triggers。但是这个Triggers只支持EventTrigger。《pro wpf》上说这个限制没有什么技术上的原因,仅仅是可能WPF开发组的人没有时间来实现,或者将来就会包含其它种的Trigger了。
对于Trigger(第一种)来说,有一个优点就是,当Button失去焦点后,它的前景色会自动变为原来的设置。如果在一个Trigger中设置个属性多次,总是以最后的那个设置为准。也可以设置很多个Trigger到一个属性上,到如下代码:
<Style x:Key="BigFontButton"> <Style.Triggers> <!--设置在不同触发条件下设置前景色--> <Trigger Property="Control.IsFocused" Value="True"> <Setter Property="Control.Foreground" Value="DarkRed" /> </Trigger> <Trigger Property="Control.IsMouseOver" Value="True"> <Setter Property="Control.Foreground" Value="LightYellow" /> <Setter Property="Control.FontWeight" Value="Bold" /> </Trigger> <Trigger Property="Button.IsPressed" Value="True"> <Setter Property="Control.Foreground" Value="Red" /> </Trigger> </Style.Triggers> </Style>
下面是一个MultiTrigger的例子。
<Style x:Key="BigFontButton"> <Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Control.IsFocused" Value="True"> <Condition Property="Control.IsMouseOver" Value="True"> <!--当获得焦点,而且鼠标移动到上面才会触发--> </MultiTrigger.Conditions> <MultiTrigger.Setters> <Setter Property="Control.Foreground" Value="DarkRed" /> </MultiTrigger.Setters> </MultiTrigger> </Style.Triggers> </Style>
和Trigger与DataTrigger相比,我们最感兴趣的还是Event Trigger,因为Event Trigger是对传统的事件驱动开发方式的封装,让我们在XAML中描述事件处理的内容,尤其是用户界面的可视部分的处理。
Event Trigger
但是与Trigger相比,EventTrigger是不会自动恢复原有设置的,必须来描述相反的状况,比如按钮按下与释放,鼠标移动到与移动出等都需要成对的设置。
Event Trigger可以描述在下面的地方:
1,在Style里。放在Style.Triggers集合中。
2,在数据模板里。放在DataTemplate.Triggers集合中。
3,在控件模板里。放在ControlTemplate.Triggers集合中。
4,直接在控件里。放在FrameworkElement.Triggers集合中。
描述Event Trigger时,需要指明路由事件,表明触发的条件。
见如下代码:
<!--定义在控件里。--> <Button Padding="10" Name="cmdGrow" Height="40" Width="160" HorizontalAlignment="Center" VerticalAlignment="Center"> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <EventTrigger.Actions> <BeginStoryboard> <!--这里相当于调用Storyboard类的begin方法--> <Storyboard> <DoubleAnimation Storyboard.TargetProperty="Width" To="300" Duration="0:0:5"></DoubleAnimation> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Button.Triggers> <Button.Content> Click and Make Me Grow </Button.Content> </Button> <!--定义在样式里。--> <Style x:Key="BigFontButtonStyle"> <Style.Setters> </Style.Setters> <Style.Triggers> <EventTrigger RoutedEvent="Mouse.MouseEnter"> <!--当鼠标进入时,开始持续0.2秒的动画,设置字体大小变为22--> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="FontSize" To="22" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <!--当鼠标离开后,开始持续1秒的动画,恢复字体大小--> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:1" Storyboard.TargetProperty="FontSize" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> </Style.Triggers> </Style>
Event Trigger的动画方式是对传统的事件驱动模型的延伸诠释,下面看看WPF/Silverlight中的新概念,Visual State Manager,同样是基于Storyboard和动画类,它使用了另外的角度来驱动动画的过程。
可视状态管理器(VSM: Visual State Manager)
这是从用户界面的可视状态入手考虑动画的描述方式。因为我们考虑的就是用户界面的动态变化,所以这种方式是一种从上而下的思考方式。
用户界面本来就是由很多控件的可视部分组成的,这些控件的可视部分可以被称为part;用户界面在不同时刻上的视觉表现,就是一个个显 示的State。在时间的参考轴下,从一个state变为另一个state,展现出动画的效果,在这种变化的过程中,实际需要操作的是part的部分,是 part的可视属性的值发生了变化,导致了state的变化。这样看待用户界面的动画模式,叫部件与状态模式(Parts & States Model)。
在这个模式里,把用户操作转化为界面的可视状态,比如用户按下事件,表示IsPressed状态;鼠标滑过事件,表示为MouseOver状态,这被抽象为VisualState类;用户界面状态被组织成可视状态组(Visual State Group),包含了一组互斥的状态,以及状态间的转换(Visual State Transition),而管理管理状态以及转换的就是可视状态管理器(VSM)。
VisualState类是需要使用Storyboard来描述的,进而通过描述动画类来描述用户界面的动态变化。VSM代替 Event Trigger管理Storyboard的动画播放操作。WPF/Silverlight为内建的基本控件规定了它们的默认可视状态组及状态,参考:Silverlight控件样式和模板中的描述,在定制化控件时,根据我们参照的控件类型,选择描述什么可视状态组和可视状态。
具体实例参考本文最后的实例。
中级动画类型
到目前为止,我们讨论了WPF/Silverlight的动画模型的全部内容。这节说的动画类型,是从应用的角度,说明可以对那些类型的对象设置动画。
在上面出现的示例中,我们最经常设置Storyboard的TargetName和TargetProperty的对象是控件。考虑一 下怎么实现阴影、模糊、旋转、平移、放大、缩放等等这些效果。通过设置控件的基本可视属性,比如Width,也许可以达到一些放大的效果;那么平移呢,设 置Top,Left坐标吗?还有阴影、模糊、旋转呢?这些都不容易通过直接设置控件的属性来达到。
为此,WPF/Sivlerlight提供了对应的类:System.Windows.Media.Transform和System.Windows.Media.Effects.Effect。前者用来处理旋转、平移、缩放等实际上坐标转换类的效果;后者用来处理位图效果,比如阴影、模糊。这个Effect命名的不好,准确地应该是BitmapEffect。Effect这个词太宽泛了,因为我们都常说“用户界面显示效果”。
而在所有控件的父类FrameworkElement中,提供了两个属性Effect和RenderTransform,用来表示控件所使用的位图效果和坐标转换效果的集合。控件通过这两个属性首先可以描述一些更高级的显示效果,再将Storyboar的Target设置为它们,就会出现更高级的动画效果。代码如下:
<!--实现旋转--> <StackPanel Margin="15"> <StackPanel.Resources> <Storyboard x:Name="myStoryboard"> <DoubleAnimation Storyboard.TargetName="myTransform" Storyboard.TargetProperty="Angle" <!--这是RotateTransform的属性,设置角度--> From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" /> </Storyboard> </StackPanel.Resources> <!--在StartAnimation方法中:myStoryboard.Begin();--> <Rectangle Width="50" Height="50" Fill="RoyalBlue" MouseLeftButtonDown="StartAnimation"> <Rectangle.RenderTransform> <RotateTransform x:Name="myTransform" Angle="45" CenterX="25" CenterY="25" /> </Rectangle.RenderTransform> </Rectangle> </StackPanel> <!--实现模糊--> <StackPanel> <StackPanel.Resources> <Storyboard x:Name="myStoryboard"> <!-- Blur the Button and then animate back to normal. --> <DoubleAnimation Storyboard.TargetName="myBlurBitmapEffect" Storyboard.TargetProperty="Radius" <!--这是BlurEffect的属性,设置模糊量--> From="0" To="40" Duration="0:0:0.3" AutoReverse="True" /> </Storyboard> </StackPanel.Resources> <!--在StartAnimation方法中:myStoryboard.Begin();--> <Button Content="Click to Blur ME!" Click="StartAnimation" Width="200" Margin="20"> <Button.Effect> <BlurEffect x:Name="myBlurBitmapEffect" Radius="0" /> </Button.Effect> </Button> </StackPanel>
更多复杂的动画,还请根据设计多实践。不在于实现方面的限制,在于能否想得到!
实例
显示效果目标
没有鼠标移动到按钮上面时,如下图:
当鼠标移动到按钮上面时,边框和上半部分变亮,按钮有凸起的感觉,如下图:
当鼠标按下按钮时,下半部分变亮,按钮有凹下的感觉,如下图:
按钮上的静态显示效果,是一种类似vista的图标效果,透明玻璃,加上光源明暗效果。边角也是圆角的。
实现界面布局
通过前面的说明,编写XAML的工作中,构建界面布局已不是重点,下面就是描述界面布局的XAML代码:
<Grid Background="Black" Width="150" Height="50"> <!--背景是黑色--> <Grid.ColumnDefinitions> <ColumnDefinition Width="*"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="Auto"/> <ColumnDefinition Width="*"/> </Grid.ColumnDefinitions> <Grid.RowDefinitions> <RowDefinition Height="*" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" /> </Grid.RowDefinitions> <!--这个Grid为3行4列--> <Grid.RenderTransform> <ScaleTransform CenterX="0.5" CenterY="0.5" ScaleX="2" ScaleY="2" /> <!--便于观看,以中心为坐标放大1倍。--> </Grid.RenderTransform> <Rectangle Grid.ColumnSpan="2" Grid.Column="1" Grid.Row="1" Stroke="#FF808080" RadiusX="2" RadiusY="2" Opacity="0.3"> <!--占用Grid的中间,圆角,并且0.3程度的透明--> </Rectangle> <Button Style="{StaticResource BlackWithBorderButtonTemplate}" Grid.Column="1" Grid.Row="1" Margin="2,2,2,2" Content="Play"> <!--静态引用一个样式资源,覆盖在上面的圆角矩形上面--> </Button> <Button Style="{StaticResource BlackWithBorderButtonTemplate}" <!--静态引用一个样式资源,覆盖在上面的圆角矩形上面--> Grid.Column="2" Grid.Row="1" Margin="2,2,2,2" Content="Stop"> </Button> </Grid>
我们怎么知道这样颜色值、圆角到底要多少程度的呢?这是界面设计规格,应该根据用户需求而来,上面的圆角矩形框的颜色是具体的颜色值,这个具体 是多少也应该根据用户需求来。也许是由美工设计人员给出设计的规格。更常态的,是在Expression Blend工具中设置的。
整个结构相当简洁,两个Button使用了同样的样式资源 BlackWithBordrButtonTemplate。重点是这个样式的描述。
实现定制样式
样式描述如下:
<Application.Resources> <Style x:Key="BlackWithBorderButtonTemplate" TargetType="Button"> <Setter Property="IsEnabled" Value="true" /> <!--设置这个Button样式的属性集合--> <Setter Property="IsTabStop" Value="true" /> <Setter Property="Margin" Value="0" /> <Setter Property="HorizontalContentAlignment" Value="Center" /> <Setter Property="VerticalContentAlignment" Value="Center" /> <Setter Property="Cursor" Value="Arrow" /> <Setter Property="Foreground" Value="#CC808080" /> <Setter Property="FontSize" Value="11" /> <Setter Property="Width" Value="50" /> <Setter Property="MinWidth" Value="50" /> <Setter Property="MaxWidth" Value="50" /> <Setter Property="Height" Value="22" /> <Setter Property="MinHeight" Value="22" /> <Setter Property="MaxHeight" Value="22" /> <Setter Property="Template"> <!--Template属性是重点--> <Setter.Value> <ControlTemplate TargetType="Button"> <Grid x:Name="RootElementBlackWithBorderButton"> <!--这个样式描述了Button的内容模型,根元素还是一个Grid--> <VisualStateManager.VisualStateGroups> <!--定义动态效果,对于Button目标类型的,设置两个组--> <VisualStateGroup x:Name="CommonStates"> <VisualState x:Name="Normal"/> <VisualState x:Name="MouseOver"> <!--描述鼠标滑过,获得焦点时的状态--> <Storyboard> <!--控制三个对象的属性变化,这里设置的都是Opacity属性,而且开始时间一样,所以同时变化--> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightTop" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0.3"/> </DoubleAnimationUsingKeyFrames><!--HighlightTop的Opacity从0.2变为0.3,所以变亮了--> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightBottom" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/> </DoubleAnimationUsingKeyFrames><!--HighlightBotton的Opacity还是0--> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Border" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0.7"/> </DoubleAnimationUsingKeyFrames><!--Border的Opacity从0.3变为0.7,所以变亮了--> </Storyboard> </VisualState> <VisualState x:Name="Pressed"><!--鼠标按下后的状态--> <Storyboard> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightTop" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/> </DoubleAnimationUsingKeyFrames><!--Opacity从0.2变为0,所以变暗了--> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightBottom" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0.3"/> </DoubleAnimationUsingKeyFrames><!--Opacity从0变为0.3,所以变亮了,持续0.3秒--> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="Border" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0.5"/> </DoubleAnimationUsingKeyFrames><!--Opacity从0.3变为0.5,所以变亮了,但和MouseOver的不同,看起来差别很小--> </Storyboard> </VisualState> <VisualState x:Name="Disabled"><!--Button不可用时的状态--> <Storyboard> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightTop" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.1500000" Value="0"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="HighlightBottom" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.1500000" Value="0"/> </DoubleAnimationUsingKeyFrames> <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="ContentPresenter" Storyboard.TargetProperty="(UIElement.Opacity)"> <SplineDoubleKeyFrame KeyTime="00:00:00.1500000" Value="0.7"/> </DoubleAnimationUsingKeyFrames> </Storyboard> </VisualState> </VisualStateGroup> <VisualStateGroup x:Name="FocusStates"> <!--这里按键获取聚焦时的状态,忽略--> <VisualState x:Name="Focused"> </VisualState> <VisualState x:Name="Unfocused"/> </VisualStateGroup> </VisualStateManager.VisualStateGroups> <!--从这里描述内部的布局,这是表示边框的圆角矩形--> <Rectangle x:Name="Border" Stroke="#FF808080" RadiusX="2" RadiusY="2" Opacity="0.3"/> <Path x:Name="HighlightTop" Margin="2,2,2,11" Opacity="0.2" <!--绘制按钮的上半部分--> Data="M0,1 C0,0.45 0.45,0 1,0 L45,0 C45.55,0 46,0.45 46,1 C46,1 46,9 46,9 C46,9 0,9 0,9 C0,9 0,1 0,1 z"> <Path.Fill> <LinearGradientBrush EndPoint="0,1" StartPoint="0,0"><!--使用线性画笔--> <GradientStop Color="#FFFFFFFF" Offset="0"/> <GradientStop Color="#FFE1E1E1" Offset="1"/> </LinearGradientBrush> </Path.Fill> </Path> <Path x:Name="HighlightBottom" Margin="2,11,2,2" Opacity="0" <!--绘制按钮的下半部分--> Data="M0,0 C0,0 31,0 46,0 C46,0 46,8 46,8 C46,8.55 45.55,9 45,9 L1,9 C0.44,9 0,8.55 0,8 C0,8 0,0 0,0 z"> <Path.Fill> <LinearGradientBrush EndPoint="0,1" StartPoint="0,0"> <GradientStop Color="#FFD6D6D6" Offset="0"/> <GradientStop Color="#FFFFFFFF" Offset="1"/> </LinearGradientBrush> </Path.Fill> </Path> <!--根据控件内容模型,这个定制控件基于Button,所以这里使用ContentControl,还可以使用ContentPresenter,但其可以绑定的属性就有了一些限制。--> <ContentControl x:Name="ContentPresenter" <!--描述内容的数据绑定--> Content="{TemplateBinding Content}" ContentTemplate="{TemplateBinding ContentTemplate}" FontFamily="{TemplateBinding FontFamily}" FontSize="{TemplateBinding FontSize}" FontStretch="{TemplateBinding FontStretch}" FontStyle="{TemplateBinding FontStyle}" FontWeight="{TemplateBinding FontWeight}" Foreground="{TemplateBinding Foreground}" Padding="{TemplateBinding Padding}" HorizontalAlignment="{TemplateBinding HorizontalAlignment}" VerticalAlignment="{TemplateBinding VerticalAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" /> </Grid> </ControlTemplate> </Setter.Value> </Setter> </Style> </Application.Resources>
实际上,一样的界面表现具体的XAML描述方式可以是不同的,这在与设计,比如可以用Canvas代替Grid,比如Button上半部和下半部的绘制方式,这是自由的。
控件定制过程
1,设计界面布局,并使用基本控件代替。
2,描述界面资源。每一种资源都要独立描述,最好不要使用内嵌的方式。
3,描述控件的样式。定义控件的微布局。
4,描述控件的动画。
下一步
控件定制是一个和界面设计的话题,多参考实例代码。本文的内容涵盖的只能说是中低阶的内容,更多高级内容还请自己实践,这也是大家进一步交流讨论的必要性体现。
自己设计想要的界面效果,然后擦亮你的键盘和眼镜(比如我 8-)),实际动手使用WPF/Silverlight来描述自己原创的用户界面吧!
至此,本系列中内容最多、最重要的部分已经写完了。以上三篇的讲述都是把WPF和Silverlight合在一起写的,可是在实际的编 程中,它们的.net framework class library有很多不同,这些不同在实际的编程中会突出的表现出来。下面就是着重说明一下它们之间的不同:WPF/Silverlight编程:WPF与Silverlight的联系和区别。