样式(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.WpfNuget包

  行为旨在封装一些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>
{
/// <summary>
/// 跟踪放置此元素的Canvas。
/// </summary>
private Canvas? _canvas;

/// <summary>
/// 跟踪元素被拖动的时间。
/// </summary>
private bool _isDragging = false;

/// <summary>
/// 当元素被点击时,记录点击的确切位置
/// </summary>
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)
{
// 找到Canvas
if (_canvas == null)
_canvas = (Canvas)VisualTreeHelper.GetParent(AssociatedObject);

// 启动拖动模式
_isDragging = true;

// 获取点击相对于元素的位置
_mouseOffset = e.GetPosition(AssociatedObject);

// 捕获鼠标这样就能一直收到MouseMove事件
AssociatedObject.CaptureMouse();
}

private void AssociatedObject_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if (_isDragging)
{
// 获取元素相对于Canvas的位置
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.WpfNuget包

  下面的标记创建一个具有三个图形的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 会自动创建行为,然后可以配置该行为(如果行为具有属性的话)。