说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。
如果你有Web编程的经验,你会知道使用Style属性给Html元素添加样式,并且更好的做法是将这些样式提取到CSS文件中。在WPF/Silverlight中我们也可以把控件的样式提取出来并进行复用,这就是本节讨论的话题 – 样式支持。
所有外观效果相关的特性,如样式、模板或皮肤等的基础是资源的定义与使用,如果对于资源还不是很熟悉,可参考前文部分章节介绍。
样式由System.Windows.Style类支持,简单的说,其将属性归纳为组,从而使复用这一组属性变得简单。
假如,这里有一个TextBlock,我们来看一下如何将样式提取出来。
1 <TextBlock Text="Click!" FontFamily="Comic Sans MS" Foreground="MediumBlue" FontSize="20"></TextBlock>
此时这个TextBlock看起来大概是这样(设计时):
同数据源的定义,我们也把样式定义于<Resource>标签中。定义一个样式第一步是要指定样式的名称及目标对象的类型,TargetType会限制Style应用到特定类型上。对于上面所示的TextBlock,Style定义如下:
1 <Style TargetType="TextBlock" x:Key="TextBlockStyle">
我们可以给一种类型定义多种样式。这样可以给控件不同的实例应用不同的样式。具体样式的定义通过<Setter>标签来完成。其中定义你需要设置的属性及其对应的值(本质上<Setter>是用来给依赖属性设置一个值),下面的代码将TextBlock的Text.FontFamily, Foreground和FontSize属性提取到样式中:
1 <Style TargetType="TextBlock" x:Key="TextBlockStyle"> 2 <Setter Property="FontFamily" Value="Comic Sans Ms"></Setter> 3 <Setter Property="Text" Value="Click!"></Setter> 4 <Setter Property="Foreground" Value="MediumBlue"></Setter> 5 <Setter Property="FontSize" Value="20"></Setter> 6 </Style>
样式定义如上所示,要将样式应用到TextBlock,则是通过TextBlock的Style属性来完成。由于样式定义与<Resource>中,我们需要使用{StaticResource}标记扩展,参考如下代码:
1 <TextBlock Style="{StaticResource TextBlockStyle}"></TextBlock>
这样只需要设置Style一个属性,就可以达到最初设置4个属性的效果,我们将这行XAML复制三份(放在一个StackPanel中,不然会叠在一起看不出效果),会得到3个样式完全相同的TextBlock:
当然如同Web编程中,我们可以在控件上直接使用属性覆盖Style中的设置。本地值比任何Style中的设置优先级高,这也符合依赖属性一文中描述的依赖属性提供程序优先级的说明。
另外,即使TextBlock位于其他内容控件的内部,也不影响使用Style给它设置样式。甚至后文介绍的模板中的控件,也可以引用Resource中定义的样式。下面的代码展示了我们把刚才定义的样式应用到一个按钮中的TextBlock上:
1 <Button x:Name="btn" Width="60" Height="80">
2 <Button.Content>
3 <StackPanel>
4 <Image Source="icon.jpg"/>
5 <TextBlock Text="Click!" Style="TextBlockStyle"/>
6 </StackPanel>
7 </Button.Content>
8 </Button>
样式的作用域
由于样式定义在各级<Resource>中,如果是<Canvas.Resource>,则样式只能在此<Canvas>范围内使用。如需在应用范围内使用一个样式,可以将样式定义在App.xaml中的<Application.Resource>内。一个定义于<Canvas.Resource>或其它低级别元素中的样式(这对所有资源都适用)可以覆盖<Application.Resource>的样式定义。
样式的高级话题
<Style>中的<Setter>只允许设置与可视特性相关的属性,但这其中也包括一些复杂属性,如下面的设置:
1 <Setter Property="Button.RenderTransformOrigin" Value="0.5,0.5"/> 2 <Setter Property="Button.RenderTransform"> 3 <Setter.Value> 4 <RotateTransform Angle="36" /> 5 </Setter.Value> 6 </Setter>
提示:通过使用BasedOn属性,一个Style可以从另一个Style继承。下面示例XAML中的Style在名为buttonStyle样式的基础上添加了Button.FontWight的设置。
1 <Style x:Key="buttonStyleWithBold" BasedOn="{StaticResource buttonStyle}"> 2 <Setter Property="Button.FontWeight" Value="Bold"/> 3 </Style>
在不同种类元素间共享样式
如我们有这样一个针对Button定义的样式:
1 <Style x:Key="btnStyle"> 2 <Setter Property="Button.FontSize" Value="22"/> 3 <Setter Property="Button.Background" Value="Azure"/> 4 <Setter Property="Button.Foreground" Value="Black"/> 5 <Setter Property="Button.Height" Value="50"/> 6 <Setter Property="Button.Width" Value="50"/> 7 <Setter Property="Button.RenderTransformOrigin" Value=".5,.5"/> 8 <Setter Property="Button.RenderTransform"> 9 <Setter.Value> 10 <RotateTransform Angle="10"/> 11 </Setter.Value> 12 </Setter> 13 </Style>
Button样式如:
通过将样式中Button.XXX改为Control.XXX我们可以将这个样式应用到其它控件:
1 <StackPanel.Resources> 2 <Style x:Key="controlStyle"> 3 <Setter Property="Control.FontSize" Value="22"/> 4 <Setter Property="Control.Background" Value="Azure"/> 5 <Setter Property="Control.Foreground" Value="Black"/> 6 <Setter Property="Control.Height" Value="50"/> 7 <Setter Property="Control.Width" Value="50"/> 8 <Setter Property="Control.RenderTransformOrigin" Value=".5,.5"/> 9 <Setter Property="Control.RenderTransform"> 10 <Setter.Value> 11 <RotateTransform Angle="10"/> 12 </Setter.Value> 13 </Setter> 14 </Style> 15 </StackPanel.Resources>
我们来看一下将这个样式分别应用到ComboBox, Expander, TabControl等控件的代码与效果:
1 <StackPanel Orientation="Horizontal"> 2 <StackPanel.Resources>…略…</StackPanel.Resources> 3 <Button Style="{StaticResource controlStyle}">1</Button> 4 <ComboBox Style="{StaticResource controlStyle}"> 5 <ComboBox.Items>2</ComboBox.Items> 6 </ComboBox> 7 <Expander Style="{StaticResource controlStyle}" Content="3"/> 8 <TabControl Style="{StaticResource controlStyle}"> 9 <TabControl.Items>4</TabControl.Items> 10 </TabControl> 11 <ToolBar Style="{StaticResource controlStyle}"> 12 <ToolBar.Items>5</ToolBar.Items> 13 </ToolBar> 14 <InkCanvas Style="{StaticResource controlStyle}"/> 15 <TextBox Style="{StaticResource controlStyle}" Text="7"/> 16 </StackPanel>
如代码,样式应用方式相同,都是给各控件的Style属性应用一个标记扩展。
当给一个元素应用一个样式,如果样式中某个依赖属性在元素中不存在对在的属性,WPF会安全的忽略这个属性,而其他属性会正常设置。这种高级的特性幕后是依赖属性所支持实现的。对于一个控件,其注册了几个专有的依赖属性,同时另一些依赖属性是几个控件共享的。如常见的TextBlock与Button等其他控件共享Foreground属性,InkCanvas与Panel, TextBlock, TextElement及FlowDocument共享Background属性。这样在被共享的样式的<setter>中设置该依赖属性任意一个所有者,这个设置会在所有共享该依赖属性元素上生效。见如下Setter:
1 <Setter Property="TextBlock.Foreground" Value="Black"/>
如果我们把这个样式应用到Button上,这个setter也可以设置Button的Foreground。(见上文对依赖属性共享的举例)
所以如果只是在TextBlock与Button之间共享Foreground属性(或其它这两者间共享的依赖属性),则可以不把Button.XXX改为Control.XXX,而是直接使用。
针对上述这些复杂的情况,最好的做法是针对不同的控件定义不同的样式。
提示:
Style自己也提供一个Resources属性,当需要将Style中某个依赖属性的值设置为很复杂的值时,可以将其作为资源定义在<Style.Resources>中。这样可以避免必须将其定义在其他元素的Resources中以致可能出现的资源引用问题。
前文我们讲过TargetType的作用,如果尝试把一个Style应用到一个非TargetType类型的控件上会导致一个编译错误。如果TargetType被指定为{x:Type Control},则这个样式可以被应用到任意控件上,当然Style元素指定的依赖属性是否可以应用到目标元素的规则上文有介绍;当给TargetType显示设置了具体类型后,Setter中的依赖属性就不需要在指定具体的元素的名称,如:
1 <Setter Property="Button.FontSize" Value="22"/>
可以写为
1 <Setter Property="FontSize" Value="22"/>
类型化Style
如果在创建Style时不指定key属性,则将创建一个隐式的Style,其将被作用到所有目标类型的元素上。相对于之前介绍的命名样式,这种隐式Style常被称作类型化样式。
类型化样式的有效范围是由Style所在的<Resources>定义范围决定的,如一个类型化样式被添加在<Application.Resources>中,则它将被应用到整个应用程序中所有目标类型的对象。然而所有目标元素都可以通过命名Style来覆盖类型化样式。(前文讲到的显示设置属性覆盖类型化Style同样有效,甚至可以通过将元素Style设为null来恢复默认样式)
注意:
类型化Style的TargetType完全匹配要应用样式的类型。这表示TargetType的子类不会继承类型化Style。如一个Style的TargetType为ToggleButton,这个类型化样式不会应用给CheckBox等ToggleButton的子类。
在介绍资源时我们提过,<Resources>标签中定义的元素被作为ResourceDictionary的一员。而类型化样式中,我们没有显式设置这个字典对象的key,WPF隐式使用TargetType的值(Type类型,非字符串)作为这个资源的key对象。通过下面的语句可以显示访问类型化Style(这只是为了演示,默认情况下对类型化Style的引用系统会在幕后完成)
1 <Button Style="{StaticResource {x:Type Button}}">按钮</Button>
在同一个<Resouces>元素内,对于一个TargetType只能有一个无key的Style,否则按我们上文分析在一个字典中将会出现相同键的对象,当然这是错误的。
提示:
对于FrameworkElement/FrameworkContentElement除了有一个Style属性外,还提供了一个FocusVisualStyle。FocusVisualStyle的Style是元素获得键盘焦点时展示的外观(该属性设置方式与Style一致)。另外对于其他一些控件,还有独有的设置。如ItemsControl提供ItemContainerStyle属性,其中的样式作用于ListBoxItem或ComboxItem等容器的项上,而像ToolBar则提供了ResourceKey属性,其中包含ButtonStyleKey与TextBoxStyleKey等xxxStyleKey属性。这些属性都是只读的,无法直接设置。但我们可以通过重写同key的样式来覆盖默认设置,从而使ToolBar中相应的控件按自定义的外观呈现。
1 <Style x:Key="{x:Static ToolBar.ButtonStyleKey}" TargetType="{x:Type Button}" />
触发器
触发器在前面章节有提及,这里将详细介绍。类似<Style>触发器<Trigger>也使用<Setter>来定义。一个样式是无条件应用其中的设置,而触发器则会根据一个或多个条件来执行。在前面章节我们曾提到WPF提供的三种类型的触发器。
-
属性触发器 – 当依赖属性的值改变时调用。
-
数据触发器 – 当普通.NET属性的值改变时调用。
-
事件触发器 – 当路由事件被触发时调用
FrameworkElement,Style,DataTemplate和ControlTemplate通过Triggers集合属性提供对触发器的支持,这其中(对于1.0版本的WPF)Style,DataTemplate和ControlTemplate支持全部3种触发器,而FrameworkElement仅支持事件触发器。对于1.0版本,这影响也不大,因为Style是设置触发器最理想的位置,样式直接与元素的可视部分相关,且可以很方便的共享。
这样我们以样式为例,依次详细介绍三个触发器
-
属性触发器
当某个依赖属性有一个特定的值(Trigger中设置的值)时,属性触发器会执行一系列Setter设置,并且在属性失去此特定值时把Setter的设置撤销。以如下XAML为例:
1 <Style x:Key="buttonStyle" TargetType="{x:Type Button}"> 2 <Style.Triggers> 3 <Trigger Property="IsMouseOver" Value="True"> 4 <Setter Property="RenderTransform"> 5 <Setter.Value> 6 <RotateTransform Angle="10"/> 7 </Setter.Value> 8 </Setter> 9 <Setter Property="Foreground" Value="Honeydew"/> 10 </Trigger> 11 </Style.Triggers> 12 <Setter Property="FontSize" Value="22"/> 13 <Setter Property="Background" Value="Azure"/> 14 <Setter Property="Foreground" Value="Black"/> 15 <Setter Property="Height" Value="50"/> 16 <Setter Property="Width" Value="50"/> 17 <Setter Property="RenderTransformOrigin" Value="0.5,0.5"/> 18 </Style>
当鼠标在按钮之外时:
当鼠标移入按钮范围内后:
当鼠标离开按钮后,按钮样式恢复。
小提示,触发器的<setter>可以覆盖<style>中同名<setter>的设置。
接着,我们看一个更复杂的应用,前面我们学习过数据绑定,在数据无效时我们需要给用户一个友好的通知,我们只需在Validation.HasError属性上设置一个属性触发器:
1 <Style x:Key="textboxStyle" TargetType="{x:Type TextBox}"> 2 <Style.Triggers> 3 <Trigger Property="Validation.HasError" Value="True"> 4 <Setter Property="Background" Value="Red" /> 5 <Setter Property="ToolTip" 6 Value="{Binding RelativeSource={RelativeSource Self},Path=(Validation.Errors)[0].ErrorContent}" 7 /> 8 </Trigger> 9 </Style.Triggers> 10 </Style>
这段XAML中值得注意的是在数据绑定中使用RelativeSource从而获取任何应用了这个样式的元素的Validation.Errors属性,接着只需将此样式应用在TextBox上即可在验证失败时获得友好的提示。
-
数据触发器
对比属性触发器,数据触发器可以由任何.NET属性触发,而不仅限于依赖属性。(但setter中也是只能设置依赖属性,前文我们也提到<setter>就是用来设置依赖属性的。)为了使用.NET属性,需要通过Binding来指定触发相关属性,而不是普通的属性名。另外数据触发器使用<DataTrigger>定义,而不是<Trigger>。下面看一个例子:
1 <Style TargetType="{x:Type TextBox}"> 2 <Style.Triggers> 3 <DataTrigger 4 Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text}" 5 Value="disabled"> 6 <Setter Property="IsEnabled" Value="False"/> 7 </DataTrigger> 8 </Style.Triggers> 9 <Setter Property="Background" 10 Value="{Binding RelativeSource={RelativeSource Self}, Path=Text}"/> 11 </Style>
上面代码中在指定数据触发器的触发属性时,我们再次使用了RelativeSource。另外样式中那个设置Background的<setter>是在StringToBrush类型转换器支持下实现的,当这个setter的值无法被转换为相应的Brush时,该TextBox会恢复默认颜色,这是WPF默认的数据绑定错误处理方式。
下列TextBox应用了上述类型化样式:
1 <TextBox Margin="3" Text="Azure"/> 2 <TextBox Margin="3" Text="Green"/> 3 <TextBox Margin="3" Text="Orange"/> 4 <TextBox Margin="3" Text="Not a Color"/> 5 <TextBox Margin="3" Text="Disabled"/>
我们可以看到样式及其中触发器带来的效果:
触发器的组合使用
我们可以通过如下的方式组合使用触发器:
-
将多个触发器应用到相同的元素上,实现逻辑或的效果。
-
将多个属性借助一个触发器来判断,实现逻辑与的效果。
逻辑或
下面的例子中,我们在<Style.Triggers>集合中添加了两个触发器,两个触发器中的Setter相同,这样当至少有一个触发器满足条件时,触发器中Setter就可生效。
1 <Style.Triggers>
2 <Trigger Property="IsMouseOver" Value="True">
3 <Setter Property="RenderTransform">
4 <Setter.Value>
5 <RotateTransform Angle="10"/>
6 </Setter.Value>
7 </Setter>
8 <Setter Property="Foreground" Value="Black"/>
9 </Trigger>
10 <Trigger Property="IsFocused" Value="True">
11 <Setter Property="RenderTransform">
12 <Setter.Value>
13 <RotateTransform Angle="10"/>
14 </Setter.Value>
15 </Setter>
16 <Setter Property="Foreground" Value="Black"/>
17 </Trigger>
18 </Style.Triggers>
提示:在单个或多个触发器(多个触发器同时处于激活状态)中如果有多个针对同一属性而值不同的setter – 即Setter冲突,这时最后一个setter会生效。
逻辑与
通过使用MultiTrigger(针对属性触发器)或MultiDataTrigger(针对数据触发器),可以实现逻辑与,这两个特殊的Trigger都提供一个Conditions集合属性,用于设置多个触发条件,参考代码(MultiTrigger为例):
1 <Style.Triggers> 2 <MultiTrigger> 3 <MultiTrigger.Conditions> 4 <Condition Property="IsMouseOver" Value="True"/> 5 <Condition Property="IsFocused" Value="True" /> 6 </MultiTrigger.Conditions> 7 <Setter Property="RenderTransform"> 8 <Setter.Value> 9 <RotateTransform Angle="10"/> 10 </Setter.Value> 11 </Setter> 12 <Setter Property="Foreground" Value="Black"/> 13 </MultiTrigger> 14 </Style.Triggers>
当<conditions>中两个条件都满足时,将会应用<setter>中的效果,另外MultiDataTrigger在支持普通.NET属性的同时也支持MultiTrigger支持的依赖属性触发条件。
前文提到的通过IsMouseEnter属性作为触发条件的触发器,也可以通过EventSetter以事件驱动的方式来实现,如这段XAML:
1 <Style x:Key="btnStyle" TargetType="{x:Type Button}"> 2 <Setter Property="FontSize" Value="22"/> 3 <EventSetter Event="MouseEnter" Handler="Button_MouseEnter" /> 4 </Style>这需要一个程序代码实现事件处理函数。
本文完
参考:
《WPF揭秘》