WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

说明:本系列基本上是《WPF揭秘》的读书笔记。在结构安排与文章内容上参照《WPF揭秘》的编排,对内容进行了总结并加入一些个人理解。

如果你有Web编程的经验,你会知道使用Style属性给Html元素添加样式,并且更好的做法是将这些样式提取到CSS文件中。在WPF/Silverlight中我们也可以把控件的样式提取出来并进行复用,这就是本节讨论的话题 – 样式支持。

所有外观效果相关的特性,如样式、模板或皮肤等的基础是资源的定义与使用,如果对于资源还不是很熟悉,可参考前文部分章节介绍。

样式由System.Windows.Style类支持,简单的说,其将属性归纳为组,从而使复用这一组属性变得简单。

假如,这里有一个TextBlock,我们来看一下如何将样式提取出来。

 <TextBlock Text="Click!" FontFamily="Comic Sans MS" Foreground="MediumBlue" FontSize="20"></TextBlock>

此时这个TextBlock看起来大概是这样(设计时):

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

同数据源的定义,我们也把样式定义于<Resource>标签中。定义一个样式第一步是要指定样式的名称及目标对象的类型,TargetType会限制Style应用到特定类型上。对于上面所示的TextBlock,Style定义如下:

 <Style TargetType="TextBlock" x:Key="TextBlockStyle">

我们可以给一种类型定义多种样式。这样可以给控件不同的实例应用不同的样式。具体样式的定义通过<Setter>标签来完成。其中定义你需要设置的属性及其对应的值(本质上<Setter>是用来给依赖属性设置一个值),下面的代码将TextBlock的Text.FontFamily, Foreground和FontSize属性提取到样式中:

 <Style TargetType="TextBlock" x:Key="TextBlockStyle">
<Setter Property="FontFamily" Value="Comic Sans Ms"></Setter>
<Setter Property="Text" Value="Click!"></Setter>
<Setter Property="Foreground" Value="MediumBlue"></Setter>
<Setter Property="FontSize" Value="20"></Setter>
</Style>

样式定义如上所示,要将样式应用到TextBlock,则是通过TextBlock的Style属性来完成。由于样式定义与<Resource>中,我们需要使用{StaticResource}标记扩展,参考如下代码:

 <TextBlock Style="{StaticResource TextBlockStyle}"></TextBlock>

这样只需要设置Style一个属性,就可以达到最初设置4个属性的效果,我们将这行XAML复制三份(放在一个StackPanel中,不然会叠在一起看不出效果),会得到3个样式完全相同的TextBlock:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

当然如同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>
按钮效果如下:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

样式的作用域

由于样式定义在各级<Resource>中,如果是<Canvas.Resource>,则样式只能在此<Canvas>范围内使用。如需在应用范围内使用一个样式,可以将样式定义在App.xaml中的<Application.Resource>内。一个定义于<Canvas.Resource>或其它低级别元素中的样式(这对所有资源都适用)可以覆盖<Application.Resource>的样式定义。

样式的高级话题

<Style>中的<Setter>只允许设置与可视特性相关的属性,但这其中也包括一些复杂属性,如下面的设置:

 <Setter Property="Button.RenderTransformOrigin" Value="0.5,0.5"/>
<Setter Property="Button.RenderTransform">
<Setter.Value>
<RotateTransform Angle="36" />
</Setter.Value>
</Setter>

提示:通过使用BasedOn属性,一个Style可以从另一个Style继承。下面示例XAML中的Style在名为buttonStyle样式的基础上添加了Button.FontWight的设置。

 <Style x:Key="buttonStyleWithBold" BasedOn="{StaticResource buttonStyle}">
<Setter Property="Button.FontWeight" Value="Bold"/>
</Style>
 

在不同种类元素间共享样式

如我们有这样一个针对Button定义的样式:

 <Style x:Key="btnStyle">
