样式(style
)时组织和重用格式化选项的重要工具。不是使用重复的标记填充XAML,以便设置外边距、内边距、颜色以及字体等细节,而是创建一系列封装所有这些细节的样式,然后在需要之处通过属性来应用样式
行为(behavior
)时一款重用用户界面代码的更有挑战性的工具。其基本思想时:使用行为封装一些通用的UI功能(例如,使元素可被拖动的代码)。如果具有适当的行为,可使用一两行XAML标记将其附加到任意元素,从而节省编写和调试代码的工作。
样式基础
样式是可应用于元素的属性值集合。WPF 样式系统与 HTML标记中的层叠样式表(CascadingStyle Sheet,CSS)标准担当类似的角色。与 CSS 类似,通过 WPF 样式可定义通用的格式化特性集合,并且为了保证一致性,在整个应用程序中应用它们。与CSS样,WPF样式也能够自动工作,指定具体的元素类型为目标,并通过元素树层叠起来。然而,WPF样式的功能更加强大,因为它们能够设置任何依赖项属性。这意味着可以使用它们标准化未格式化的特性,如控件的行为。WPF样式也支持触发器(trigger
),当属性发生变化时,可通过触发器改变控件的样式,并且可使用模板重新定义控件的内置外观。F且学习了如何使用样式,就可以在所有WPF应用程序中使用它们。
在使用资源设置属性时,正确地匹配数据类型是非常重要的。这时,WPF使用类型转换器的方式和直接设置特性值是不同的。例如,如果正为元素设置 FontFamily特性,可使用字符串Times New Roman,因为 FontFamilyConverter 转换器会创建所需要的 FontFamily 对象。但如果试图使用字符串资源设置 FontFamily属性,情况就不同了–这时,XAML解析器会抛出异常
1 2 3 4 5 6 7 8 9 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> </Style >
1 2 <Button x:Name ="btn" Style ="{StaticResource DefaultButtonStyle}" Content ="Button" > </Button >
1 btn.Style = (Style)FindResource("DefaultButtonStyle" );
Setters
集合是Style
类中最重要的属性,但并非唯一属性。Style
类中更有5可重要属性:
Style类的属性
属性
说明
Setters
设置属性值以及自动关联事件处理程序的Setter
对象或EventSetter
对象的集合
Triggers
继承自TriggerBase
类并能自动改变样式设置的对象集合。例如,当另一个属性改变时,或者当发生了某个事件时,可以修改样式
Resources
希望用于样式的资源集合。例如,可能需要使用一个对象设置多个属性。这时,更高效的做法是作为资源创建对象,然后在 Setter
对象中使用该资源(而不是使用嵌套的标签作为每个 Seter
对象的一部分创建对象)
BaseOn
通过该属性可创建继承自(并且可以有选择地进行重写)其他样式设置的更具体样式
TargetType
该属性标识应用样式的元素的类型。通过该属性可创建只影响特定类型元素的设置器,还可以创建能够为恰当的元素类型自动起作用的设置器
创建样式对象
可通过直接填充特定元素的样式集合来定义样式,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <Button Padding ="5" Margin ="5" > <Button.Style > <Style TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="18" /> <Setter Property ="FontWeight" Value ="Bold" /> </Style > </Button.Style > <Button.Content > A Customized Button</Button.Content > </Button >
上面的代码虽然可奏效,但显然不是很有用,因为现在无法与其他元素共享该样式
如果只使用样式设置一些属性(如本例所示),就不值得使用这种方法,因为直接设置属性更加容易。然而,如果正在使用样式的其他特性,并且只希望将它应用到单个元素,这一方法有时会有用。例如,可使用该方法为元素关联触发器,还可以通过该方法修改元素控件模板的一部分(对于这种情况,需要使用 Setter.TargetName
属性,为元素内部的特定组件应用设置器,如列表框中的滚动条按钮)
设置属性
正如已经看到的,每个 Style对象都封装了一个 Setter 对象的集合。每个 Setter 对象设置元素的单个属性。唯一的限制是设置器只能改变依赖项属性——不能修改其他属性 。
在某些情况下,不能使用简单的特性字符串设置属性值。例如,不能使用简单的字符串创建ImageBrush
对象。对于此类情况,可使用XAML
技巧,用于嵌套的元素代替特性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> <Setter Property ="Background" > <Setter.Value > <ImageBrush TileMode ="Tile" Viewport ="0 0 32 32" ViewportUnits ="Absolute" ImageSource ="/Images/happyface.png" Opacity ="0.3" /> </Setter.Value > </Setter > </Style >
关联事件处理程序
属性设置器是所有样式中最常见的要素,但也可以创建为事件关联特定事件处理程序的EventSetter 对象的集合。下面列举的示例为 MouseEnter 和 MouseLeave 事件关联事件处理程序:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <Window ... > <Window.Resources > <Style x:Key ="MouseOverHighlightStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontSize" Value ="30" /> <EventSetter Event ="Button.MouseEnter" Handler ="element_MouseEnter" /> <EventSetter Event ="Button.MouseLeave" Handler ="element_MouseLeave" /> </Style > </Window.Resources > <StackPanel > <Button Style ="{StaticResource MouseOverHighlightStyle}" Padding ="5" Margin ="5" Content ="A Coustinze Button" /> </StackPanel > </Window >
1 2 3 4 5 6 7 8 9 private void element_MouseEnter (object sender, MouseEventArgs e ){ ((Button)sender).Background = new SolidColorBrush(Colors.LightGoldenrodYellow); } private void element_MouseLeave (object sender, MouseEventArgs e ){ ((Button)sender).Background = null ; }
WPF 极少使用事件设置器这种技术。如果需要使用此处演示的功能,您可能更喜欢使用事件触发器,它以声明方式定义了所希望的行为(并且不需要任何代码)。事件触发器是专为实现动画而设计的,当创建鼠标悬停效果时它们更有用。
当处理使用冒泡路由策略的事件时,事件设置器并非好的选择。对于这种情况,在高层次的元素上处理希望处理的事件通常更容易。例如,如果希望将工具栏上的所有按钮连接到同一个Click事件处理程序,最好为包含所有按钮的Toolbar 元素关联单个事件处理程序。对于这种情况,没必要使用事件设置器。
多层样式
尽管可在许多不同层次定义任意数量的样式,但每个 WPF 元素一次只能使用一个样式对象。乍一看,这像是一种限制,但由于属性值继承和样式继承特性,这种限制实际上并不存在。
对于另外一些情况,可能希望在另一个样式的基础上创建样式。可通过为样式设置 BasedOn特性来使用此类样式继承。例如,分析下面两个样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <Window ... > <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 > <Button Style ="{StaticResource EmphasizedBigFontButtonStyle}" Padding ="5" Margin ="5" Content ="A Coustinze Button" /> </StackPanel > </Window >
可使用 Basedon
属性创建一条完整的样式继承链。唯一的规则是,如果两次设置了同一个属性,最后的属性设置器(在继承链中最远的派生类中的设置器)会覆盖其他以前的定义。
通过类型自动应用样式
特定类型的元素自动应用样式,只需要设置 TargetType
属性以指定合适的类型(如前所述),并完全忽略键名。这样做时,WPF实际上是使用类型标记扩展来隐式地设置键名,如下所示:
1 2 <Style TargetType ="{x:Type Button}" > </Style >
现在,样式已自动应用于整个元素树中的所有按钮上。例如,如果在窗口中采用这种方式定义了一个样式,它会被应用到窗口中的每个按钮上(除非有一个更特殊的样式替换了该样式)。
1 <Button Style ="{x:Null}" />
将 Style
属性设置为 NULL
值,这样就有效地删除自动样式。
触发器
WPF 中有个主题,就是以声明方式扩展代码的功能。当使用样式、资源或数据绑定时,将发现即使不使用代码,也能完成不少工作。
触发器是另一个实现这种功能的例子。使用触发器,可自动完成简单的样式改变,而这通常需要使用样板事件处理逻辑。例如,当属性发生变化时可以进行响应,并自动调整样式。
触发器通过 Style.Triggers
集合链接到样式。每个样式都可以有任意多个触发器,而且每个触发器都是 System.Windows.TriggerBase
的派生类的实例。
继承自TriggerBase的类
名称
说明
Trigger
这是一种最简单的触发器。可以监测依赖项属性的变化,然后使用设置器改变样式
MultiTrigger
与Trigger类似,但这种触发器联合了多个条件。只有满足了所有这些条件,才会启动触发器
DataTrigger
这种触发器使用数据绑定。与Trigger类似,只不过监视的时任意绑定数据的变化
MultiDataTrigger
联合多个数据触发器
EventTrigger
这是最复杂的触发器。当事件发生时,这种触发器应用动画
通过使用 FrameworkElement.Triggers
集合,可直接为元素应用触发器,而不需要创建样式但这存在一个相当大的缺陷。这个 Triggers
集合只支持事件触发器(并非技术上的原因造成了该限制,只是因为 WPF 团队没时间实现该特性,将来的版本中可能包含该特性)。
简单触发器
可为任何依赖项属性关联简单触发器。例如,可通过响应Control类的IsFocused、IsMouseOver 以及 IsPressed属性的变化,创建鼠标悬停效果和焦点效果。
每个简单触发器都指定了正在监视的属性,以及正在等待的属性值。当该属性值出现时,将应用存储在 Trigger.Setters 集合中的设置器(但不能使用更复杂的触发器逻辑。例如,比较某个值以查看其是否处于某个范围,或执行某种计算等。对于这些情况,最好使用事件处理程序)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> <Style.Triggers > <Trigger Property ="IsMouseOver" Value ="True" > <Setter Property ="Control.Foreground" Value ="DarkRed" /> </Trigger > </Style.Triggers > </Style >
触发器的有点时不需要为翻转它们而编写任何逻辑。只要停止应用触发器,元素就会恢复到正常外观。
触发器时众多覆盖从依赖项属性返回的值的属性提供者之一。但原始的属性值(不管是在本地设置的还是通过样式设置的)仍会保留。只要触发器被禁用,触发器之前的属性值就会再次可用
可创建一次应用于同一元素的多个触发器。如果这些触发器设置不同的属性,这种情况就不会出现混乱。然而,如果多个触发器修改同一属性,那么最后的触发器将有效。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> <Style.Triggers > <Trigger Property ="IsFocused" Value ="True" > <Setter Property ="Control.Foreground" Value ="DarkRed" /> </Trigger > <Trigger Property ="IsMouseOver" Value ="True" > <Setter Property ="Control.Foreground" Value ="LightYellow" /> </Trigger > <Trigger Property ="IsPressed" Value ="True" > <Setter Property ="Control.Foreground" Value ="Red" /> </Trigger > </Style.Triggers > </Style >
如果希望创建只有当几个条件都为真时才激活的触发器,可使用MultiTrigger
。这种触发器提供了一个Conditions
集合,可通过该集合定义一些列属性和值的组合。在下面的示例中,只有当按钮具有焦点而且鼠标悬停在该按钮上时,才会应用格式化信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> <Style.Triggers > <MultiTrigger > <MultiTrigger.Conditions > <Condition Property ="IsFocused" Value ="True" /> <Condition Property ="IsMouseOver" Value ="True" /> </MultiTrigger.Conditions > <MultiTrigger.Setters > <Setter Property ="Foreground" Value ="DarkRed" /> </MultiTrigger.Setters > </MultiTrigger > </Style.Triggers > </Style >
对于MultiTrigger
不必关心声明条件的顺序,因为这些条件都必须保持为真
事件触发器
普通触发器等待属性发生变化,而事件触发器等待特定的事件被引发。您可能会认为此时应使用设置器来改变元素,但情况并非如此。相反,事件触发器要求用户提供一系列修改控件的动作。这些动作通常被用于动画。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <Style x:Key ="DefaultButtonStyle" TargetType ="{x:Type Button}" > <Setter Property ="FontFamily" Value ="Times New Roman" /> <Setter Property ="FontSize" Value ="20" /> <Setter Property ="FontWeight" Value ="Bold" /> <Style.Triggers > <EventTrigger RoutedEvent ="MouseEnter" > <EventTrigger.Actions > <BeginStoryboard > <Storyboard > <DoubleAnimation Duration ="0:0:0.2" Storyboard.TargetProperty ="FontSize" To ="30" /> </Storyboard > </BeginStoryboard > </EventTrigger.Actions > </EventTrigger > <EventTrigger RoutedEvent ="MouseLeave" > <EventTrigger.Actions > <BeginStoryboard > <Storyboard > <DoubleAnimation Duration ="0:0:0.2" Storyboard.TargetProperty ="FontSize" To ="20" /> </Storyboard > </BeginStoryboard > </EventTrigger.Actions > </EventTrigger > </Style.Triggers > </Style >
在 XAML 中,必须在故事板中定义每个动画,故事板为动画提供了时间线。用户可以在故事板内部定义希望使用的一个或多个动画对象。每个动画对象执行本质上相同的任务:在一定时期内修改依赖项属性
。
与属性触发器不同,如果希望元素返回到原始状态,需要反转事件触发器(这是因为默认的动画行为时一旦动画完成就继续处于激活状态,从而保持最后的属性值)
行为
样式提供了重用一组属性设置的实用方法。它们为帮助构建一致的、组织良好的界面迈出了重要的第一步——但是它们还有许多限制。
创建行为
.Net Framework
:添加对System.Windows.Interactivity.dll
的引用
.Net Core
:安装Microsoft.Xaml.Behaviors.Wpf
Nuget包
行为旨在封装一些U功能,从而可以不必编写代码就能够将其应用到元素上。从另一个角度看,每个行为都为元素提供了一个服务。该服务通常涉及监听几个不同的事件并执行几个相关的操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 public class DragInCanvasBehavior : Behavior <UIElement >{ private Canvas? _canvas; private bool _isDragging = false ; private Point _mouseOffset; protected override void OnAttached () { AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown; AssociatedObject.MouseMove += AssociatedObject_MouseMove; AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp; } protected override void OnDetaching () { AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown; AssociatedObject.MouseMove -= AssociatedObject_MouseMove; AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp; } private void AssociatedObject_MouseLeftButtonDown (object sender, System.Windows.Input.MouseButtonEventArgs e ) { if (_canvas == null ) _canvas = (Canvas)VisualTreeHelper.GetParent(AssociatedObject); _isDragging = true ; _mouseOffset = e.GetPosition(AssociatedObject); AssociatedObject.CaptureMouse(); } private void AssociatedObject_MouseMove (object sender, System.Windows.Input.MouseEventArgs e ) { if (_isDragging) { Point point = e.GetPosition(_canvas); AssociatedObject.SetValue(Canvas.TopProperty, point.Y - _mouseOffset.Y); AssociatedObject.SetValue(Canvas.LeftProperty, point.X - _mouseOffset.X); } } private void AssociatedObject_MouseLeftButtonUp (object sender, System.Windows.Input.MouseButtonEventArgs e ) { if (_isDragging) { AssociatedObject.ReleaseMouseCapture(); _isDragging = false ; } } }
使用行为
.Net Framework
:添加对System.Windows.Interactivity.dll
的引用
.Net Core
:安装Microsoft.Xaml.Behaviors.Wpf
Nuget包
下面的标记创建一个具有三个图形的Canvas面板,两个Ellipse元素使用了DragInCanvasBehavior
,并能在Canvas面板中拖动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 <Window x:Class ="WpfApp.MainWindow" xmlns ="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x ="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d ="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local ="clr-namespace:WpfApp" xmlns:res ="clr-namespace:ResourceLibrary;assembly=ResourceLibrary" xmlns:i ="http://schemas.microsoft.com/xaml/behaviors" xmlns:custom ="clr-namespace:BehaviorsLibrary;assembly=BehaviorsLibrary" mc:Ignorable ="d" Title ="MainWindow" Height ="450" Width ="800" > <Canvas > <Rectangle Canvas.Left ="10" Canvas.Top ="10" Fill ="Yellow" Width ="40" Height ="60" /> <Ellipse Canvas.Left ="10" Canvas.Top ="70" Fill ="Blue" Width ="80" Height ="60" > <i:Interaction.Behaviors > <custom:DragInCanvasBehavior /> </i:Interaction.Behaviors > </Ellipse > <Ellipse Canvas.Left ="80" Canvas.Top ="70" Fill ="OrangeRed" Width ="40" Height ="70" > <i:Interaction.Behaviors > <custom:DragInCanvasBehavior /> </i:Interaction.Behaviors > </Ellipse > </Canvas > </Window >
Blend中的设计时行为支持
在 Expression Blend中,对行为的操作就是拖放和配置操作。为给控件添加动作,从AssetLibrary中拖动一个动作,然后将其拖动到控件上(在该例中,是 Canvas 面板中的某个形状)。当采取这一步骤时,Expression Blend 会自动创建行为,然后可以配置该行为(如果行为具有属性的话)。