若是局限于简单的、灰色外观的普通按钮以及其余经常使用控件,WPF将是没有新意的捆绑。WPF提供了几个特性,容许为基本元素插入一些本身的爱好,并标准化应用程序的可视化外观。本次主要学习样式和行为。样式是组织和重用格式化选项的重要工具。不是使用重复的标记填充XAML,以设置注入外边距、内边距、颜色以及字体等细节,而能够建立一系列封装全部这些细节的;样式。而后能够在须要之处经过一个属性应用样式。行为是一个重用用户界面代码的更有挑战性的工具。其基本思想是行为封装了一些通用的UI功能。若是具备适当的行为,可使用一两行XAML标记将其附加到一个元素,从而能够为您节省便携盒调试代码的工做。canvas
11.1 样式基础app
样式是能够应用于元素的属性值的集合。WPF样式系统和HTML标记中的层叠样式表标准扮演相似的角色。与CSS相似,经过WPF样式能够定义通用的格式化特性集合,而且为了保证一致性,在整个应用程序中应用它们。与CSS同样,WPF样式也可以自动工做,指定具体的元素类型的目标,而且经过元素树层叠起来。然而,WPF样式的功能更增强大,由于它们可以设置任何依赖项属性。这意味着可使用它们标准化未格式化的特性,如控件的行为。WPF样式还支持触发器,当一个属性发生变化时能够经过触发器改变控件的样式,而且可使用模板从新定义控件的内置外观。为了理解适合使用样式的场合,分析一个简单的示例。设想须要标准化在窗口中使用的字体。最简单的方法是设置包含窗口的字体属性。这些属性是在Control类中定义的,得益于这些属性值的继承特性,当在窗口级别上设置这些属性时,在窗口中的全部元素都会使用相同的属性值,除非明确的覆盖它们。如今考虑一种状况,但愿只为用户界面中的一部分锁定字体。若是能在一个特定容器中隔离这些元素,可使用本质上相同的方法,并设置容器的字体属性。若是,咱们但愿使全部的按钮具备抑制的字体和文本尺寸,而且使用和其余元素不一样的字体设置。就须要一种方法在某个地方定义这些细节,并在全部应用它们的地方重用这些细节。以下所示:框架
<Window.Resources> <FontFamily x:Key="ButtonFontFamily">Times New Roman</FontFamily> <s:Double x:Key="ButtonFontSize">18</s:Double> <FontWeight x:Key="ButtonFontWeight">Bold</FontWeight> </Window.Resources>
这是在窗口中定义的资源,一旦定义了资源,下一步是在元素中实际使用这些资源。ide
<Button Padding="5" Margin="5" FontFamily="{StaticResource ButtonFontFamily}" FontWeight="{StaticResource ButtonFontWeight}" FontSize="{StaticResource ButtonFontSize}" >A Customized Button</Button>
这种方式虽然将字体细节移除了标记,但还存有问题,一、除恶资源名称类似以外,没有明确指明三个资源是相关的。若是须要设置更多的字体属性,或者决定为不一样类型的元素维护不一样字体设置,这将变的复杂。二、须要使用资源的标记很是繁琐。咱们能够经过定义一个将全部字体捆绑在一块儿的自定义类。样式对这一问题提供了很是好的解决方案。能够定义一个独立的包装全部但愿设置的属性的样式。以下:工具
<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对象用于一个但愿设置的属性。每一个Setter对象由两部分信息组成:但愿进行设置的属性和但愿为该属性应用的值。与全部资源同样,样式对象有一个键名,从而当须要时能够从集合中提取它。每一个WPF元素均可以使用一个样式,样式经过元素的Style属性插入到元素中。以下,将上面建立的样式配置到一个按钮:学习
<Button Padding="5" Margin="5" Style="{StaticResource BigFontButtonStyle}" >A Customized Button</Button>
也能够经过代码设置样式。须要作的所有工做就是使用熟悉的FindResource()方法,从最近的资源集合中提取样式。以下:字体
cmdButton.Style=(Style)cmd.FindResource("样式键名");
Setter集合是Style类中最重要的属性,但并非惟一的属性,在Style类中共有5个重要属性,以下表中介绍了这些属性动画
Setters | 设置属性值以及自动关联事件处理程序的Setter对象或EventSetter对象的集合 |
Triggers | 继承自TriggerBase类而且可以自动改变样式设置的对象的集合。例如,当另外一个属性改变时,或者当发生某个事件时,能够修改样式 |
Resources | 但愿用于样式的资源集合。例如,可能须要使用一个对象设置多个属性。这时,做为资源建立对象,而后再在Setter对象中使用该资源,这样会更高效 |
BasedOn | 经过该属性能够建立继承自其余样式设置的更复杂样式 |
TargetType | 该属性表示应用样式的元素的类型。经过该属性能够建立只影响特定类型元素的设置器,而且还能够建立可以为恰当的元素类型自动起做用的设置器 |
11.1.1 建立样式对象this
若是但愿建立具备更精细目标的样式,可使用容器的Resoures集合定义样式,如StakPanel面板或Grid面板。若是但愿在应用程序中重用样式,可使用应用程序的Resources集合定义样式。严格来说,不须要同时使用样式和资源。由于没法与其余元素共享该样式,若是只是使用样式设置一些属性,直接设置熟悉更加容易。可是,可使用该方法为一个元素关联触发器。经过该方法还能够修改元素控件模板的一部分。spa
11.1.2 设置属性
每一个Style对象包装了一个Setter对象的集合。每一个Setter对象设置元素的单个属性。惟一的限制是设置器只能改变依赖项属性——不能修改其余属性。在某些状况下,不能使用简单的特性字符创设置属性值。例如,不能使用简单的字符串建立ImageBrush对象。对于这种状况,可使用属性的XAML技巧,使用嵌套的元素代替特性。下面是一个示例:
<Style x:key="HappyTiledElementStyle"> <Setter Property="Control.Background"> <Setter.Value> <ImageBrush TileMode="Tile" ViewportUnits="Absolute" Viewport="0 0 32 32" ImageSoure="happyface.jpg" Opacity="0.3"> </ImageBrush> </Setter.Value> </Setter> </Style>
为了标识但愿设置的属性,须要提供类和属性的名称。然而,使用的类名没必要是定义属性的类名。也能够是继承了属性的派生类。例如以下样式,该样式使用Button类的引用代替了Control类的引用
<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>
在WPF中还存在这样一些状况,在元素框架层次中的多个位置定义了同一个属性。例如,在Control类和TextBlock类中都定义了所有的字体属性。若是正在建立应用到TextBlock对象以及继承自Control类的元素的样式,能够按以下方式建立标记。
<Style x:Key="BigFontButtonStyle"> <Setter Property="Button.FontFamily" Value="Times New Roman" /> <Setter Property="Button.FontSize" Value="18" /> <Setter Property="TextBlock.FontFamily" Value="Arial" /> <Setter Property="TextBlock.FontSize" Value="10" /> </Style>
可是尽管Button.FontFamily属性和TextBlock.FontFamily属性是在他们各自的基类中分别声明,但它们都引用同一个依赖项属性。因此,当使用这个样式时,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>
11.1.3 关联事件处理程序
属性设置器是全部样式中最多见的要素,可是也能够建立为事件关联特定事件处理程序的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>
在WPF中,事件设置器是一种不多使用的技术。若是须要使用此处演示的功能,可能更喜欢使用事件触发器,它以声明的方式定义了所但愿的行为。事件触发器是专门为实现动画而设计的。
11.1.4 多层样式
尽管能够砸许多不一样层次蒂尼任意数量的样式,可是每一个WPF元素一次只能使用一个样式对象。乍一看,好像是一种限制,但因为属性值继承和样式继承特性,所以实际上这种限制并不存在。例如,设想但愿为一组控件使用相同的字体,而又不想为每一个控件应用相同的样式。对于这种状况能够经过样式设置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>
11.1.5 经过类型自动应用样式
到目前为止,已经看到了如何建立具备名称的样式以及如何在标记中引用它们。但还有另外一种方法,既能够为特定类型的元素自动应用样式。只须要设置TargetType属性以指定合适的类型,并彻底忽略键名。当这样作时,WPF其实是隐式的使用类型标记扩展设置键名,以下所示:
x:Key="{x:Type Button}"
如今样式被自动应用于整个元素树中的全部按钮上。下面是一个示例,能够获得与上面相同的结果
<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 Button</Button> <TextBlock Margin="5">Normal Content.</TextBlock> <Button Padding="5" Margin="5" Style="{x:Null}" >A Normal Button</Button> <TextBlock Margin="5">More normal Content.</TextBlock> <Button Padding="5" Margin="5">Another Customized Button</Button> </StackPanel>
不过中间的按钮经过将样式设置为Null有效的删除了样式。
尽管自动样式很是方便,可是它们会让设计变得复杂。下面是几条缘由:
11.2 触发器
WPF中的一个主题是以声明方式扩展代码的功能。当使用样式、资源以及数据绑定时,将会发现即便不使用代码,也能完成很多工做。使用触发器,能够自动完成简单的样式改变,而这一般须要使用样板事件处理逻辑。例如,当一个属性发生变化时能够进行响应,并自动调整样式。触发器经过Style.Triggers集合链接到样式。每一个样式均可以有任意多个触发器,而且每一个触发器都是System.Windows.TriggerBase的派生类的实例。下表列出了WPF中的选项
Trigger | 这是一种最简单的触发器。它监测依赖属性的变化,而后使用设置器改变样式 |
MultiTrigger | 与Trigger相似,可是这种触发器联合了多个条件。只有知足了全部这些条件,才会启动触发器 |
DataTrigger | 这种触发器使用数据绑定。它与Trigger相似,只不过它监视的是全部绑定数据的变化 |
MultiDataTrigger | 联合多个数据触发器 |
EventTrigger | 这是最复杂的触发器。当一个事件发生时,这种触发器应用一个动画 |
经过FrameworkElement.Trigger集合,能够直接为元素应用触发器,而不须要建立样式。但这存在一个至关大的缺陷。这个Triggers集合只支持事件触发器。
11.2.1 简单触发器
能够为任何依赖项属性关联一个简单触发器。例如,能够经过响应Control类的IsFocused、IsMouseOver以及IsPressed属性的变化,建立鼠标悬停效果和焦点效果。每一个简单触发器都指定了正在监视的属性,以及正在等待的属性值。当该属性值出现时,将自动应用存储在Trigger.Setters集合中的设置器。但不能使用复杂的触发器逻辑。下面的触发器等待按钮获取键盘焦点,当获取焦点时或将其前景色设置为深红色。
<Style.Triggers> <Trigger Property="Control.IsFocused" Value="True"> <Setter Property="Control.Foreground" Value="DarkRed" /> </Trigger> <Trigger Property="Control.IsMouseOver" Value="True"> <Setter Property="Control.Foreground" Value="LightYellow" /> <Setter Property="Control.FontWeight" Value="Bold" /> </Trigger> <Trigger Property="Button.IsPressed" Value="True"> <Setter Property="Control.Foreground" Value="Red" /> </Trigger> </Style.Triggers>
能够建立一次应用于相同元素的多个触发器。若是这些触发器设置不一样的属性,这种状况就不会出现混乱。然而,若是多个触发器修改了相同的属性,那么最终是最后的触发器有效。如上面的示例中IsPressed属性最后触发因此它得以实现。触发器在标记中的排列顺序彻底决定了最终的结果。若是但愿建立只有当几个条件都为真时才激活的触发器,可使用MultiTrigger。这种触发器提供了一个Conditions集合,能够经过该集合定义一系列属性和值的组合。以下示例中,只有按钮具备焦点并且鼠标悬停在该按钮上时,才会应用格式化信息:
<Style.Triggers> <MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Control.IsFocused" Value="True"/> <Condition Property="Control.IsMouseOver" Value="True"/> </MultiTrigger.Conditions> <MultiTrigger.Setters> <Setters Property="Control.Foreground" Value="DarkRed"/> </MultiTrigger.Setters> </MultiTrigger> </Style.Triggers>
11.2.2 事件触发器
普通的触发器等待一个属性发生变化,而事件触发器等待特定的事件被激发。事件触发器要求用户提供一系列修改控件的动做。这些动做一般被用于一个动画。下面使用一个动画效果是按钮的FontSize属性从而造成动画效果。
<EventTrigger RoutedEvent="Mouse.MouseEnter"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:0.2" Storyboard.TargetProperty="FontSize" To="22" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger> <EventTrigger RoutedEvent="Mouse.MouseLeave"> <EventTrigger.Actions> <BeginStoryboard> <Storyboard> <DoubleAnimation Duration="0:0:1" Storyboard.TargetProperty="FontSize" /> </Storyboard> </BeginStoryboard> </EventTrigger.Actions> </EventTrigger>
在这个示例中,使用了一个预先构建的DoubleAnimation类。DoubleAnimation类可以在一段给定的时间内将任何双精度数值逐渐改变为设定的目标值。由于双精度数值以较小的步长改变,因此将会发现字体逐渐的增大。当依赖项属性等于一个特定值时也能够执行动画。若是没有适合的事件可供使用而又但愿执行一个动画时,须要使用以前介绍的属性触发器。不为属性触发器提供任何Setter对象。反而,设置Trigger.EnterActions和Trigger.ExitActions属性。这两个属性都有一个动做集合,例如,启动一个动画的BeginStoryboard动做。当属性到达指定的值时,执行EnterActions,而当属性离开指定的值时,执行ExitActions。
11.3 行为
样式提供了重用一组属性设置的实用方法。属性设置仅仅是用户界面基础结构中的小部分。设置在大部分基本的程序一般须要大量的用户界面代码,这些代码与应用程序的功能无关。在许多程序中,用于用户界面的代码,不管是在数量仍是复杂性上都超出了业务代码。许多这类代码是通用的,这意味着建立的每一个WPF对象中须要编写相同的内容。因此开发了行为这一特征。其想法很简单:您建立一个封装了一些通用用户界面功能的行为。这一个功能能够是基本功能,也能够是负责功能。一旦构建了功能,能够将其添加到任意应用程序中的另外一个控件中,具体方法是该控件链接到适当的行为并设置行为的属性。
11.3.1 获取行为支持
重用用户界面代码通用块的基本机构不是WPF的一部分。反而,它被捆绑到Expression Blend。这是由于行为开始是做为Expression Blend的设计时特性引入的。Expression Blend仍然是经过将行为拖动到须要行为的控件上来添加行为的惟一工具。只须要付出不多的努力就能够在Visual Studio应用程序中建立和使用行为。只须要手动编写标记,而不是使用工具箱。为了得到支持行为的程序集,有两种选择。
不管采用哪一种方法,在相似c:\Program Files\Microsoft SDKs\Expression\Blend3\Interactivity\Libraries\WPF的文件夹中都将发现两个重要的程序集:
11.3.2 理解行为模型
行为特性具备两个版本。一个版本针对Silverlight添加行为支持而设计,另外一个版本是针对WPF设计的。尽管这两个版本提供了相同的特性,可是行为特性和Silverlight领域更吻合,由于它弥补了更大的鸿沟。然而,WPF支持触发器,行为特性包含本身的触发器系统,而触发器系统与WP模型不匹配。相似名称的这两个特性有部分重合可是不彻底相同。在WPF中,触发器最重要的角色是构建灵活的样式和控件模板。在触发器的帮助下,样式和模板能够变得更加智能;WPF触发器支持更增强大的样式和控件模板。而Expression Blend触发器支持快速的不须要代码的应用程序设计。对于使用WPF的普通开发人员来讲全部这些意味着:
11.3.3 建立行为
行为旨在封装一些UI功能,从而能够不用编写代码就可以将其做用到元素上。从另一个角度看,每一个行为都为元素提供了一个服务。该服务一般涉及到监听几个不一样的事件并执行几个相关操做。为了更好的理解行为,最好的方法是本身建立一个行为。假设为任意元素提供使用鼠标在anvas面板上拖动元素的功能。对于单个元素实现该功能的基本步骤是很是简单的,代码监听属性事件并修改设置相应Canvas坐标的附加属性。
首先建立一个WPF类库程序集。在该程序中,添加对System.Windows.Interactivity.dll程序集的引用。而后,建立一个继承自Behavior基类的类。Behavior是一个通用类,该类使用一个类型参数。可使用该类型参数将行为限制到特定的元素,或者可使用UIElement或FrameworkElement将他们都包含进来,以下所示:
public class DragInCanvasBehavior : Behavior<UIElement> {}
在任何行为中,第一步是覆盖OnAttached()和OnDetaching()方法。当调用OnAttached()方法时,能够访问放置行为的元素(经过AssociatedObject属性),而且能够关联事件处理程序。当调用OnDetaching()方法时,移除事件处理程序。下面是DragInCanvasBehavior类用于监视MouseLeftButtonDown、MouseMove以及MouseLeftButtonUp事件的代码:
protected override void OnAttached() { base.OnAttached(); // 链接事件处理程序. this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove += AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp; } protected override void OnDetaching() { base.OnDetaching(); // 分离事件处理程序。 this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp; }
最后一步是在事件处理程序中运行适当的代码。例如,当用户单击鼠标左键时,DragInCanvasBehavior开始拖动操做,记录元素左上角与鼠标指针之间的偏移,并捕获鼠标:
private Canvas canvas; // 跟踪被拖动元素。 private bool isDragging = false; // 当元素被点击时,记录的确切位置单击 private Point mouseOffset; private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // 找到画布 if (canvas == null) canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas; // 拖动模式开始. isDragging = true; // 获得点击的位置相对于元素(元素的左上角是(0,0)。 mouseOffset = e.GetPosition(AssociatedObject); // 捕获鼠标。这样你就会保持receiveing MouseMove事件即便用户混乱鼠标元素。 AssociatedObject.CaptureMouse(); }
当元素处于拖动模式而且移动鼠标时,重新定位元素:
private void AssociatedObject_MouseMove(object sender, 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, MouseButtonEventArgs e) { if (isDragging) { AssociatedObject.ReleaseMouseCapture(); isDragging = false; } }
11.3.4 使用行为
建立一个新的WPF应用程序项目。而后添加对定义DragInCanvasBehavior类的类库以及System.Windows.Interactivity.dll程序集的引用。在XML中映射这两个名称空间。假设DragInCanvasBehavior类的类库名为CustomBehaviorsLibrary,则须要如下标记:
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:custom="clr-namespace:CustomBehaviorsLibrary;assembly=CustomBehaviorsLibrary"
为了使用这个行为,只须要使用Interaction.Behaviors附加属性在Canvas面板中添加任意元素。下面的标记建立了一个具备三个图形的Canvas面板。两个Ellipse元素使用了DragInCanvasBehavior,而且能在Canvas面板中拖动。Rectangle元素没有使用DragInCanvasBehavior,所以没法移动。
<Canvas> <Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60"></Rectangle> <Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> <Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> </Canvas>
本章节介绍了如何使用样式重用元素的格式化设置,还分析了如何使用行为开发整洁的用户界面功能包,而后能够将其链接到任意元素。这两个工提供了制做智能程度更高、具备更好维护性用户界面的方法——几种格式化细节和复杂逻辑,而不要求开发人员在整个应用程序中的不一样位置使用这些细节和逻辑并屡次使用。