<Setter Property="Button.FontSize" Value="22"/>
<Setter Property="Button.Background" Value="Azure"/>
<Setter Property="Button.Foreground" Value="Black"/>
<Setter Property="Button.Height" Value="50"/>
<Setter Property="Button.Width" Value="50"/>
<Setter Property="Button.RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="Button.RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
</Style>

Button样式如:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

通过将样式中Button.XXX改为Control.XXX我们可以将这个样式应用到其它控件:

 <StackPanel.Resources>
<Style x:Key="controlStyle">
<Setter Property="Control.FontSize" Value="22"/>
<Setter Property="Control.Background" Value="Azure"/>
<Setter Property="Control.Foreground" Value="Black"/>
<Setter Property="Control.Height" Value="50"/>
<Setter Property="Control.Width" Value="50"/>
<Setter Property="Control.RenderTransformOrigin" Value=".5,.5"/>
<Setter Property="Control.RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Resources>

我们来看一下将这个样式分别应用到ComboBox, Expander, TabControl等控件的代码与效果:

 <StackPanel Orientation="Horizontal">
<StackPanel.Resources>…略…</StackPanel.Resources>
<Button Style="{StaticResource controlStyle}">1</Button>
<ComboBox Style="{StaticResource controlStyle}">
<ComboBox.Items>2</ComboBox.Items>
</ComboBox>
<Expander Style="{StaticResource controlStyle}" Content="3"/>
<TabControl Style="{StaticResource controlStyle}">
<TabControl.Items>4</TabControl.Items>
</TabControl>
<ToolBar Style="{StaticResource controlStyle}">
<ToolBar.Items>5</ToolBar.Items>
</ToolBar>
<InkCanvas Style="{StaticResource controlStyle}"/>
<TextBox Style="{StaticResource controlStyle}" Text="7"/>
</StackPanel>

如代码,样式应用方式相同,都是给各控件的Style属性应用一个标记扩展。

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

当给一个元素应用一个样式,如果样式中某个依赖属性在元素中不存在对在的属性,WPF会安全的忽略这个属性,而其他属性会正常设置。这种高级的特性幕后是依赖属性所支持实现的。对于一个控件,其注册了几个专有的依赖属性,同时另一些依赖属性是几个控件共享的。如常见的TextBlock与Button等其他控件共享Foreground属性,InkCanvas与Panel, TextBlock, TextElement及FlowDocument共享Background属性。这样在被共享的样式的<setter>中设置该依赖属性任意一个所有者,这个设置会在所有共享该依赖属性元素上生效。见如下Setter:

 <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中的依赖属性就不需要在指定具体的元素的名称,如:

 <Setter Property="Button.FontSize" Value="22"/>

可以写为

 <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的引用系统会在幕后完成)

 <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中相应的控件按自定义的外观呈现。

 <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是设置触发器最理想的位置,样式直接与元素的可视部分相关,且可以很方便的共享。

这样我们以样式为例,依次详细介绍三个触发器

  1. 属性触发器

当某个依赖属性有一个特定的值(Trigger中设置的值)时,属性触发器会执行一系列Setter设置,并且在属性失去此特定值时把Setter的设置撤销。以如下XAML为例:

 <Style x:Key="buttonStyle" TargetType="{x:Type Button}">
<Style.Triggers>
<Trigger Property="IsMouseOver" Value="True">
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Honeydew"/>
</Trigger>
</Style.Triggers>
<Setter Property="FontSize" Value="22"/>
<Setter Property="Background" Value="Azure"/>
<Setter Property="Foreground" Value="Black"/>
<Setter Property="Height" Value="50"/>
<Setter Property="Width" Value="50"/>
<Setter Property="RenderTransformOrigin" Value="0.5,0.5"/>
</Style>

当鼠标在按钮之外时:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

当鼠标移入按钮范围内后:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

当鼠标离开按钮后,按钮样式恢复。

