前面三章介绍了WPF资源系统,使用资源可在一个地方定义对象而在整个标记中重用他们。尽管可使用资源存储各种对象,但使用资源最常见的原因之一是通过他们的保存样式。
样式是可应用于元素的属性值集合。WPF样式系统与HTML标记中的层叠样式表(Cascading Style Sheet,CSS)标准担当类似的角色。与CSS类似,通过WPF样式可定义通用的格式化特性集合,并且为了保证一致性,在整个应用程序中应用他们。与CSS一样,WPF样式也能够自动工作,指定具体的元素类型为目标,并通过元素树层叠起来。然而,WPF样式的功能更加强大,因为他们能够设置任何依赖项属性。这意味着可以使用它们标准化未格式化的特性,如控件的行为。WPF样式也支持触发器(trigger),当属性发生变化时,可通过触发器改变控件的样式,并且可使用模板重新定义控件的内置外观。一旦学习了如何使用样式,就可以在所有WPF应用程序中使用他们。
为了理解适合使用样式的场合,分析一下简单的示例是有帮助的。设想需要标准化在窗口中使用的字体。最简单的方法是设置包含窗口的字体属性。这些属性是在Control类中定义的,包括FontFamily、FontSize、FontWeight(用于粗体)、FontStyle(用于斜体)以及FontStretch(用于压缩或扩展的变体)。得益于这些属性值得继承特性,当在窗口级别设置这些属性时,窗口中的所有元素都会使用相同的属性值,除非明确地覆盖它们。
现在考虑一种不同情形,希望只为用户界面中的一部分锁定字体。如果能在特定的容器中隔离这些元素(例如,它们都处于Grid或StackPanel面板中),就可以使用本质上相同的方法,并设置容器的字体属性。但问题未必总是这么简单。例如,可能希望使用所有按钮具有一致的字体和文本尺寸,并使用和其他元素不同的字体设置。对于这种情况,就需要一种方法在某个地方定义这些细节,并在所有应用它们的地方重用这些细节。
资源提供了一个解决方案,但有些笨拙。因为WPF中没有Font对象(只有与字体属性相关的集合),所以需要定义几个相关的资源,如下所示:
<Window.Resources> <FontFamily x:Key="ButtonFontFamily">Times New RomanFontFamily> <s:Double x:Key="ButtonFontSize">18s:Double> <FontWeight x:Key="ButtonFontWeight">BoldFontWeight>Window.Resources>
上面的代码片段(标记)为窗口添加了三个资源:第一个资源是FontFamily对象,该对象包含希望使用的字体名称;第二个资源是存储数字18的double对象;第三个资源是枚举值FontWeight.Bold。假定已将.NET名称空间System映射到XAML名称空间前缀s。如下所示:
<Window x:Class="Styles.ReuseFontWithResources" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" Title="ReuseFontWithResources" Height="300" Width="300">
一旦定义所需的资源,下一步就是在元素中实际使用这些资源。因为在应用程序的整个生命周期中,这些资源永远不会发生变化,所以使用静态资源是合理的,如下所示:
<Button Padding="5" Margin="5" FontFamily="{StaticResource ButtonFontFamily}" FontWeight="{StaticResource ButtonFontWeight}" FontSize="{StaticResource ButtonFontSize}" >A Customized ButtonButton>
这个示例可以工作,它将字体细节(所谓的magic number)移出了标记。但该例也存在两个问题:
除了资源名称相似外,没有明确指明这三个资源是相关的。这使维护应用程序变得复杂。如果需要设置更多字体属性,或决定为不同类型的元素维护不同的字体设置,这个问题尤为严重。
需要使用资源的标记非常繁琐。实际上,还没有原来不使用资源时简明(直接在元素中定义字体属性)。
可通过定义将所有字体细节捆绑在一起的自定义类(如FontSetting类)来改善第一个问题。然后可作为资源创建FontSetting对象,并在标记中使用它的各种属性。然而,这种方法仍需使用繁琐的标记——并且还需要做一些额外的工作。
样式为解决这个问题提供了非常好的解决方案。可定义独立的用于封装所有希望设置的属性的样式,如下所示:
<Window.Resources> <Style x:Key="BigFontButtonStyle"> <Setter Property="Control.FontFamily" Value="Times New Roman"/> <Setter Property="Control.FontSize" Value="18"/> <Setter Property="Control.FontWeight" Value="Bold" /> Style> Window.Resources>
上面的标记创建了一个独立资源:一个System.Windows.Style对象。这个样式对象包含了一个设置器集合,该集合具有三个Setter对象,每个Setter对象用于一个希望设置的属性。每个Setter对象由两部分信息组成:希望进行设置的属性的名称和希望为该属性应用的值。与所有资源一样,样式对象都有一个键名,从而当需要时可以从集合中提取它。在该例中,键名是BigFontButtonStyle(根据约定,用于样式的键名通常以Style结尾)。
每个WPF元素都可使用一个样式(或者没有样式),样式通过元素的Style属性(该属性是在FrameworkElement基类中定义的)插入到元素中。例如,要使用上面创建的样式配置按钮,需要让按钮指向样式资源,如下所示:
<Button Margin="5" Padding="5" Style="{StaticResource BigFontButtonStyle}">A Customized ButtonButton>
当然,也可通过代码设置样式。需要做的全部工作就是使用大家熟悉的FindResource()方法,从最近的资源集合中提取样式。下面的代码为名为cmd的Button对象设置样式:
cmdButton.Style=(Style)cmd.FindResource("BigFontButtonStyle");
如下图所示,窗口中的两个按钮使用了BigFontButtonStyle样式:
样式系统增加了许多优点。不仅可创建多组明显相关的属性设置,而且使得应用这些设置更加容易,从而精简了标记。最让人满意的是,可应用样式而不用关心设置了哪些属性。在上一个示例中,字体设置被组织到名为BigFontButtonStyle的样式中。如果以后决定大字体按钮还需要更多的内边距和外边距空间,也可为Padding和Margin属性添加设置器。所有使用样式的按钮会自动采用新的样式设置。
Setters集合是Style类中最重要的属性,但并非唯一属性。Style类中共有5个重要属性。下表列出了这些属性。
表 Style类的属性
一、创建样式对象
在上一个示例中,样式对象时在窗口级别定义的,之后再窗口的两个按钮中重用该样式。尽管这是一种常见的设计方式,但并非是唯一的选择。
如果希望创建目标更加精细的样式,可使用容器的Resources集合定义样式,如StackPanel面板或Grid面板。如果希望在应用程序中重用样式,可使用应用程序的Resources集合定义样式。这些也是常用的方法。
严格来将,不需要同事使用样式和资源。例如,可通过直接填充特定按钮的样式集合来定义样式,如下所示:
<Button Margin="5" Padding="5"> <Button.Style> <Style> <Setter Property="Control.FontFamily" Value="Times New Roman"/> <Setter Property="Control.FontSize" Value="20"/> <Setter Property="Control.FontWeight" Value="Bold" /> Style> Button.Style> <Button.Content>A Customized ButtonButton.Content> Button>
上面的代码虽然可凑效,但显然不是很有用,因为现在无法与其他元素共享该样式。
如果只使用样式设置一些属性,就不值得使用这种方法。因为直接设置属性更加容易。然而,如果正在使用样式的其他特性,并且只希望将它应用到单个元素。这一方法有时会有用。例如,可使用该方法为元素关联触发器,还可以通过该方法修改元素控件模板的一部分(对于这种情况,需要使用Setter.TargetName属性,为元素内部的特定组件应用设置器,如列表框中的滚动条按钮)。
二、设置属性
正如已经看到的,每个Style对象都封住了一个Setter对象的集合。每个Setter对象设置元素的单个属性。唯一的限制是设置器只能改变依赖项属性——不能修改其他属性。
在某些情况下,不能使用简单的特新字符串设置属性值。例如,不能使用简单字符串创建ImageBrush对象。对于此类情况,可使用大家熟悉的XAML技巧,用嵌套的元素代替特性。下面是一个示例:
<Style x:Key="HappyTiledElementStyle"> <Setter Property="Control.Background"> <Setter.Value> <ImageBrush TileMode="Tile" ViewportUnits="Absolute" ImageSource="happyface.jpg" Viewport="0 0 32 32" Opacity="0.3"/> Setter.Value> Setter> Style>
为了标识希望设置的属性,需要提供类和属性的名称。然而,使用类名未必是定义属性的类名,也可以是继承了属性的派生类。例如,考虑如下版本的BigFontButtonStyle样式,该样式用Button类的引用替代Control类的引用:
<Style x:Key="BigFontButtonStyle"> <Setter Property="Button.FontFamily" Value="Times New Roman"/> <Setter Property="Button.FontSize" Value="18"/> <Setter Property="Button.FontWeight" Value="Bold" /> Style>
如果将上面的样式进行替换后,将得到相同的结果。那么两者之间到底有什么区别呢?对于这种情况,区别在于WPF对可能包含相同的FontFamily、FontSize以及FontWeight属性,但又不是继承自Button的其他类的处理方式。例如,如果为Label控件使用该版本的BigFontButtonStyle样式,就没有效果。WPF简单地忽略这三个属性,因为不会应用他们。但如果使用原样式,字体属性就会影响就会影响Label控件,因为Label类继承自Control类。
在WPF中还存在这样一些情况,在元素框架层次中的多个位置定义了同一个属性。例如,在Control和TextBlock类中都定义了全部的字体属性(如FontFamily)。如果正在创建应用到TextBlock对象以及继承自Control类的元素的样式,可按如下方式创建标记:
<Style x:Key="BigFontButtonStyle"> <Setter Property="Button.FontFamily" Value="Times New Roman"/> <Setter Property="Button.FontSize" Value="18"/> <Setter Property="Button.FontWeight" Value="Bold" /> <Setter Property="TextBlock.FontFamily" Value="Times New Roman"/> <Setter Property="TextBlock.FontSize" Value="18"/> Style>
然而,这样不会得到期望的结果。问题在于,尽管Button.FontFamily和TextBlock.FontFamily属性是在各自的基类中分别声明,但它们都引用同一个依赖性属性(换句话说,TextBlock.FontSizeProperty和Control.FontSizeProperty引用都指向同一个DependencyProperty对象。)。所以,当使用这个样式时,WPF设置FontFamily和FontSize属性两次。最后应用的设置具有优先权,并同时应用到Button和TextBlock对象。尽管这个问题非常特别,许多属性并不存在该问题,但如果经常创建为不同的元素类型应用不同格式的样式,分析是否存在这一问题就显得很重要了。
还可使用另一种技巧简化样式声明。如果所有属性都准备用于相同的元素类型,就设置Style对象的TargetType属性来指定准备应用属性的类。例如,如果创建只应用于按钮的样式,可按如下方式创建样式:
<Style x:Key="BigFontButtonStyle" TargetType="Button"> <Setter Property="FontFamily" Value="Times New Roman"/> <Setter Property="FontSize" Value="18"/> <Setter Property="FontWeight" Value="Bold" />Style>
这样方法比较方便。正如将在后面分析的,如果不使用样式键名,TargetType属性还可作为自动应用样式的快捷方式。
三、关联事件处理程序
属性设置器是所有样式中最常见的要素,但也可以创建为事件关联特定事件处理程序的EventSetter对象的集合。下面列举的示例为MouseEnter和MouseLeave事件关联事件处理程序:
<Style x:Key="MouseOverHighlightStyle"> <Setter Property="TextBlock.Padding" Value="5"/> <EventSetter Event="FrameworkElement.MouseEnter" Handler="element_MouseEnter" /> <EventSetter Event="FrameworkElement.MouseLeave" Handler="element_MouseLeave" /> Style>
下面的事件处理代码:
private void element_MouseEnter(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = new SolidColorBrush(Colors.LightGoldenrodYellow); } private void element_MouseLeave(object sender, MouseEventArgs e) { ((TextBlock)sender).Background = null; }
MouseEnter和MouseLeave事件使用直接事件路由,这意味着他们不再元素树中冒泡和隧道移动。如果希望为大量元素应用鼠标悬停其上的效果(例如,当鼠标移动到元素上时,希望改变元素的背景色),需要为每个元素添加MouseEnter和MouseLeave事件处理程序。基于样式的事件处理程序简化了这项任务。现在只需要应用单个样式,该样式包含了属性设置器和事件设置器:
<TextBlock Style="{StaticResource MouseOverHighlightStyle}">Hover over me.TextBlock>
下图显示了该技术的一个简单演示程序,该程序中有三个元素,其中两个元素使用了MouseOverHighlightStyle样式。
该示例完整xaml文件:
<Window x:Class="Styles.EventSetter" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="EventSetter" Height="300" Width="300"> <Window.Resources> <Style x:Key="MouseOverHighlightStyle"> <Setter Property="TextBlock.Padding" Value="5"/> <EventSetter Event="FrameworkElement.MouseEnter" Handler="element_MouseEnter" /> <EventSetter Event="FrameworkElement.MouseLeave" Handler="element_MouseLeave" /> Style> Window.Resources> <StackPanel> <TextBlock Style="{StaticResource MouseOverHighlightStyle}">Hover over me.TextBlock> <TextBlock Padding="5">Don't bother with me.TextBlock> <TextBlock Style="{StaticResource MouseOverHighlightStyle}">Hover over me.TextBlock> StackPanel>Window>
EventSetter
WPF极少使用事件设置器这种技术。如果需要使用此处演示的功能,可能更喜欢使用事件触发器,它以声明方式定义了所希望的的行为。事件触发器是专为实现动画而设计的,当创建鼠标悬停效果时它们更有用。
当处理使用冒泡路由策略的事件时,事件设置器并非好的选择。对于这种情况,在高层次的元素上处理希望处理的事件通常更容易。例如,如果希望将工具栏上的所有按钮连接到同一个Click事件处理程序,最好为包含所有按钮的Toolbar元素关联单个事件处理程序。对于这种情况,没必要使用事件设置器。
四、多层样式
尽管可在许多不同层次定义任意数量的样式,但每个WPF元素一次只能使用一个样式对象。乍一看,这像是一种限制,但由于属性值继承和样式继承特性,这种限制实际上并不存在。
例如,假设希望为一组控件使用相同的字体,又不想为每个控件应用相同的样式,对于这种情况,可将它们放置到面板(或其他类型的容器)中,并设置容器的样式。只要设置的属性具有属性值继承特性,这些值就会被传递到子元素。使用这种模型的属性包括IsEnabled、IsVisible、Foreground以及所有字体属性。
对于另外一些情况,可能希望在另一个样式的基础上创建样式。可通过为样式设置BasedOn特性来使用此类样式继承。例如,分析下面的两个样式:
<Window.Resources> <Style x:Key="BigFontButtonStyle"> <Setter Property="Control.FontFamily" Value="Times New Roman" /> <Setter Property="Control.FontSize" Value="18" /> <Setter Property="Control.FontWeight" Value="Bold" /> Style> <Style x:Key="EmphasizedBigFontButtonStyle" BasedOn="{StaticResource BigFontButtonStyle}"> <Setter Property="Control.Foreground" Value="White" /> <Setter Property="Control.Background" Value="DarkBlue" /> Style> Window.Resources>
第一个样式(BigFontButtonStyle)定义了三个字体属性。第二个样式(EmphasizedBigFontButtonStyle)从BigFontButtonStyle样式获取这些属性设置,然后通过另外两个改变前景色和背景色的画刷属性对它们进行了增加。通过使用这种分成两部分的设计方式,可只应用字体设置,也可以应用字体设置和颜色设置的组合。通过这种设计还可创建包含已经定义的字体或颜色细节的更多样式。
下图显示了样式继承在一个简单窗口中的工作情况,该窗口使用了这两个样式:
该示例完整XAML如下:
<Window x:Class="Styles.StyleInheritance" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="StyleInheritance" Height="300" Width="300"> <Window.Resources> <Style x:Key="BigFontButtonStyle"> <Setter Property="Control.FontFamily" Value="Times New Roman" /> <Setter Property="Control.FontSize" Value="18" /> <Setter Property="Control.FontWeight" Value="Bold" /> Style> <Style x:Key="EmphasizedBigFontButtonStyle" BasedOn="{StaticResource BigFontButtonStyle}"> <Setter Property="Control.Foreground" Value="White" /> <Setter Property="Control.Background" Value="DarkBlue" /> Style> Window.Resources> <StackPanel Margin="5"> <Button Padding="5" Margin="5" Style="{StaticResource BigFontButtonStyle}" >Uses BigFontButtonStyleButton> <TextBlock Margin="5">Normal Content.TextBlock> <Button Padding="5" Margin="5" >A Normal ButtonButton> <TextBlock Margin="5">More normal Content.TextBlock> <Button Padding="5" Margin="5" Style="{StaticResource EmphasizedBigFontButtonStyle}" >Uses EmphasizedBigFontButtonStyleButton> StackPanel>Window>
StyleInheritance
五、通过类型自动应用样式
到目前位置,已讨论了如何创建具有名称的样式以及如何在标记中引用它们。但还有一种方法,可以为特定类型的元素自动应用样式。
这一工作非常简单。只需要设置TargetType属性以指定合适的类型(如前所述),并完全忽略键名。这样做时,WPF实际上是使用类型标记扩展来隐式地设置键名,如下所示:
x:Key="{x:Type Button}"
现在,样式已自动应用于整个元素树中的所有按钮上。例如,如果在窗口中采用这种方式定义了一个样式,它会被应用到窗口中的每个按钮上(除非有一个更特殊的样式替换了该样式)。
下面列举一个示例,该例中的窗口自动设置按钮样式。
<Window x:Class="Styles.AutomaticStyles" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="AutomaticStyles" Height="300" Width="300"> <Window.Resources> <Style TargetType="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 ButtonButton> <TextBlock Margin="5">Normal Content.TextBlock> <Button Padding="5" Margin="5" Style="{x:Null}" >A Normal ButtonButton> <TextBlock Margin="5">More normal Content.TextBlock> <Button Padding="5" Margin="5">Another Customized ButtonButton> StackPanel>Window>
在该例中,中间的按钮显示替换了样式。但该按钮并没有为自己提供一个新样式,而将Style属性设置为null值,这样就有效地删除了样式。
尽管自动样式非常方便,但它们会让设计变得复杂。下面列出几条原因:
- 在具有许多样式和多层样式的复杂窗口中,很难跟踪是否通过属性值继承或通过样式设置了某个特定属性(如果是通过样式设置的,那么是通过哪个样式设置的呢?)。因此,如果希望改变某个简单细节,就需要查看整个窗口的全部标记
- 窗口中的格式化操作在开始时通常更一般,但会逐渐变得越来越详细。如果刚开始为窗口应用了自动样式,在许多地方可能需要使用显示的样式覆盖自动样式。这会使整个设计变得复杂。为每个希望设置的格式化特征的组合创建名得样式,并根据名称应用他们会更加直观。
- 在比如,如果为TextBlock元素创建自动样式,那么会同时修改使用TextBlock的其他控件(如模板驱动的ListBox控件)
为避免出现这些问题,最好果断地使用自动样式。如果决定使用自动样式为整个用户界面提供单一、一致的外观,可尝试为特例使用明确的样式。