Windows Presentation Foundation   本书介绍了应用程序的两种主要类型:用户直接运行的桌面应用程序和用户通过浏览器访问的Web应用程序。我们在.NET Framework的两个不同区域创建它们:Windows窗体和ASP.NET页面。这些应用程序类型有各自的优缺点。桌面应用程序有较大的灵活性和较强的响应性,而Web应用程序可以被许多用户同时在远程访问。   在目前的计算环境下,应用程序之间的界限越来越模糊了。有了Web服务和Windows Communication Foundation(WCF,详见第35章)后,桌面应用程序和Web应用程序都可以用更分布的方式执行,在局域网和广域网上交换数据。另外,Web客户应用程序(即IE或Firefox等浏览器)不再被看作所谓的“瘦”客户程序,因为它们不再仅仅显示信息。最新的浏览器和运行它们的计算机的功能已经远不止此。   近年来,用户体验的个性化已经渐成气候。Web应用程序现在一般使用JavaScript、Flash、Java应用程序和其他技术,越来越像桌面应用程序了。例如,Google Docs的功能就说明了这一点。而桌面应用程序相互连接的趋势越来越明显,其功能从简单(自动更新、在线帮助等)到高级(例如,在线数据源和对等联网),应有尽有。这一点可以通过图34-1来说明。 图 34-1   Windows Presentation Foundation(WPF)是一种统一的技术,利用它编写的应用程序可以在桌面和Internet之间搭起桥梁。本章介绍的WPF应用程序可以作为桌面应用程序运行,或在浏览器上作为Web应用程序运行。WPF有一个删节版本Silverlight,可用于给Web应用程序添加动态内容。   本章将学习WPF,理解如何使用它创建下一代应用程序,主要内容如下: ● WPF的概念 ● 基本WPF应用程序的组成 ● WPF基础 ● 用WPF编程 34.1 WPF的概念   利用WPF(以前称为Avalon)技术可以编写出独立于平台的应用程序,它清楚地定义了设计和功能之间的界限。WPF借用并扩展了以前许多技术的概念和类,包括Windows窗体、ASP.NET、XML、数据绑定技术、GDI+等。如果读者以前使用.NET Framework创建过Web应用程序,第一次看到WPF应用程序的代码时,会立即注意到许多类似之处。尤其是,WPF应用程序使用标记和后台编码模型,这与ASP.NET类似。但在其内部,它们的不同点和相似点一样多,因此WPF对于开发人员和用户而言都是一种全新的技术。   WPF开发的一个关键概念是设计与功能几乎完全分开。这样,设计人员和C#开发人员就可以一起开发项目,其自由程度可以达到以前高级设计概念或第三方工具的要求。这个功能受到所有人的欢迎,包括小型团队、爱好开发的人员、合作开发大项目的大型开发团队和设计人员。   下面几节介绍WPF给设计人员和开发人员带来了什么好处,使他们能携手工作。 34.1.1 WPF给设计人员带来的好处   在WPF中,用户界面设计使用的语言是Extensible Application Markup Language(XAML,读作zammel)。它类似于ASP.NET中使用的标记语言,因为它也使用XML语法,允许以声明性的、带层次结构的方式把控件添加到用户界面上。也就是说,可以用XML元素的形式添加控件,用XML属性指定控件的属性。控件也可以包含其他控件,这对布局和功能都非常重要。   但是,XAML比ASP.NET强大许多,在显示给用户时绝不仅限于HTML的功能。XAML在设计时考虑了目前强大的图形卡,并允许使用这些图形卡通过DirectX 7或更高版本提供的全部高级功能。下面列出了这些功能: ● 浮点数坐标和矢量图提供的布局可以缩放、旋转和转换,且没有质量损失。 ● 高级显示的2D和3D功能 ● 字体的高级处理和显示 ● UI对象的纯色、渐变色和纹理填充,且可以设置透明度 ● 动画故事板功能,可以用于所有情形,包括用户触发的事件,如鼠标单击按钮事件 ● 可重用的资源,以动态设置控件的样式   许多功能都专门面向Microsoft Vista操作系统,该操作系统可以通过Aero接口访问高级图形功能。但是,WPF应用程序也可以运行在其他操作系统上,例如Windows XP。如果图形卡因某种原因无法工作,内置于.NET Framework 3.5运行库中的支持显示系统可以显示XAML(但有性能损失)。   VS和VCE包含创建XAML代码并设置其样式的功能,但设计人员选择的工具是Microsoft Expression Blend(EB)。这是一个设计和布局软件包,可用于创建XAML文件,接着开发人员就可以使用XAML文件创建应用程序。实际上,EB使用与VS和VCE相同的解决方案和项目文件,所以可以在这些环境中的一个中创建项目,在另一个环境中编辑它。在EB中,在编辑后台编码文件时,只需在Files窗口(等价于VS和VCE中的Solution Explorer)中双击它。图34-2显示了EB界面。 图 34-2   在microsoft.com/expression/products/overview.aspx?key=blend上可以找到有关EB试用版本(和EB2,在编写本书时,它正在开发)的许多信息,并可以下载。但是,编写WPF应用程序或编辑XAML并不需要EB。图34-3显示了图34-2中的项目,但该项目在VCE中加载。 图 34-3   提示:   在VS和VCE的2005和2008版本之间有一个区别,它会妨碍EB当前版本的使用。但是,可以使用一个配置工具,重新配置EB,以使用VS2008和VCE 2008。这个应用程序可以从http://blogs.msdn.com/expression/archive/2007/05/29/working-with-visual-studio-code-name-orcas-and-expression-blend.aspx上获得。   在运行配置程序(BlendConfiguration.exe)时,选择把EB配置为使用Visual Studio代码名Orcas。   这个问题有望在EB或EB2的未来版本中解决。   在VCE中,默认屏幕会在两个窗格上显示XAML(目前不必考虑显示在XAML视图中的代码)和这些XAML的预览。属性编辑器不太直观,选择对象时,这个编辑器有一些略微古怪的特性。在图34-3中,选择了Label控件,但选区不完全匹配这个控件。   应用程序在两个环境中加载的效果相同,如图34-4所示。 图 34-4 34.1.2 WPF给C#开发人员带来的好处   如上一节所述,开发人员可以创建能在VS或VCE中工作的项目和解决方案,而设计人员可以在Expression Blend中编辑这些项目和解决方案。但与设计人员不同,开发人员主要在VS或VCE中工作。   如本节的引言所述,WPF使用与ASP.NET非常类似的后台编码模型。例如,把Click属性添加到表示按钮控件的XML元素中,就可以给按钮控件添加一个事件处理程序。这个属性在XAML页面的后台编码文件中指定了事件处理程序的名称,事件处理程序可以用C#编写。   注意,还可以在WPF应用程序中操作控件,其方式类似于Windows窗体应用程序使用编程技术布置用户界面。可以使用后台代码实例化控件,设置其属性,添加事件处理程序,给窗口添加控件,这完全绕过了XAML。其代码一般比对应的XAML声明代码长得多,且无法分隔开设计和功能。编程方式在某些情形下是必须的,但一般应使用XAML来布置用户界面上的控件。   本章主要从C#开发人员的角度来阐述。WPF是一个需要一本书来介绍的主题,所以本章仅简要探讨它。 34.2 基本WPF应用程序的组成   WPF使用起来很直观,学习它的最佳方式是开始使用它。如果读者已经阅读了本书的其他章节,就会很熟悉许多技术。   下面的示例要创建一个简单的WPF应用程序,在该示例的说明中,会详细探讨代码和结果,了解如何把代码组合起来。   试试看:创建一个基本的WPF应用程序   (1) 创建一个新WPF应用程序Ch34Ex01,将其保存在C:\BegVCSharp\Chapter34目录下。   (2) 修改Window1.xaml中的代码,如下所示:                         但是,下面的代码不能工作,因为有两个地方使用了内容语法,它们的中间用一个使用属性元素语法的元素隔开了:   6. 标记扩展   前面的示例说明了标记扩展也可以用于属性值——例如,值{x:null}。只要使用了花括号{},就是在使用标记扩展。这些标记扩展都可以在特性语法和属性元素语法代码中使用。   本章的所有标记扩展都专门用于WPF。WPF特有的扩展包括用于引用资源和数据绑定的扩展。 34.3.2 桌面和Web应用程序   本章前面的示例演示了WPF应用程序如何作为独立的桌面应用程序和Web应用程序运行。前面把WPF Application和WPF Browser Application项目模板作为起点,添加了XAML和C#代码,以完成该应用程序。WPF Application模板把项目编译为.exe文件,WPF Browser Application模板把项目编译为.xbap文件。   提示:   XBAP(读作ex-bap)是XAML Browser Application的首字母的缩写,用WPF创建的Web应用程序常常称为XBAP应用程序。   这些应用程序类型之间的大多数区别都是项目文件(.csproj)之间的区别。Web浏览器应用程序定义了一些额外的设置,包括两个应用程序的签名和应用程序的清单(如前所述,为了安全起见),以及允许在浏览器中调试的设置。还有一个测试凭证,可用于这个签名。在产品环境下,需要用自己的证书替代这个凭证。   在桌面WPF和Web WPF应用程序之间转换是一个难度很高的过程,因为必须改变许多设置,还要修改一些XAML代码,如示例所示。这些改动包括把改为,删除位图效果等功能。最好的方法通常是创建独立的项目,如前面的示例所示。目前最佳的解决方案是由Karen Corby实现的,可以从她的博客http://scrobs.com/2006/06/04/vs- template-flexible-application/上获得。Karen创建了一个灵活的应用程序模板,可以用于替代默认的WPF Application和WPF Browser Application模板。这个模板可以使用IDE上的配置下拉框在桌面应用程序和Web应用程序之间切换。如果希望常常在应用程序类型之间切换,这个模板就值得一试,但实际上我们总是要选择一个环境,并一直使用它。   图34-8显示了如何使用Karen的灵活应用程序模板切换到XBAP输出。 图 34-8   必须手工完成的一个任务是禁用不能在XABP应用程序(它运行在部分信任级别上)上使用的特性。例如,不能使用位图效果。有关在部分信任的环境下可以和不能使用的完整特性列表,可参见MSDN文档中的Windows Presentation Foundation Partial Trust Security。 34.3.3 Application对象   在WPF中,大多数应用程序(包括所有的XBAP应用程序和使用WPF Application模板的桌面应用程序)都包含一个派生自System.Windows.Application的类实例。在前面的示例应用程序中,这个对象由App.xaml和App.xaml.cs文件定义。Ch34Ex01的App.xaml如下所示:   元素的语法类似于前面讨论的元素,它以相同的方式使用x:Class特性,把代码链接到后台代码中的部分类定义上。   这段代码定义的对象是WPF应用程序的入口。这个对象只能有一个实例,使用静态属性Application.Current可以通过代码访问它。应用程序的Application对象非常有用,原因如下: ● 它提供了在应用程序的生命周期的特定时刻引发的许多事件,包括前面介绍的LoadCompleted和DispatcherUnhandledException,LoadCompleted在应用程序加载并显示时引发,DispatcherUnhandledException在抛出一个未处理的异常时引发。 ● 它包含的方法可以用于设置或加载cookie,定位和加载资源等。 ● 它的几个属性可用于访问应用程序范围内的资源(参见“静态和动态资源”一节)和应用程序中的窗口。   Application对象引发的事件是这个列表中最有用的特性,也是最常用的特性。 34.3.4 控件基础   WPF提供了许多可用于创建应用程序的控件。本章简要介绍WPF及其功能,所以不详细探讨每个WPF控件,而是通过使用它们来学习。   下面列出了WPF提供的控件: Border BulletDecorator Button CheckBox ComboBox ContextMenu DocumentViewer Expander FlowDocumentPageViewer FlowDocumentReader FlowDocumentScrollViewer Frame GroupBox Image Label ListBox ListView Menu PasswordBox Popup ProgressBar PrintDialog RadioButton RepeatButton RichTextBox ScrollBar ScrollViewer Separator Slider StatusBar TabControl TextBlock TextBox ToolBar ToolTip TreeView Viewbox   提示:   这个列表不包含用于布局的WPF控件,这些控件参见本章后面的内容。   这里列出的一些控件有非常熟悉的名称,实际上,它们的功能与Windows窗体和ASP.NET应用程序中的对应控件非常类似。例如,Button控件可用于显示按钮。其他一些控件您可能不太熟悉,所以需要试用,以了解它们的功能。   这些控件最初都只有非常基本的外观。为了美化它们,必须给它们设置样式,这会使WPF的强大功能变得非常明显,如本章后面所述。除了设置样式之外,WPF控件还使用了其他几个特性。本节将介绍如下内容: ● 依赖属性 ● 关联属性 ● 路由事件   与其他桌面应用程序和Web应用程序开发一样,也可以创建自己的控件,而且总是要创建这种控件。在创建控件时,可以使用这里介绍的所有特性。下面几节会举几个例子。   1. 依赖属性   依赖属性是在整个WPF中使用的一种属性,尤其是在控件上使用,它提供了扩展一般.NET属性的功能。为了说明这一点,考虑某个一般的.NET属性。在.NET中创建类时,一般要使用非常简单的代码来实现属性: private string aStringProperty; public string AStringProperty { get { return aStringProperty; } set { aStringProperty = value; } }   这里定义了一个公共属性,它以一个私有字段为基础。这些简单的执行代码绝对适用于大多数目的,但除了基本的状态访问之外,没有包含太多的功能。例如,如果要给控件ControlA添加一个AStringProperty,让另一个控件ControlB响应对该属性的改动,就必须执行如下步骤:   (1) 使用前面的代码给控件ControlA添加AStringProperty。   (2) 给ControlA添加一个事件。   (3) 给ControlA添加一个方法,来引发事件。   (4) 在ControlA中为AStringProperty的set访问器添加代码,以调用事件,引发方法。   (5) 给ControlA添加代码,以订阅ControlA中的事件。   提示:   这里不需要列出代码,因为本书已经多次列出了这些代码。   这种方法存在的问题是要达到这个效果,没有可以遵循的明确标准。不同的开发人员可能以不同的方式添加代码,来达到相同的效果。而且,这需要在开发控件时标识所有可能需要通知他人的属性。   这个问题的WPF解决方案是用依赖属性替代前面代码中使用的简单属性定义,然后使用格式化的、结构化的技术提供属性改变通知。依赖属性用WPF属性系统注册,允许使用扩展的功能。这个扩展功能包括(但不仅限于)属性改动的自动通知。依赖属性有如下特性: ● 可以使用样式改变依赖属性的值。 ● 可以使用资源或通过数据绑定设置依赖属性的值。 ● 可以改变动画中依赖属性的值。 ● 可以在XAML中按层次设置依赖属性——在父元素上设置的依赖属性值可以用于设置其子元素的对应依赖属性的默认值。 ● 使用定义好的编码模式可以配置属性值改动的通知。 ● 可以配置一系列相关属性,在改变其中一个属性值时,它们就会全部更新。这称为强制转换。改变的属性会强制改变其他属性的值。 ● 可以把元数据应用于依赖属性,指定其他行为特征。例如,可以指定如果给定属性改变了,就需要重新布置用户界面。   实际上,由于依赖属性的实现方式,我们可能最初注意不到它们与一般属性有什么区别。但是,在创建自己的控件时,会很快发现如果使用一般的.NET属性,有许多功能都突然消失了。   依赖属性在WPF中使用得非常普遍,所以后面将介绍如何实现它们。   2. 关联属性   关联属性可以用于定义它的类实例的每个子对象。例如,假定有一个类Recipe,它可以包含表示成分的子对象。在类Recipe定义中可以定义一个关联属性Quantity,它可以由每个子对象使用。注意,子对象不必为关联属性指定值。   这么做的主要原因是用于设置关联属性值的XAML代码很容易理解:             这里使用的语法是前面介绍的特性语法的一种形式。其中关联属性使用父元素名、句点和关联属性名来表示。   在WPF中,关联属性有许多用途。稍后在“控件布局”一节中介绍如何定位控件时,会介绍许多关联属性。我们将学习容器控件如何定义关联属性,让每个子控件都可以确定要停靠在容器的哪条边上。   3. 路由事件   WPF应用程序的本质是层次结构,所以其中的控件常常包含其他控件,这些控件又包含更多的控件。路由事件是指事件影响层次结构中的一个控件,该控件又影响层次结构中的其他事件的机制,且不需要复杂的代码。   一个很好的例子是允许用户用鼠标与应用程序交互操作,当然这非常常见。用户单击应用程序中的按钮时,一般要响应单击事件。Windows窗体和ASP.NET开发中的一种常见方式是为按钮的事件提供事件处理程序,来响应鼠标的单击。   这种技术存在很大的局限性,且在一些Windows窗体应用程序中会导致代码比较混乱,尽管初看起来不是这样。其原因是需要某种机制引发某个按钮的单击事件,标识该控件应响应鼠标单击,而这并不是非常明显。在这里举出的简单例子中,是应引发按钮的单击事件,还是应引发包含按钮的Window的单击事件?如果按钮和Window都有事件处理程序,且只引发了其中一个事件,一般希望引发按钮的事件。但如果希望引发两个事件,那么引发事件的顺序如何?对于Windows窗体应用程序,这需要编写相当复杂的定制代码。   WPF控件(包括Button和Window)的鼠标单击事件实现为路由事件,解决了这个问题。路由事件由层次结构中的所有对象按指定顺序引发,可以完全控制响应它们的方式。   例如,假定Window包含一个Grid,Grid又包含一个Rectangle。单击Rectangle时,事件的引发顺序如下:   (1) 引发Window上的鼠标按下事件   (2) 引发Grid上的鼠标按下事件   (3) 引发Rectangle上的鼠标按下事件   (4) 引发Rectangle上的另一个鼠标按下事件   (5) 引发Grid上的另一个鼠标按下事件   (6) 引发Window上的另一个鼠标按下事件。   添加适当的事件处理方法,就可以响应上述序列中的任一个事件。还可以在事件处理方法的任一点中断该序列,但事件处理程序默认不会中断该序列。这说明,可以在一个事件(这里是一个鼠标按下事件)中触发多个事件处理方法。   在描述前面的事件序列时,WPF引入了一个有用的术语。事件在控件的层次结构中向下移动时,称为通道(tunneling);向上移动时,称为冒泡(bubbling)。   另外,只要在WPF中使用了路由事件,事件名就可以指出该事件是通道事件还是冒泡事件。所有的通道事件都以前缀Preview开头。例如,Window控件有PreviewMouseDown和MouseDown 事件。可以给它们中的一个或两个添加处理程序,或者不添加任何处理程序。   图34-9列出了上述事件的引发顺序,并说明了事件在控件层次结构中如何通过通道和冒泡。 图 34-9   路由事件处理程序有两个参数:事件源和RoutedEventArgs实例或派生于RoutedEventArgs的类。   执行路由事件的事件处理程序时,可以把RoutedEventArgs对象的Handled属性设置为true。这样,就不会执行进一步的处理了,即该事件不会引发更多的事件处理程序。   RoutedEventArgs还有一个属性Source,它允许指定哪个控件最先引发了事件。这是WPF最初检测到事件的控件,所以在图34-9中,该控件应是Rectangle。这是非常有用的,因为父控件可以确定单击了哪个子控件。注意,这个“单击测试”是相当复杂的,例如,WPF可以忽略控件透明区域上的单击,我们不需要做任何工作,就可以启用这个功能。另外,还可以创建透明控件,来响应鼠标单击,所以灵活性非常大。   还要注意,路由事件覆盖了多个鼠标单击,它们可以用于各种目的,包括键盘交互操作、数据绑定、计时器等。稍后介绍的关联事件会使路由事件更有用。   下面的示例演示了本节描述的情形,还介绍了路由事件的其他信息。   试试看:处理路由事件   (1) 创建一个新WPF应用程序Ch34Ex02,将其保存在C:\BegVCSharp\Chapter34目录下。   (2) 修改Window1.xaml的代码,如下所示:         (3) 修改Window1.xaml.cs的代码,如下所示(注意,根据所使用的IDE和输入XAML代码的方式,可能会自动添加空的事件处理方法): public partial class Window1 : Window { private void Generic_MouseDown(object sender, MouseButtonEventArgs e) { outputText.Text = string.Format( "{0}Event {1} raised by control {2}. e.Source={3}\n", outputText.Text, e.RoutedEvent.Name, sender.ToString(), ((FrameworkElement)e.Source).Name); } private void Window_MouseUp(object sender, MouseButtonEventArgs e) { outputText.Text = string.Format( "{0}==========\n\n", outputText.Text); } private void clickMeButton_Click(object sender, RoutedEventArgs e) { outputText.Text = string.Format( "{0}Button clicked!\n==========\n\n", outputText.Text); } }   (4) 运行应用程序。当程序运行时,单击左上角的矩形、矩形和按钮之间的浅蓝色区域,以及按钮上的矩形,结果如图34-10所示。 图 34-10   示例的说明   这个示例突出显示了所有WPF控件都有的PreviewMouseDown和MouseDown 事件,说明了如何处理路由事件。还介绍了在事件链中包含按钮时会发生什么。所使用的XAML代码非常简单,其基本部分(在这个示例中)如下: ...   在事件处理程序中,从sender参数中获得Grid控件的一个引用,然后使用RoutedEventArgs.Source属性确定单击了哪个按钮,并做出相应的响应。这个事件仅在单击按钮时触发,而在单击Grid控件的背景时不会触发,因为Grid控件没有Click事件。 34.3.5 控件的布局   本章的前面一直使用Grid元素布置控件,这主要是因为在创建新的WPF应用程序时,这是默认提供的控件。但是我们还没有充分利用这个类的全部功能,也没有学习能得到其他布局效果的其他布局容器。本节将详细介绍控件布局,因为这是WPF中需要掌握的一个基本概念。   所有的内容布局控件都派生于抽象类Panel。这个类仅定义了一个容器,该容器可以包含派生于UIElement的对象集合。所有的WPF控件都派生于UIElement。不能直接使用Panel类控制布局,但可以派生于它。另外,还可以使用下面派生于Panel的布局控件: ● Canvas:这个控件可以以任意方式定位子控件。它对子控件的定位没有任何限制,但也没有给子控件的定位提供任何帮助。 ● DackPanel:这个控件可以把子控件停靠在它的4条边上。最后一个子控件会占据剩余的空间。 ● Grid:前面已经介绍了这个控件如何灵活地定位子控件。但还没有介绍如何把这个控件的布局分成行和列,使控件在栅格布局中对齐。 ● StackPanel:这个控件以水平或垂直布局来布置其子控件。 ● WrapPanel:这个控件与StackPanel一样,也以水平或垂直布局来布置其子控件,但它不是仅在单行或单列上布置控件,而是根据可用的空间,允许把子控件放在多行或多列上。   下面详细介绍这些控件的用法。但首先要理解几个基本概念: ● 控件如何以堆栈顺序显示 ● 如何使用对齐、页边距和填充来定位控件及其内容 ● 如何使用Border控件   1. 堆栈顺序   容器控件包含多个子控件时,它们会以指定的堆栈顺序显示。读者应在绘图软件包中熟悉这个概念。理解堆栈顺序的最佳方式是假定每个控件放在一个玻璃盘上,而容器控件包含一叠这样的玻璃盘。因此,如果通过这些透明的玻璃从上向下看,就可以看到容器的内容。包含在容器中的控件重叠放置,因此看到的内容取决于玻璃盘的顺序。如果某个控件在该叠玻璃盘偏上的位置,就可以在重叠的区域中看到它。而位于该叠玻璃盘偏下位置的控件就会被其上的控件部分或全部隐藏。   用鼠标单击窗口时,这也会影响单击测试。考虑到控件的重叠,目标控件应总是位于堆栈最上面的那个控件。控件的堆栈顺序由它们显示在容器的子控件列表中的顺序确定。容器中的第一个子控件放在堆栈最下面的一层,最后一个控件放在堆栈最上面的一层。在第一个和最后一个子控件中间的其他子控件放在中间的层上。在可以在WPF中使用的一些布局控件中,子控件的堆栈顺序还有其他含义,如后面所述。   2. 对齐、页边距、填充和尺寸   前面的示例说明了Margin、HorizontalAlignment和VerticalAlignment如何在Grid容器中定位控件。还说明了如何使用Height和Width指定尺寸。这些属性和还没有探讨的Padding属性对所有的(或大多数)布局控件都很有用,但它们的使用方式不同。不同的布局控件也可以为这些属性设置默认值。后面的各小节会介绍一些示例,但在此之前先介绍基本概念。   两个对齐属性确定了控件的对齐方式,但我们并没有列出这些属性的所有值。例如,HorizontalAlignment可以设置为Left、Right、Center或Stretch。Left和Right会把控件定位在容器的左或右边界上,Center把控件定位在中心,Stretch会改变控件的宽度,使其边界延伸到容器的边界上。VerticalAlignment与此类似,其值有Top、Bottom、Center或Stretch。   Margin和Padding分别指定在控件的边界周围和控件边界的内部预留多少空间。前面的示例使用Margin指定控件相对于Grid左上角的位置。这是有效的,因为把HorizontalAlignment设置为Left,VerticalAlignment设置为Top,控件就会位于容器的左上角,而Margin在控件的边界周围插入了一个空隙。Padding的用法类似,但在控件的内容与其边界之间空出了一些地方。这对Border控件尤其有用,如下一节所述。Margin和Padding都可以指定为4个部分值(其形式是leftAmount、topAmount、rightAmount、bottomAmount)或者指定为一个值(Thickness值)。   Height和Width常常由其他属性控制。例如,把HorizontalAlignment设置为Stretch,控件的Width属性就随着其容器宽度的变化而变化。   3. Border控件   Border控件是一个非常简单且非常有用的容器控件,它包含一个子控件,而不像稍后介绍的更复杂的控件那样包含多个控件。这个子控件会完全填满Border控件。这似乎没有什么用,但可以使用Margin和Padding属性把Border定位在其容器中,把Border的内容定位在Border的边界中。也可以设置Border的Background属性,使之可见。稍后介绍这个控件。   4. Canvas控件   如前所述,Canvas控件对子控件的定位提供了完全的自由。另外,用于某个子元素的HorizontalAlignment和VerticalAlignment属性不会影响其他元素的定位。   使用Margin可以在Canvas中定位元素,如前面的示例所示。但更好的方式是使用Canvas类的Canvas.Left、Canvas.Top、Canvas.Right和Canvas.Bottom关联属性:   上面的代码定位了一个按钮,使其顶边距离Canvas的顶边10个像素,其左边界距离Canvas的左边界10个像素。注意Top和Left属性的优先级高于Bottom和Right。例如,如果指定了Top和Bottom属性,就忽略Bottom属性。   图34-11显示了Canvas控件中定位的两个Rectangle控件,但窗口有两个不同的尺寸。 图 34-11   提示:   本节的所有示例布局都在本章下载代码的LayoutExamples项目中。   一个Rectangle相对于Canvas控件的左上角定位,另一个Rectangle相对于Canvas控件的右下角定位。在重新设置窗口的大小时,这些相对位置保持不变。还可以看出Rectangle控件的堆栈顺序的重要性。右下角的Rectangle在堆栈顺序中比较高,所以两个Rectangle重叠时,就可以看到右下角的Rectangle。   这个示例的代码如下:        5. DockPanel控件   顾名思义,DockPanel控件允许把控件停靠在它的一个边界上。读者即使以前从来没有注意过,也应很熟悉这类布局。Word中的Ribbon控件就总是位于Word窗口的顶部,VS和VCE中的各个窗口也是用这种方式定位的。在VS和VCE中,拖动窗口就会改变窗口的停靠方式。   DockPanel有一个关联属性DockPanel.Dock,子控件可以使用该属性指定它停靠在哪条边上。这个属性可以设置为Left、Top、Right和Bottom。   DockPanel中控件的堆栈顺序非常重要,因为每次把控件停靠在一条边上时,都会相应地减小后续子控件的可用空间。例如,把一个工具条停靠在DockPanel的顶部,再把第二个工具条停靠在DockPanel的左边。第一个控件会延伸到DockPanel显示区域的整个顶部,但第二个控件只能从第一个工具条的底部开始沿着DockPanel的左边界延伸到DockPanel的底部。   在前面的子控件都定位好后,最后一个指定的控件通常会占据剩余的空间(可以控制这个行为)。   在DockPanel中定位控件时,该控件占据的空间可能比DockPanel给该控件预留的区域小。例如,如果把Width为100、Height为50、HorizontalAlignment为Left的按钮停靠在DockPanel的顶部,则该按钮右边的空间就不能用于其他停靠的子控件。另外,如果按钮控件的Margin为20,就要在DockPanel顶部预留总共90个像素(控件的高度加上边距和下边距)。在用DockPanel进行布局时,要考虑到这种情况;否则就得不到期望的结果。   图34-12显示了一个DockPanel布局示例。 图 34-12   这个布局的代码如下所示:             这段代码在示例布局中使用前面介绍的Border控件清楚地划分了停靠控件的区域,再使用Label控件输出简单的信息文本。为了理解该布局,必须从上而下地读取代码,依次查看每个控件:   (1) 第一个Border控件停靠在DockPanel的顶部。DockPanel中被占据的总区域是顶部的55个像素(Height+2×Margin)。注意,Padding属性对这个布局没有影响,因为它在Border边界的内部,但这个属性控制着其内嵌Lable控件的定位。如果没有Height或Width属性的限制,Border控件就会在DockPanel中延伸,填满DockPanel顶部的所有可用空间。   (2) 第二个Border控件也停靠在DockPanel的顶部,占据了显示区域顶部的另外55个像素。这个Border控件还包含Width属性,使Border仅占据DockPanel的部分宽度。它位于中心,因为DockPanel中的HorizontalAlignment的默认值是Center。   (3) 第三个Border控件停靠在DockPanel的左边,占据显示区域左边的210个像素。   (4) 第四个Border控件停靠在DockPanel的底部,占据30个像素加上它包含的Label控件的高度。这个高度由Margin、Padding和Border控件的内容确定,因为它没有明确指定。Border控件锁定在DockPanel的右下角,因为它的HorizontalAlignment为Right。   (5) 第五个也是最后一个Border控件填满了剩余的空间。   运行这个示例,试着改变其内容的尺寸。注意,控件的堆栈顺序越大,获得其空间的优先级就越高。缩小窗口,第五个Border控件会很快被堆栈顺序较大的控件覆盖,使用DockPanel控制布局时要小心避免这种情况,例如,可以设置窗口的最小尺寸。   6. Grid控件   Grid控件可以包含多行和多列,用于布置子控件。本章前面已经使用过几次Grid控件,但所有的示例都使用单行单列的Grid控件。要添加更多的行和列,必须使用RowDefinitions和ColumnDefinitions属性,它们分别是RowDefinition和ColumnDefinition对象的集合,用属性元素语法指定: ...   这段代码定义了一个包含3行2列的Grid控件。注意,这里不需要额外的信息。在这段代码中,Grid控件重新设置大小时,其每一行和每一列都会自动重置大小。每一行的高度都是Grid控件高度的1/3,每一列的宽度都是Grid控件宽度的一半。把Grid.ShowGridlines属性设置为true,就可以在Grid的单元格之间显示线条。   Width、Height、MinWidth、MaxWidth、MinHeight和MaxHeight可以用于控制尺寸的重置。例如,设置列的Width属性可以确保该列的宽度保持不变。也可以把列的Width属性设置为*,表示“计算完其他列的宽度后填满剩余的空间”。这是默认值。多个列的宽度都是*时,剩余的空间就由它们平分。*值也可以用于行的Height属性。Height和Width的另一个值是Auto,表示根据控件的内容设置行或列的大小。也可以使用GridSplitter控件,让用户通过单击和拖动操作来定制行和列的尺寸。   Grid控件的子控件可以使用关联属性Grid.Column和Grid.Row指定它们包含在什么单元格中。这些属性的默认值都是0,所以如果忽略这两个属性,子控件就位于左上单元格中。子控件也可以使用Grid.ColumnSpan和Grid.RowSpan放在表的多个单元格中,此时左上单元格由Grid.Column和Grid.Row指定。   图34-13中的Grid控件包含多个椭圆和一个GridSplitter,且窗口有两个不同的尺寸。 图 34-13   所使用的代码如下:                                                          这段代码使用行和列定义上的各种属性,在重置显示区域的大小时,获得了有趣的效果,所以最好测试一下。   首先考虑行。顶行的高度是固定的50像素,第二行的最小高度设置为100像素,第三行填满了剩余的空间。这表示如果Grid的高度小于150像素,第三行就是不可见的。Grid的高度在150~250像素之间时,只有第三行的尺寸会在0~100像素之间变化。这是因为剩余的空间应计算为总高度减去高度固定的行的高度之和。剩余的空间位于第二行和第三行之间,但因为第二行的最小高度是100像素,所以它的高度不会变化,除非Grid的总高度达到250像素。最后,Grid的总高度超过250像素时,第二行和第三行会分享剩余的空间,所以它们的高度都等于或大于100像素。   接着看看列。只有第三列的尺寸是固定的50像素。第一列和第二列的总宽度是300像素。因此,在Grid控件的总宽度超过550像素时,只有第四列的尺寸会增大。为了弄明白这一点,应考虑列可用的像素值是多大,它们是如何分配的。首先,把50个像素分配给第三列,剩余的500像素分配给其余的列。第三列的最大宽度是100像素,第一列和第四列的宽度就剩下400像素。第一列的最大宽度是200像素,所以即使宽度超过了这个值,第一列也不会占据更多的空间。而第四列的尺寸会增加。   注意这个示例的另外两点。首先,最后一个定义的椭圆占据了第三列和第四列,以演示Grid.ColumnSpan的用法。其次,提供了一个GridSplitter,允许重置第一列和第二列的大小。但是,一旦Grid控件的总宽度超过550像素,这个GridSplitter就不能设置这些列的大小,因为第一列和第二列都不能增大尺寸了。   GridSplitter控件很有用,但其外观很呆板。这是一个需要设置样式的控件,至少要把其Background属性设置为Transparent,使之不可见。   如果窗口中有多个Grid控件,还可以在行或列定义上使用ShareSizeGroup属性为行或列定义共享的尺寸组。ShareSizeGroup属性可以设置为字符串标识符。例如,如果在一个Grid控件中,共享尺寸组中的一列改变了,则该组中另一个Grid控件的列也会改变,以匹配前一个Grid控件。通过Grid.IsShareSizeGroup属性可以启用或禁用这个功能。   7. StackPanel控件   明白Grid控件的复杂性之后,读者会发现StackPanel控件是一个相对简单的布局控件。可以将StackPanel控件看作DockPanel的一个删节版本,因为子控件停靠的边是固定的。这两个控件的另一个区别是StackPanel的最后一个子控件不填满剩余的空间。但是,控件会默认延伸到StackPanel控件的边界上。   控件堆叠的方向由3个属性确定。Orientation可以设置为Horizontal或Vertical,Horizontal Alignment和VerticalAlignment可以确定控件是沿着StackPanel的顶边、底边、左边还是右边定位。给所使用的对齐属性设置Center值,甚至可以使控件在StackPanel的中心处堆叠。   图34-14显示了两个StackPanel控件,它们都包含3个按钮。StackPanel控件使用包含两行一列的Grid控件定位。 图 34-14   这里使用的代码如下:   使用StackPanel来布局时,常常需要添加滚动条,以便查看包含在StackPanel中的所有控件。WPF为我们做了许多工作。使用ScrollViewer控件就可以添加滚动条——只需在这个控件中包含StackPanel:   可以使用更复杂的技术以不同的方式滚动,或者用编程方式滚动,但上述代码常常就是需要编写的代码。   8. WrapPanel控件   WrapPanel实际上是StackPanel的一个扩展版本,其中“不合适”的控件会移动到附加的行或列上。图34-15显示的WrapPanel控件包含多个形状,而窗口设置为两个不同的尺寸。 图 34-15   代码的缩减版本如下: ...   WrapPanel控件是创建动态布局的一种好方法,允许用户控制内容的显示方式。 34.3.6 控件的样式   WPF的一个杰出特性是它允许设计人员全面控制用户界面的外观和操作方式。其核心是可以给控件设置任意二维或三维样式。前面一直使用的是.NET 3.5为控件提供的基本样式,但控件可以使用的样式是无穷多的。   本节介绍两种基本技术: ● 样式:成批应用于控件的属性组 ● 模板:用于建立控件外观的控件   这两种技术有些重叠,因为样式可以包含模板。   1. 样式   WPF控件有一个Style属性(继承自FrameworkElement),它可以设置为Style类的实例。Style类相当复杂,具有高级样式化功能,但其核心是一组Setter对象。每个Setter对象都根据Property属性(要设置的属性名)及Value属性(属性要设置的值)设置属性值。可以把在Property中使用的名称完全限定为控件类型(如Button.Foreground),也可以设置Style对象的TargetType属性(如Button),使之可以解析属性名。   下面的代码说明了如何使用Style对象设置Button控件的Foreground属性:   显然,在这段代码中,以通常的方式设置按钮的Foreground属性要简单得多。把样式转换为资源后,样式会非常有用,因为资源是可以重用的。本章后面将介绍其转换过程。   2. 模板   控件用模板构建,而模板是可以定制的。模板包含一个控件层次结构,用于建立控件的显示外观,该外观可能包含控件的内容显示器,例如,显示内容的按钮。   控件的模板常常存储在其Template属性中,Template属性是ControlTemplate类的一个实例。ControlTemplate类包含TargetType属性,它可以设置为用于定义模板的控件类型,这个属性可以包含单个控件,也可以是一个容器,例如Grid,所以它不会限制我们的操作。   一般使用样式来设置类的模板。这只需用如下方式为要使用的控件提供Template属性:   一些控件需要多个模板。例如,CheckBox控件使用一个模板表示其复选框(CheckBox. Template),使用另一个模板输出复选框旁边的文本(CheckBox. ContentTemplate)。   需要内容显示器的模板可以在输出内容的位置上包含ContentPresenter控件。一些控件,尤其是输出数据项集合的控件,使用另一种技术,本章不讨论该技术。   在与资源合并使用时,替代模板是很有用的。但是,设置控件的样式是一种非常常见的技术,需要用一个示例来说明。   试试看:使用样式和模板   (1) 创建一个新WPF应用程序Ch34Ex03,将其保存在C:\BegVCSharp\Chapter34目录下。   (2) 修改Window1.xaml的代码,如下所示: < Window x:Class="Ch34Ex03.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Nasty Button" Height="150" Width="550">      (3) 修改Window1.xaml.cs中的代码,如下所示: public partial class Window1 : Window { ... private void Button_Click(object sender, RoutedEventArgs e) { MessageBox.Show("Button clicked. "); } }   (4) 执行应用程序,单击一次按钮,结果如图34-16所示。 图 34-16   示例的说明   这个示例中的按钮非常难看。但是,抛去美观方面的因素,这个示例其实说明了可以在WPF中毫不费力地全面控制按钮的外观。但改变按钮的模板时,注意按钮的功能没有变化。即可以单击按钮,在事件处理程序中响应该单击。   注意在这里使用的模板上没有实现与Windows按钮关联的一些操作。例如,把鼠标停放在按钮上或单击按钮时,没有可视化的反馈。这个按钮无论是否获得焦点,其外观都是相同的。为了得到这些效果,需要学习触发器,这是下一节的内容。   但在此之前,先详细分析一下示例代码,主要关注样式和模板,看看模板是如何创建的。   刚开始的代码很普通,用于显示按钮控件:   模板代码可以汇总为一个Grid控件,它在一行中有3个单元格。这些单元格包含一个Ellipse、一个Rectangle、模板的ContentPresenter和另一个Ellipse: ... ... ...   这些代码都不复杂,下面要进一步分析它们。 34.3.7 触发器   在本章的第一个示例中,学习了触发器可以把事件关联到动作上。WPF中的事件可以包含所有的操作,包括按钮单击、应用程序启动和停止等。在WPF中有几种类型的触发器,它们都继承自基类TriggerBase。示例中使用的触发器类型是EventTrigger。EventTrigger类包含一个动作集合,每个动作都是派生于基类TriggerAction的一个对象。这些动作都在激活触发器时执行。   在WPF中,并不是许多类都继承自TriggerAction,当然,我们可以定义自己的类。可以使用EventTrigger通过BeginStoryboard动作触发动画,通过ControllableStoryboardAction处理故事板,通过SoundPlayerAction触发声音效果。最后一个触发器在动画中最常用,所以下一节介绍它。   每个控件都有Triggers属性,用于直接在该控件上定义触发器。也可以在该层次结构中进一步定义触发器,例如,在前面所示的Window对象上定义触发器。在给控件设置样式时,最常用的触发器类型是Trigger(也可以使用EventTrigger引发控件动画)。Trigger类用于设置属性,以响应其他属性的变化,在Style对象中使用该类时特别有用。   Trigger对象的配置如下: ● 要定义Trigger对象监控的属性,使用Trigger.Property属性。 ● 要定义Trigger对象的激活时间,设置Trigger.Value属性。 ● 要定义Trigger对象执行的动作,把Trigger.Setters属性设置为Setters对象的集合。   这里的Setter对象就是前面“样式”一节中的Setter对象。   例如,下面的触发器将检查属性MyBooleanValue的值,在该属性为true时,把Opacity属性的值设置为0.5:   这段代码并没有给出许多信息,因为它不与任何控件或样式关联。下面的代码比较容易理解,因为它说明了Trigger在Style对象中的使用:   这段代码在Button.IsMouseOver属性为true时,把按钮控件的Foreground属性改为Yellow。IsMouseOver是几个非常有用的属性之一,可以用作查找控件信息和控件状态的快捷方式。顾名思义,如果鼠标停放在控件上,这个属性就是true。此时可以给鼠标的停放操作编码。类似于这个属性的其他属性有IsFocused,它确定控件是否获得焦点;IsHitTestVisible,它指定是否能单击某个控件(即该控件没有被堆栈顺序更高的控件遮挡住);以及IsPressed,它指定按钮是否被按下。最后一个属性仅应用于继承自ButtonBase的按钮,而其他属性可以应用于所有控件。   除了Style.Triggers属性之外,还可以使用ControlTemplate.Triggers属性获得许多信息。这个属性可以为包含触发器的控件创建模板。所以默认的Button模板可以响应鼠标的停放、单击和焦点的变化。我们必须修改这个模板,自己实现该功能。 34.3.8 动画   动画是用故事板创建的。毫无疑问,定义动画的最佳方式是使用Expression Blend等设计器。也可以直接编辑XAML代码,根据后台编码中的含义来定义动画,因为XAML只是建立WPF对象模型的一种方式。   故事板用Storyboard对象定义,Storyboard对象包含一个或多个时间线。定义动画可以使用关键帧,也可以使用几个更简单的对象来封装整个动画。复杂的故事板甚至可以包含嵌套的故事板。   如示例所示,Storyboard包含在一个资源目录中,所以必须用x:Key属性标识它。   在故事板的时间线中,可以连续改变应用程序中的任意元素的属性,该属性的类型可以是double、Point或Color。这涵盖了需要连续改变的所有元素属性,所以非常灵活。有一些操作不能执行,例如,用一种笔刷完全替代另一种笔刷,但只要有了这3种类型,就总有办法达到需要的任何效果。 这3种类型都有两个关联的时间线控件,可用作Storyboard的子控件。这6个控件是DoubleAnimation、DoubleAnimationUsingKeyFrames、PointAnimation、PointAnimationUsingKey Frames、ColorAnimation和ColorAnimationUsingKeyFrames。每个时间线控件都可以使用关联属性Storyboard.TargetName和Storyboard.TargetProperty关联到特定控件的特定属性上。例如,如果要连续改变一个Rectangle控件(其Name属性是MyRectangle)的Width属性,就可以把Storyboard.TargetName和Storyboard.TargetProperty属性设置为MyRectangle和Width。也可以使用DoubleAnimation或DoubleAnimationUsingKeyFrames连续改变这个属性。   Storyboard.TargetProperty属性可以解释相当高级的语法,以便定位动画中感兴趣的属性。在本章开头的示例中,为两个关联属性使用了下面的值: Storyboard.TargetName="ellipse1" Storyboard.TargetProperty="(UIElement.RenderTransform). (TransformGroup.Children)[0].(RotateTransform.Angle)"   控件ellipse1的类型是Ellipse,TargetProperty指定椭圆在变换时旋转的角度。这个角度通过Ellipse的RenderTransform属性(继承自UIElement)和TransformGroup对象的第一个子对象(就是这个属性的值)来定位。第一个子对象是RotateTransform,角度是这个对象的Angle属性。   这个语法比较长,但是使用起来很简单。最困难的地方是确定给定的属性继承自哪个基类,但对象浏览器可以提供这方面的帮助。   下面看看无关键帧的简单动画时间线,然后再介绍使用关键帧的时间线。   1. 没有关键帧的时间线   没有关键帧的时间线是DoubleAnimation、PointAnimation和ColorAnimation。这些时间线有相同的属性名,但这些属性的类型随时间线的类型而不同(注意,所有的持续时间属性在XAML代码中都以形式[days.]hours:minutes:seconds指定),如表34-2所示。 表 34-2 属 性 用 法 Name 时间线的名称,用于在其他地方引用它 BeginTime 在触发故事板之后、时间线启动之前的时间 Duration 时间线的持续时间 AutoReverse 时间线完成时是否倒回,是否把属性返回为初始值。这个属性是一个布尔值 RepeatBehavior 把这个属性设置为指定的持续时间,会使时间线根据指定的值重复执行——一个整数后跟x(例如5x)表示时间线重复指定的次数;也可以使用Forever,让时间线重复执行到暂停或停止故事板为止 FillBehavior 如果时间线完成时,故事板仍旧在执行,时间线该如何操作。使用HoldEnd可以在时间线完成时不改变属性值(默认),使用Stop可以把属性值返回为初始值 SpeedRatio 控制动画相对于其他属性的指定值的执行速度,默认为1,但可以在其他代码中修改它,加快或减慢动画的执行 From 在动画开始时属性的初始值。可以忽略这个值,使用属性的当前值 To 在动画结束时属性的最终值。可以忽略这个值,使用属性的当前值 By 使用这个值使属性从当前值连续变化为当前值与指定值之和。这个属性可以单独使用,也可以与From属性联合使用   例如,下面的时间线使Rectangle控件(其Name属性是MyRectangle)的Width属性在5秒内在100~200之间连续变化:   2. 有关键帧的时间线   有关键帧的时间线是DoubleAnimationUsingKeyFrames、PointAnimationUsingKeyFrames和ColorAnimationUsingKeyFrames。这些时间线类使用的属性与上一节的时间线类的相同,但没有From、To和By属性,而是有一个KeyFrames属性,它是关键帧对象的集合。   这些时间线可以包含任意多个关键帧,每个关键帧都可以用不同的方式改变属性值。每类时间线都有3种关键帧: ● Discrete:离散的关键帧会使连续变化的值跳到指定的值上,没有任何切换。 ● Linear:线性的关键帧会使连续变化的值以线性变换方式连续改变为指定的值。 ● Spline:样条曲线的关键帧会使连续变化的值按照三次贝塞尔曲线函数定义的非线性变换方式,连续改变为指定的值。   因此关键帧对象有9种:DiscreteDoubleKeyFrame、LinearDoubleKeyFrame、SplineDoubleKey Frame、DiscretePointKeyFrame、LinearPointKeyFrame、SplinePointKeyFrame、DiscreteColorKey Frame、LinearColorKeyFrame和SplineColorKeyFrame。   关键帧类的3个属性与上一节介绍的时间线类的相同,但样条曲线的关键帧还有一个额外的属性,如表34-3中所示。 表 34-3 属 性 用 法 Name 关键帧的名称,用于在其他地方引用关键帧 KeyTime 关键帧的位置,表示为时间线启动后过去的时间 Value 到达关键帧时的属性值或到达关键帧时应设置的属性值 KeySpline cp1x,cp1y cp2x,cp2y形式的两组数字,它们定义了用于连续改变属性的三次贝塞尔函数(仅用于样条曲线的关键帧)   例如,连续改变一个方块中Ellipse的Center属性(其类型是Point),就可以建立该Ellipse的位置动画:   Point值在XAML代码中以x,y的形式指定。 34.3.9 静态和动态资源   WPF的另一个优秀特性是可以定义资源,如控件的样式和模板,它们可以在应用程序中重用。如果在正确的地方定义资源,还可以在多个应用程序中使用它们。   资源定义为ResourceDictionary对象中的项。顾名思义,这是指定了键的对象集合。因此在本章前面的示例代码中定义资源时使用x:Key特性:指定与资源关联的键。可以在许多地方访问ResourceDictionary对象。可以把资源包含在本地控件中、本地窗口中、本地应用程序中,或者放在外部程序集中。   引用资源有两种方式:静态和动态。注意这个区别并不意味着资源本身有任何区别,这说明不能把资源定义为静态的或动态的。区别在于如何使用它。   1. 静态资源   在设计期间知道使用什么资源,而且知道该引用不会在应用程序的生存期中变化时,就使用静态资源。例如,如果定义了一个按钮样式,用于应用程序中的按钮,在应用程序运行时就不太可能改变它。此时,就应静态引用资源。另外,使用静态资源时,资源类型在编译期间解析,所以性能很好。   要引用静态资源,可以使用下面的标记扩展语法: {StaticResource resourceName}   例如,如果为按钮控件定义了一个样式,其x:Key特性设置为MyStyle,就可以在控件中引用它,如下所示:   2. 动态资源   用动态资源定义的属性可以在运行期间改变为另一个动态资源。在许多情况下,这都是很有用的。有时希望用户能控制应用程序的一般主题,此时就可以动态分配资源。另外,有时在运行期间并不关心资源需要的键,例如,动态关联了资源程序集。   因此动态资源比静态资源更灵活。但是,动态资源有一个缺点:使用动态资源的系统开销略大,所以如果要优化应用程序的性能,就不应使用动态资源。   动态引用资源的语法非常类似于静态引用资源的语法: {DynamicResource resourceName}   例如,如果为按钮控件定义了一个样式,其x:Key特性设置为MyDynamicStyle,就可以在控件中引用它,如下所示:   3. 引用样式资源   前面介绍了如何动态和静态引用按钮控件中的样式资源。这里使用的样式资源位于本地Window控件的Resources属性中,例如: ...   每个要使用这个控件样式的按钮控件都必须在其Style属性中引用它(静态或动态)。另外,还可以定义一个样式资源,使之对给定的控件类型是全局可用的。即Style对象应用于应用程序中给定类型的每个控件。为此,只需忽略x:Key特性: ...   这是给应用程序添加主题的好方法。我们可以为所使用的各种控件类型定义一组全局样式,使它们可以在任意地方使用。   前面几节讨论了许多基础知识,下面在一个示例中综合使用它们。这个示例修改了上一个示例中的按钮控件,使用触发器和动画,把样式定义为全局可重用的资源。   试试看:触发器、动画和资源   (1) 创建一个新的WPF应用程序Ch34Ex04,将其保存在C:\BegVCSharp\Chapter34目录下。   (2) 将Ch34Ex03的Window1.xaml代码复制到Ch34Ex04的Window1.xaml中,但修改Window元素上的名称空间引用,如下所示: 元素上添加一个子元素,把   (5) 运行应用程序,验证结果与上一个示例相同。   (6) 在模板的主Grid控件和包含ContentPresenter元素的Rectangle中添加Name属性: ... ... ... ...   (7) 在元素中,在标记的前面添加如下代码: tag   (8) 运行应用程序,把鼠标停放在按钮上,如图34-17所示,按钮会闪动并发光。   (9) 单击按钮,如图34-18所示,发光效果改变了。 图 34-17 图 34-18   示例的说明   这个示例完成了两个工作。第一,定义了一个全局资源,用于格式化应用程序中的所有按钮(目前只有一个按钮,但这不重要)。第二,给上一个示例创建的样式添加了一些特性,使该样式能被接受。该样式会闪动并发光,以响应鼠标停放在按钮上和单击按钮操作。   把样式设置为全局资源,只需把