小提示,触发器的<setter>可以覆盖<style>中同名<setter>的设置。

接着,我们看一个更复杂的应用,前面我们学习过数据绑定,在数据无效时我们需要给用户一个友好的通知,我们只需在Validation.HasError属性上设置一个属性触发器:

 <Style x:Key="textboxStyle" TargetType="{x:Type TextBox}">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="Background" Value="Red" />
<Setter Property="ToolTip"
Value="{Binding RelativeSource={RelativeSource Self},Path=(Validation.Errors)[0].ErrorContent}"
/>
</Trigger>
</Style.Triggers>
</Style>

这段XAML中值得注意的是在数据绑定中使用RelativeSource从而获取任何应用了这个样式的元素的Validation.Errors属性,接着只需将此样式应用在TextBox上即可在验证失败时获得友好的提示。

  1. 数据触发器

对比属性触发器,数据触发器可以由任何.NET属性触发,而不仅限于依赖属性。(但setter中也是只能设置依赖属性,前文我们也提到<setter>就是用来设置依赖属性的。)为了使用.NET属性,需要通过Binding来指定触发相关属性,而不是普通的属性名。另外数据触发器使用<DataTrigger>定义,而不是<Trigger>。下面看一个例子:

 <Style TargetType="{x:Type TextBox}">
<Style.Triggers>
<DataTrigger
Binding="{Binding RelativeSource={RelativeSource Self}, Path=Text}"
Value="disabled">
<Setter Property="IsEnabled" Value="False"/>
</DataTrigger>
</Style.Triggers>
<Setter Property="Background"
Value="{Binding RelativeSource={RelativeSource Self}, Path=Text}"/>
</Style>

上面代码中在指定数据触发器的触发属性时,我们再次使用了RelativeSource。另外样式中那个设置Background的<setter>是在StringToBrush类型转换器支持下实现的,当这个setter的值无法被转换为相应的Brush时,该TextBox会恢复默认颜色,这是WPF默认的数据绑定错误处理方式。

下列TextBox应用了上述类型化样式:

 <TextBox Margin="3" Text="Azure"/>
<TextBox Margin="3" Text="Green"/>
<TextBox Margin="3" Text="Orange"/>
<TextBox Margin="3" Text="Not a Color"/>
<TextBox Margin="3" Text="Disabled"/>

我们可以看到样式及其中触发器带来的效果:

WPF,Silverlight与XAML读书笔记第四十四 - 外观效果之样式

触发器的组合使用

我们可以通过如下的方式组合使用触发器:

  • 将多个触发器应用到相同的元素上,实现逻辑或的效果。
  • 将多个属性借助一个触发器来判断,实现逻辑与的效果。

逻辑或

下面的例子中,我们在<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为例):

 <Style.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver" Value="True"/>
<Condition Property="IsFocused" Value="True" />
</MultiTrigger.Conditions>
<Setter Property="RenderTransform">
<Setter.Value>
<RotateTransform Angle="10"/>
</Setter.Value>
</Setter>
<Setter Property="Foreground" Value="Black"/>
</MultiTrigger>
</Style.Triggers>

当<conditions>中两个条件都满足时,将会应用<setter>中的效果,另外MultiDataTrigger在支持普通.NET属性的同时也支持MultiTrigger支持的依赖属性触发条件。

前文提到的通过IsMouseEnter属性作为触发条件的触发器,也可以通过EventSetter以事件驱动的方式来实现,如这段XAML:

 <Style x:Key="btnStyle" TargetType="{x:Type Button}">
<Setter Property="FontSize" Value="22"/>
<EventSetter Event="MouseEnter" Handler="Button_MouseEnter" />
</Style>

这需要一个程序代码实现事件处理函数。

本文完

参考:

《WPF揭秘》

上一篇:JavaScript对象(窗口对象 定时器对象 )


下一篇:luogu P2860 [USACO06JAN]冗余路径Redundant Paths