WPF学习系列之七 (样式与行为)

时间:2021-12-04 15:06:43
  1. 样式(Styles)是组织和重用格式化选项的重要工具。不是使用重复的标记填充XAML,以设置诸如边距、颜色及字体等细节,而可以创建一系列封装所有这些细节的样式。然后可以在需要之处通过一个属性应用样式。
    行为(behavior)是一个重用用户界面代码的工具,它封装了一些通用的UI功能。如果具有适当的行为,可以使用一两行XAML标记将其附加到一个元素,从而可以节省编写和调试代码的工作。
  2. 创建样式对象
    WPF学习系列之七 (样式与行为)
    <Window.Resources>
    <Style x:Key="BigFontButtonStyle">
    <Setter Property="Control.FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="Control.FontSize" Value="20"></Setter>
    <Setter Property="Control.FontWeight" Value="Bold"></Setter>
    </Style>
    </Window.Resources>
    WPF学习系列之七 (样式与行为)

    上面的标记创建了一个独立的资源:一个System.Windows.Style对象,与所有资源一样,样式对象有一个键名,从而当需要时 可以从集合中提取它,根据约定,用于样式的键名通常以Style结尾。样式通过元素的Style属性(该属性是在FrameworkElement基类中 定义的)插入到元素中,如下所示为按钮使用了以上定义的样式:

    <Button Style="{StaticResource BigFontButtonStyle}">A Button For My Test</Button>

    也可以使用代码设置样式,就是使用FindResources()方法从最近的资源集合中提取样式:

    btnStyleTest1.Style = (Style)btnStyleTest1.FindResource("BigFontButtonStyle");

    样式设置元素的初始外观,但是可以随意覆盖它们设置的这些特性。例如,如果应用了BigFontButtonStyle样式,并且明确地设置了FontSize属性,则在按钮中的FindSize设置就会覆盖样式。
    Setters集合是Style类中最重要的属性,但并不是唯一的属性,在Style类*有5个重要属性:

    WPF学习系列之七 (样式与行为)

  3. 设置样式属性

    每个Style对象包装了一个Setter对象的集合,每个Setter对象设置元素的单个属性。唯一的限制是设置器只能改变依赖项属性而不能修改其它属性。

    在某些情况下,不能使用简单的特性字符串设置属性值,此时可以使用嵌套的属性元素代替特性,如下是一个示例:

    WPF学习系列之七 (样式与行为)
    <Window.Resources>
    <Style x:Key="HappyFaceTileStyle">
    <Setter Property="Control.Background">
    <Setter.Value>
    <ImageBrush ImageSource="images/happyface.jpg" ViewportUnits="Absolute" Viewport="0,0,32,32" TileMode="Tile"></ImageBrush>
    </Setter.Value>
    </Setter>
    </Style>
    </Window.Resources>
    WPF学习系列之七 (样式与行为)

    如果希望在多个样式中(或在同一样式的多个设置器中)重用相同的图像画刷,可以将其定义为资源,然后再在样式中使用资源:

    WPF学习系列之七 (样式与行为)
    <Window.Resources>
    <ImageBrush x:Key="happyFace" ImageSource="images/happyface.jpg" ViewportUnits="Absolute" Viewport="0,0,32,32" TileMode="Tile"/>
    <Style x:Key="HappyFaceTileStyle">
    <Setter Property="Control.Background" Value="{StaticResource happyFace}"></Setter>
    </Style>
    </Window.Resources>
    WPF学习系列之七 (样式与行为)

    为了标识希望设置的属性,需要设置类和属性的名称,然而,类名不必是定义属性的类名,也可以是继承了属性的派生类,如下示例,它使用Button类的引用代替了Control类的引用,此时,如果为Label元素也引用了此样式,哪么对于Label此样式就不起作用:

    <Style x:Key="BigFontButtonStyle">
    <Setter Property="Button.FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="Button.FontSize" Value="20"></Setter>
    <Setter Property="Button.FontWeight" Value="Bold"></Setter>
    </Style>

    如下样式,Button.FontSize属性和TextBlock.FontSize属性是在它们各自的基类中分别声明,但它们都引用同 一个依赖项属性(也就是说,TextBlock.FontSize和Control.FontSize引用都指向同一个 DependencyProperty对象)。所以,当使用这个样式时,WPF设置FontSize属性两次,最后引用的设置具有优先权,并且被同时引用 到Button和TextBlock对象。

    <Style x:Key="BigFontButtonStyle">
    <Setter Property="Button.FontSize" Value="20"></Setter> <Setter Property="TextBlock.FontSize" Value="33"></Setter>
    </Style>

    如果定义的样式中所有的属性都准备用于相同的元素类型,可以设置Style对象的TargetType属性,指定准备应用属性的类来简化样式声明,例如,如果只创建只应用于按钮的样式,可按如下方式创建样式:

    <Style x:Key="BigFontButtonStyle" TargetType="Button">
    <Setter Property="FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="FontSize" Value="20"></Setter>
    <Setter Property="FontWeight" Value="Bold"></Setter>
    </Style>
  4. 关联事件处理程序
    先看下面的示例,在样式设置中使用EventSetter对象为MouseEnter和MouseLeave事件关联事件处理程序:

    <Style x:Key="MouseOverHighlightStyle">
    <EventSetter Event="TextBlock.MouseEnter" Handler="element_MouseEnter"></EventSetter>
    <EventSetter Event="TextBlock.MouseLeave" Handler="element_MouseLeave"></EventSetter>
    <Setter Property="TextBlock.Padding" Value="5"></Setter>
    </Style>

    下面是事件处理程序:

    WPF学习系列之七 (样式与行为)
    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;
    }
    WPF学习系列之七 (样式与行为)

    MouseEnter和MouseLeave事件使用直接事件路由,这意味着它们不在元素树中冒泡或隧道。如果希望为大量元素应用以上的效 果,需要为每个元素添加MouseEnter和MouseLeave事件处理程序,基于样式的事件处理程序简化了这一任务,现在只需要应用单个样式,该样 式包含了属性设置器和事件设置器:

    <TextBlock Grid.Row="3" Style="{StaticResource MouseOverHighlightStyle}">Hover over me</TextBlock>

    在WPF中,事件设置器是一种很少使用的技术,而更多的使用的是事件触发器。当处理冒泡路由策略时,事件设置器不是一个好的选择,对于是种情况,在高层次的元素上处理希望处理的事件通常更容易。

  5. 多层样式
    尽管可以在许多不同的层次定义任意数量的样式,但是每个元素一次只能使用一个样式对象,乍一看,这好像是一种限制,但由于属性值继承
    和样式继承特性,因此,实际上这种限制并不存在。如果希望为一组相同的控件使用相同的字体,而又不想为每个控件应用相同的样式,对于这种情况,可以将它们
    放置到一个面板,并设置面板的样式,只要设置的属性具有属性值继承特性,这些值就会被传递到子元素。使用这种模型的属性包括IsEnabled、
    IsVisible、Foreground以及所有字体属性。

    对于样式继承特性,可以通过为样式设置 BasedOn 特性在另一个样式的基础上创建样式,例如以下代码,第二个样式继承了第一个样式的属性设置:

    WPF学习系列之七 (样式与行为)
    <Style x:Key="BigFontButtonStyle">
    <Setter Property="Control.FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="Control.FontSize" Value="20"></Setter>
    <Setter Property="Control.FontWeight" Value="Bold"></Setter>
    </Style>
    <Style x:Key="EmphasizedBigFontButtonStyle" BasedOn="{StaticResource BigFontButtonStyle}">
    <Setter Property="Control.Background" Value="White"></Setter>
    <Setter Property="Control.Foreground" Value="DarkBlue"></Setter>
    </Style>
    WPF学习系列之七 (样式与行为)

    除非有特殊原因要求一个样式继承自另一个样式(例如,第二个样式是第一个样式的特例,并且只改变了继承来有大量设置中的几个特征),否则不要使用样式继承,因为这种继承会使应用程序更加脆弱。

  6. 通过类型自动应用样式
    可以通过设置样式的TargetType属性为特定类型的元素自动应用样式,并完全忽略键名,此时,WPF实际上是隐式地使用类型标记扩展设置键名,如下代码所示:

    x:Key="{x:Type Button}"

    如下示例,在一个窗口中为样式设置TargetType特性为Button,此样式就会被应用到这个窗口中的所有按钮上:

    <Style TargetType="Button">
    <Setter Property="FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="FontSize" Value="20"></Setter>
    <Setter Property="FontWeight" Value="Bold"></Setter>
    </Style>

    可以为一个元素的样式特性设置null值来删除样式:

    <Button Style="{x:Null}">Test Button</Button>

    尽管自动样式很方便,但是它会使设计变得复杂,下面是几条原因。为了避免出现此问题,最好果断的使用自动样式为整个用户界面提供一个单一且一致的外观,然后再为特例设置明确的样式。

    • 在具有许多样式和多层样式的复杂窗口中,跟踪是否通过属性值继承或通过样式设置了某个特定属性有些困难,因此,如果希望改变一个简单的细节,需要查看整个窗口的全部标记。
    • 窗口中的格式化化操作在开始时通常更一般,但是会越来越详细,如果刚开始为窗口应用了自动样式,在许多地方可能需要通过显式的样式覆盖自动样式,这会使整个设计变得复杂。为每个希望设置的格式化特征的组合创建命名的样式,并根据名称应用它们会更直观。
    • 如果为TextBlock创建了一个自动样式,会同时修改使用TextBlock的其它控件。
  7. 触发器
    使用触发器,可以自动完成简单的样式改变,例如,当一个属性发生变化时可以进行响应,并自动调整样式。触发器在Style的
    Triggers集合中定义,每个样式都可以有任意多个触发器,每个触发器都是System.Windows.TriggerBase的派生类的实例。

    WPF学习系列之七 (样式与行为)

  8. 简单触发器

    可以为任何依赖项属性关联一个简单触发器。每个触发器都指定了正在监视的属性,以及正在等待的值,当该属性值出现时,将自动应用存储在Trigger.Setters集合中的设置。例如下面的触发器等待按钮获取键盘焦点,当获取焦点时会将其前景色设置为深红色:
    WPF学习系列之七 (样式与行为)
    <Style x:Key="BigFontButtonStyle">
    <Style.Setters>
    <Setter Property="Control.FontFamily" Value="Times New Roman"></Setter>
    <Setter Property="Control.FontSize" Value="20"></Setter>
    <Setter Property="Control.FontWeight" Value="Bold"></Setter>
    </Style.Setters>
    <Style.Triggers>
    <Trigger Property="Control.IsFocused" Value="True">
    <Setter Property="Control.Foreground" Value="DarkRed"></Setter>
    </Trigger>
    </Style.Triggers>
    </Style>
    WPF学习系列之七 (样式与行为)

    触发器的优点是不需要为翻转它们而编写任何逻辑,只要停止应用触发器,元素就会恢复到它的正常外观。可以创建一次应用相同元素的多个触发器,如果这些触发器设置不同的属性,这种情况就不会出现混乱,但如果多个触发器修改相同的属性,那么最后的触发器有效。 
    如果希望创建只有当几个条件都为真时才激活的触发器,可以使用 MultiTrigger。它提供了一个 Conditions 集合,可以通过该属性定义一系列属性和值的组合,在下例中,只有当按钮具有焦点且鼠标悬停在该按钮上时,才会应用样式:

    WPF学习系列之七 (样式与行为)
    <Style x:Key="BigFontButtonStyle">
    <Style.Triggers>
    <MultiTrigger>
    <MultiTrigger.Conditions>
    <Condition Property="Control.IsFocused" Value="True"></Condition>
    <Condition Property="Control.IsMouseOver" Value="True"></Condition>
    </MultiTrigger.Conditions>
    <MultiTrigger.Setters>
    <Setter Property="Control.Foreground" Value="DarkRed"></Setter>
    </MultiTrigger.Setters>
    </MultiTrigger>
    </Style.Triggers>
    </Style>  
    WPF学习系列之七 (样式与行为)
  9. 事件触发器
    普通的触发器等待一个属性发生变化,而事件触发器等待一个特定的事件被激发,它也不是通过设置器来改变元素,而是要求用户提供一系列修改
    控件的动作,这些动作通常被用于一个动画。下面的示例是事件触发器等待MouseEnter事件,然后动态的在0.2秒内将按钮的字体放大到22个单位:
    WPF学习系列之七 (样式与行为)
    <Style x:Key="EventTriggerTest">
    <Style.Triggers>
    <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"></DoubleAnimation>
    </Storyboard>
    </BeginStoryboard>
    </EventTrigger.Actions>
    </EventTrigger>
    </Style.Triggers>
    </Style>
    WPF学习系列之七 (样式与行为)

    在XAML中,必须在故事板(Storyboard)中定义每个动画,它为动画提供了时间线,在故事板内部,用户可以定义希望使用的一个或多个动画对象,每个动画对象执行本质上相同的任务:在一定时期内修改依赖项属性。
    DoubleAnimation类(同所有动画类,都位于System.Windows.Media.Animation名称空间中):它能在一段给定的时间内将任何双精度值属性逐渐改变为设定的目标值,改变的实际尺寸取决于时间总量和需要改变的总量。

    与属性触发器不同,如果希望翻转回原来的状态,需要反转事件触发器。在以上示例中,样式使用了一个响应MouseLeave事件的事件触发器,并在1秒内恢复到原来的大小,此时不需要指明目标字体尺寸,WPF会假定反转回原来的字体尺寸。

  10. 行为,行为旨在封装一些UI功能,从而可以不用编写代码就能够将其应用到元素上。

    样式提供了重用一组属性设置的实用方法,但在典型的应用程序中,属性设置只是用户界面基础结构中的一小部分,而大部分基本的程序通常需要
    大量的用户界面代码,这些代码通常与应用程序的功能无关,在许多程序中,用于用户界面的代码(如驱动动画、实现平滑效果、维护用户界面状态)无论是在数量
    上还是在复杂性上都超过了业务代码,而许多这类代码是通用的,所以Expression
    Elend创作者开发了一个称为行为的特征。其思想是开发人员创建一个封装了一些通用用户界面功能的行为,然后通过将行为拖动到需要行为的控件上,此控件
    就具有了行为所定义的一些功能。

    为了获得支持行为特性的程序集,可以安装Expression Blend或安装Expression Blend SDK(下载地址:http://tinyurl.com/kkp4g8):

    System.Windows.Interactivity.dll:它定义了支持行为的基本类,它是行为特征的基础。

    Microsoft.Expression.Interactions.dll:它通过添加可选的以核心行为类为基础的动作和触发器类,增加了一些有用的扩展。

  11. 创建行为

    首先添加对System.Windows.Interactivity.dll的引用,然后创建一个继承自Behavior基类的类。在
    此类中覆盖OnAttached()和OnDetaching()方法,在这两个方法中可以通过AssociatedObject属性访问放置行为的元
    素。在OnAttached()方法中关联事件处理程序,在OnDetaching()中移除事件处理程序。
    以下是创建一个可以为任意元素提供使用鼠标在Canvas面板上拖动元素的行为类 DragInCanvasBehavior:
    WPF学习系列之七 (样式与行为)
     1 public class DragInCanvasBehavior : Behavior<UIElement>
    2 {
    3 private Canvas canvas;
    4
    5 //以下的OnAttached()和OnDetaching()方法是用于监视MouseLeftButtonDown、MouseMove、MouseLeftButtonUp事件的代码。
    6 protected override void OnAttached()
    7 {
    8 base.OnAttached();
    9
    10 // Hook up event handlers.
    11 this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown;
    12 this.AssociatedObject.MouseMove += AssociatedObject_MouseMove;
    13 this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp;
    14 }
    15
    16 protected override void OnDetaching()
    17 {
    18 base.OnDetaching();
    19
    20 // Detach event handlers.
    21 this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown;
    22 this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove;
    23 this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp;
    24 }
    25
    26 // Keep track of when the element is being dragged.
    27 private bool isDragging = false;
    28
    29 // When the element is clicked, record the exact position
    30 // where the click is made.
    31 private Point mouseOffset;
    32
    33 //当用户单击鼠标左键时,DragInCanvasBehavior开始拖动操作,记录元素左上角与鼠标指针之间的偏移,并捕获鼠标。
    34 private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    35 {
    36 // Find the canvas.
    37 if (canvas == null) canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas;
    38
    39 // Dragging mode begins.
    40 isDragging = true;
    41
    42 // Get the position of the click relative to the element
    43 // (so the top-left corner of the element is (0,0).
    44 mouseOffset = e.GetPosition(AssociatedObject);
    45
    46 // Capture the mouse. This way you'll keep receiveing
    47 // the MouseMove event even if the user jerks the mouse
    48 // off the element.
    49 AssociatedObject.CaptureMouse();
    50 }
    51
    52 //当元素处于拖动模式且移动鼠标时,重新定位元素。
    53 private void AssociatedObject_MouseMove(object sender, MouseEventArgs e)
    54 {
    55 if (isDragging)
    56 {
    57 // Get the position of the element relative to the Canvas.
    58 Point point = e.GetPosition(canvas);
    59
    60 // Move the element.
    61 AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y);
    62 AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X);
    63 }
    64 }
    65
    66 //当释放鼠标时,结束拖动。
    67 private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    68 {
    69 if (isDragging)
    70 {
    71 AssociatedObject.ReleaseMouseCapture();
    72 isDragging = false;
    73 }
    74 }
    75 }
    WPF学习系列之七 (样式与行为)
  12. 使用行为
    首先在项目中添加对定义DragInCanvasBehavior类的类库及
    System.Windows.Interactivity.dll程序集的引用,然后在窗口XML中映射这两个名称空间。只需要使用
    Interaction.Behaviors附加属性在Canvas面板中添加任意元素。如下是创建了一个具有三个元素的Canvas面板,其中两个元素
    使用了DragInCanvasBehavior行为:
    WPF学习系列之七 (样式与行为)
    <Window x:Class="WpfApplication1.DragInCanvasTest"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
    xmlns:custom="clr-namespace:CustomBehaviousLibrary;assembly=CustomBehaviousLibrary"
    Title="DragInCanvasTest" Height="300" Width="300">
    <Grid>
    <Canvas>
    <Rectangle Canvas.Left="36" Canvas.Top="49" Height="46" Name="rectangle1" Stroke="Black" Width="76" Fill="#FFC00000">
    <i:Interaction.Behaviors>
    <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior>
    </i:Interaction.Behaviors>
    </Rectangle>
    <Ellipse Canvas.Left="82" Canvas.Top="126" Height="69" Name="ellipse1" Stroke="Black" Width="39" Fill="#FF2323D1">
    <i:Interaction.Behaviors>
    <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior>
    </i:Interaction.Behaviors>
    </Ellipse>
    <Ellipse Canvas.Left="164" Canvas.Top="103" Height="34" Name="ellipse2" Stroke="Black" Width="41" Fill="#FF1CAA1C" />
    </Canvas>
    </Grid>
    </Window>
    WPF学习系列之七 (样式与行为)