jk's notes
  • 事件触发器与动画

事件触发器与动画

本章的案例可以都研究一番.

属性触发器允许某个属性在符合要求时, 会引用特定资源 (样式). 本章提供另一个方面的触发器, 即某个事件触发时调用触发器. 而动画是一系列顺滑的属性变更.

  • 属性样式的变更是突发性的, 而动画则是渐变的
  • 而且系统提供了丰富的事件可以进行控制, 而属性相对较少

事件触发

首先作者依旧将属性触发器做了一个简要复习, 作为引子, 通过对比来引入事件触发起. 并一次说明了鼠标进入, 离开, 移动事件.

要创建事件触发器, 可以在控件或样式中添加 Triggers 标签属性, 然后在其中添加 EventTrigger 元素.

每一个 EventTrigger 元素都有 RoutedEvent 属性, 用于描述监听什么事件. 其值为 发起事件的控件类型.事件名.

然后在 EventTrigger 中提供 Actions 标签属性, 在 Actions 中可以编写需要执行的动画 (这里被称为故事板 storyboard).

通常会在 Actions 中使用 BeginStoryboard 元素来执行 Storyboard 对象.

后面会介绍 Storyboard 的细节, 这里假设在 Window 资源中已定义好, 命名为 sbBigScale.

例如:

<Button Width="100" Height="50" Content="点击">
	<Button.Triggers>
  	<EventTrigger RoutedEvent="Button.MouseEnter">
      <EventTrigger.Actions>
        <BeginStoryboard Storyboard="{StaticResource sbBigScale}">
        </BeginStoryboard>
      </EventTrigger.Actions>
    </EventTrigger>
  </Button.Triggers>
</Button>

这里的 BeginStoryboard 就是运行故事面板的意思, 逻辑上标签命名为 RunStoryboard 更好.

事件触发位置 (Event Trigger Locations)

作者描述了一个点击按钮后, 按钮会旋转一个角度的示例.

image-20250724172951373

其实不明其作用.

Storyboard 无法创建一个变换, 但是可以修改 Transformation 属性的值. 因此一般要实现一个变换效果, 都是先定义一个初始值, 然后在 Storyboard 中对其进行修改.

故事版 (Storyboard) 也是一种资源, 目的是简化按钮的定义. 也可以将 EventTrigger 定义在 按钮的 Style 中.

然后作者又演示了一个 demo, 集中来维护事件. 这个案例是左边显示小图标, 点击后会移动放大显示在右侧.

image-20250724182024779

在该示例中有一个 RemoveStoryboard, 用于移除之前没有执行完的故事板.

本例的代码可以研究一下.

属性元素中的 Storyboard

Storyboard 也是一个属性, 代码逻辑如下:

<Button>
  <Button.Triggers>
    <EventTrigger RoutedEvent="...">
      <EventTrigger.Actions>
      </EventTrigger.Actions>
    </EventTrigger>
  </Button.Triggers>
</Button>

样式中的 Storyboard

将 Storyboard 定义在按钮中, 会使代码难以维护. 所以可以将其定义在 Style 中

<Style x:Key="..." TargetType="...">
  <Style.Triggers>
    <EventTrigger RoutedEvent="...">
      <EventTrigger.Actions>
      </EventTrigger.Actions>
    </EventTrigger>
  </Style.Triggers>
</Style>

使用的时候, 直接使用该样式, 同时具备了对应的事件触发器:

<Button Style="{StaticResource ...}"></Button>

属性触发动画

通常, 属性触发器使用 setter 来修改属性值, 但是也可以用来运行 storyboard. 属性触发器运行 storyboard 有别与 setter.

setter 修改属性, 是在属性具备某个值的时候立即进行的. 有点突变的感觉.

而动画的播放是有一个过程的. 类似于渐变. 但是这会存在一个问题. 比如动画播放只需要十秒, 而属性值的变化持续了 20 秒, 那么后 10 秒如何呈现? 重复播放动画. 似乎有点不合适.

jk: 是不是会重复有待验证.

为了合理的应用动画, 属性触发器提供了两个额外的部分, 也可以执行动画: EnterAction 和 ExitAction.

  • setter 依旧是立即执行.
  • EnterAction 节点中的动画会在属性值变化成指定值时触发.
  • ExitAction 节点中的动画会在属性值编程另一个值的时候触发.

演示代码片段:

<Style x:Key="btnStyle" TargetType="Button">
  <Setter Property="Margin" Value="10" />
  <Setter Property="Width" Value="100" />
  <Setter Property="Height" Value="50" />
  <Style.Triggers>
    <Trigger Property="IsMouseOver" Value="False" >
    	<Setter Property="FontStyle" Value="Italic" />
      <Trigger.EnterAction >
        <BeginStoryboard Storyboard="{StaticResource sbBigSize}" />
      </Trigger.EnterAction>
      <Trigger.ExitAction>
        <BeginStoryboard Storyboard="{StaticResource sbSmallSize}" />
      </Trigger.ExitAction>
    </Trigger>
  </Style.Triggers>
</Style>

然后作者解释了这段代码的执行特征. 包括执行时间等.

STORYBOARD

至此已经知道如何执行 Storyboard 了, 下面看看怎么定义. 一个 Storyboard 就是一个对象, 它定义了一个或多个动画的时间线.

animation 在一段时间内, 让属性从一个值变成另一个值. 动画可以设定从什么时候开始, 持续多久时间.

Animation 类让动画创建变得简单, 你只需要设定要需要在一定时间内, 某些属性需要变化的值即可. frame 的数量决定了动画的速度.

Animation 类让你设定要变成什么, 不用去关心怎么变.

要保证动画的性能, 请尽可能缩小动画影响的范围. 因为某个控件的大小可能会影响到周围控件的变化. 这与一些布局控件有一定关系.

具体的动画内容就是在设置不同类型的属性, 例如 Double, Color, String, Thickness 等.

下面是 5 个简单的 DoubleAnimation 对象:

<Storyboard x:Key="sbImg1">
  <DoubleAnimation Duration="0:0:0.5" To="120" 
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="(Canvas.Left)"/>
  <DoubleAnimation Duration="0:0:0.5" To="0" 
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="(Canvas.Top)"/>
  <DoubleAnimation Duration="0:0:0.5" To="485" 
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="Width"/>
  <DoubleAnimation Duration="0:0:0.5" To="400" 
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="Height"/>
  <DoubleAnimation Duration="0:0:0.5" To="1" 
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="Opacity"/>
</Storyboard>

然后作者介绍了一下这段代码的作用, 在 0.5 秒内更改了属性值.

下面的章节介绍了一些常用的属性. 下一节会介绍常用动画类.

Storyboard 和动画属性

animation 类提供了一些控制动画的属性. 例如 Duration 表示动画持续的时间.

Storyboard 类也提供了部分同名的属性, 其 animation 会继承这些属性. 可以做到统一设定.

下面是 animation 与 Storyboard 的属性:

  • AccelerationRatio 类似于加速度, 数值在达到目标值前的加速率.
  • AutoReverse 若为 true, 表示数值达到目标后, 反向变化. 可以作为往复运动的控制.
  • BeginTime 启动 storyboard 后多久开始进行动画. 取值格式为 "时:分:秒"

如果动画没有出现, 可以考虑检查该属性是否设置错误.

  • DecelerationRatio 与 AccelerationRatio 相反.
  • Duration 动画持续时间. 格式为 "时:分:秒".
  • FillBehavior 动画结束后属性值该怎么变化: HoldEnd 保留最终值, Stop 恢复默认值.
  • RepeatBehavior 表示动画怎么重复. 可取值有 3 种: 1. 重复次数, 如 2x, 5x; 2. 一个时间, 例如 0:0:10 表示动画持续 10 秒; 3. Forever, 表示保持重复.
  • SpeedRatio 相对父元素的速度比. 例如为 2 就表示速度比父元素的快一倍. 可用在一个 Storyboard 中有多个动画的时候加以控制.

然后作者设计了一个赛马的案例:

image-20250729210738128

<Storyboard>
  <DoubleAnimation Duration="0:0:2" From="5" To="500"
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="(Canvas.Left)"/>
  <DoubleAnimation Duration="0:0:2" From="5" To="500"
                   AccelerationRatio="0.4"
                   DecelerationRatio="0.2"
                   Storyboard.TargetName="img2"
                   Storyboard.TargetProperty="(Canvas.Left)"/>
</Storyboard>

Animation 有一些额外的属性, 不从 Storyboard 继承:

  • From 表示动画中的属性值从什么开始.
  • To 表示动画结束的值.
  • By 用于表示属性值的增量, 即在动画结束时, 属性值与原始值的变化是多少.

似乎 By 与 To 是有冲突的.

From 如果不指定, 则表示动当前值开始变化.

动画的类型

动画就是按照一个时间规律修改某个属性的值, 属性是有类型的, 动画的类型依赖于属性的类型.

WPF 包含的类型有:

Boolean, Byte, Char, Color, Decimal, Double, Int16, Int32, Int64, Matrix, Object, Point, Point3D, Quaternion, Rect, Rotation3D, Single, Size, String, Thickness, Vector, Vector3D.

每一个类型对应了两种动画:

  • Animation 后缀, 表示线性动画. 数据变化是从某个值开始到另一个值.
  • AnimationUsingKeyFrame 后缀, 表示关键帧动画. 数据从某个值开始到另一个值的过程中, 一定会先到某个指定的值. 这个指定的值即为关键帧, 也可以是各种类型的值. 不同类型决定了不同的变化.

大多数关键帧的动画在数值变化范围内都是线性的, 样条曲线的, 或离散的.

下面逐个说明: 线性的, 样条的, 离散的, 以及 path 的.

简单线性动画

线性动画使用简单的方式将数值从一个值变化到另一个值. 例如:

<Storyboard>
  <DoubleAnimation Duration="0:0:2" From="5" To="500"
                   Storyboard.TargetName="img1"
                   Storyboard.TargetProperty="(Canvas.Left)">
  </DoubleAnimation>
  <DoubleAnimation Duration="0:0:2" From="5" To="500"
                   AccelerationRatio="0.4"
                   DecelerationRatio="0.2"
                   Storyboard.TargetName="img2"
                   Storyboard.TargetProperty="(Canvas.Left)">
  </DoubleAnimation>
</Storyboard>

两个动画的区别在于, 第二个动画有一个加速度, 和一个减速度的效果. 而第一个动画是匀速的.

如果指定了 Storyboard.TargetName, 那么就是独有的, 如果找不到控件会报错. 不设置表示可以用于多个控件.

  • 单独的定义 Storyboard.
  • 定义 Style, 在其中定义 EventTrigger, 来启用动画.
  • 或将 EventTrigger 定义在控件的 Triggers 中.

线性关键帧 (Linear Key Frames)

使用方法与线性动画类似:

  • 使用 AnimationUsingKeyFrames 后缀的动画类型.
  • 设置 Storyboard.Target*** 来指定修改什么控件的什么属性.
  • 在标签中提供 LinearDoubleKeyFrame, 并设置 Value 和 KeyTime, 表明在什么时刻要变成什么值.

例如:

<Storyboard>
  <DoubleAnimationUsingKeyFrames Storyboard.TargetName="btnMover"
                                 Storyboard.TargetProperty="(Canvas.Left)">
    <LinearDoubleKeyFrame Value="10" KeyTime="0:0:1.5"></LinearDoubleKeyFrame>
    <LinearDoubleKeyFrame Value="100" KeyTime="0:0:2.5"></LinearDoubleKeyFrame>
    <LinearDoubleKeyFrame Value="10" KeyTime="0:0:4"></LinearDoubleKeyFrame>
  </DoubleAnimationUsingKeyFrame>
</Storyboard>

样条关键帧 (Spline Key Frames)

样条关键帧是一个由点控制的曲线. 其变化与控制点如图所示

image-20250730112950023

有点像贝塞尔曲线. 然后作者解释了这个变化曲线的作用, 以及数据改变的效果特征.

要使用这个效果, 只需要提供 KeySpline 属性, 它有提供两个点 (控制点) 的规范化坐标, 即取值在 0 到 1 之间. 例如:

<Storyboard x:Key="a3">
    <DoubleAnimationUsingKeyFrames Storyboard.TargetProperty="Canvas.Left">
        <SplineDoubleKeyFrame Value="300" KeyTime="0:0:5" 
                              KeySpline="0.5,0 0.5,1"></SplineDoubleKeyFrame>
        <SplineDoubleKeyFrame Value="600" KeyTime="0:0:7" 
                              KeySpline="0.5,0 0.5,1"></SplineDoubleKeyFrame>
    </DoubleAnimationUsingKeyFrames>
</Storyboard>

经测试 TargetProperty 需要括号.

离散关键帧

线性与样条关键帧使得动画的变换都是连续的, 离散关键帧会使属性的变化是跳跃式的.

使用 DiscreteDoubleKeyFrame 代替 SplineDoubleKeyFrame 即可.

路径动画 (Path Animation)

依照路径的定义与方向来变化.

  • 定义 PathGeometry, 使用 Figures 属性来定义路径.

  • 使用 AnimationUsingPath 后缀的动画.

  • 需要设置 Source 来确定使用什么分量.

例如:

<PathGeometry x:Key="pathMove" 
              Figures="M 10,85
                       A 100,70 0 1 1 210,85
                       A 100,70 0 1 0 410,85
                       A 130,70 0 1 0 150,85
                       A 70, 70 0 1 1 10,85" />
<Storyboard x:key="sbMoveButton" RepeatBehavior="Forever">
  <DoubleAnimationUsingPath Duration="0:0:4"
                            Storyboard.TargetName="btnMover"
                            Storyboard.TargetProperty="(Canvas.Left)"
                            Source="X"
                            PathGeometry="{StaticResource pathMove}" />
  <DoubleAnimationUsingPath Duration="0:0:4"
                            Storyboard.TargetName="btnMover"
                            Storyboard.TargetProperty="(Canvas.Top)"
                            Source="Y"
                            PathGeometry="{StaticResource pathMove}" />
  <DoubleAnimationUsingPath Duration="0:0:4"
                            Storyboard.TargetName="btnMover"
                            Storyboard.TargetProperty="RenderTransform.Angle"
                            Source="Angle"
                            PathGeometry="{StaticResource pathMove}" />
</Storyboard>

混合与匹配关键帧

就是说在一个 Storyboard 中可以根据时间定义不同的动画对象.

特殊情况

WPF 给下列动画类型提供了大多数类型的类

  • 简单动画 (线性动画)
  • 线性关键帧动画
  • 样条关键帧动画
  • 离散关键帧动画

除此之外, 对于 String, Boolean, Char 的类型, 仅提供了离散关键帧动画. 很显然, banana 无法线性的变成 time.

控制 Storyboard

  • BeginStoryboard, 开始运行.
  • PauseStoryboard, 暂停.
  • SeekStoryboard, 将 Storyboard 设置为时间线上的一个特殊值.
  • StopStoryboard 停止,逻辑上可以理解为重置.
  • RemoveStoryboard, 停止并释放资源.

使用 EventTrigger 将控制与 按钮关联起来. 有一个盲区, 就是 EventTrigger 的 SourceName 属性. 经验证, 定义在与按钮同级别的容器中的 Trigger, 使用 按钮的 Name 作为 SourceName, 用来控制是由那个按钮来响应对应的 Trigger.

一般来说动画 Trigger 与样式分开定义.

MEDIA 与 TIMELINE

BeginStoryboard, PauseStoryboard 等这类控制类都是对动作的包装, 用于执行动作行为. 不同于一些物理对象, 如按钮, 文本等. 还有一个有用的包装动作类为 SoundPlayerAction, 它用于播放声音.

<Button Width="100" Height="50" Content="播放">
    <Button.Triggers>
        <EventTrigger RoutedEvent="Button.Click">
            <EventTrigger.Actions>
                <SoundPlayerAction Source="002-夜曲 - 周杰伦.wav" />
            </EventTrigger.Actions>
        </EventTrigger>
    </Button.Triggers>
</Button>

需要注意的是资源的属性

image-20250730154357277

这种似乎没办法精细控制.

也可以在 Storyboard 内部播放声音. 只需要在 Window 中添加 MediaElement 控件即可. 然后在 Storyboard 中添加 MediaTimeline 对象, 设置其 TargetName 为 MediaElement 对象, 以及 Source 属性为音频文件 (以内容形式存在).

 <Grid>
     <Grid.Resources>
         <Storyboard x:Key="p" RepeatBehavior="Forever">
            <MediaTimeline BeginTime="0:0:1" 
                           Storyboard.TargetName="player" 
                           Source="002-夜曲 - 周杰伦.wav" />
         </Storyboard>
     </Grid.Resources>
     
     <MediaElement Grid.Row="0" x:Name="player" />

     <Button x:Name="btn" Height="50" Width="100"  Content="播放2" Grid.Row="1">
         <Button.Triggers>
             <EventTrigger RoutedEvent="Button.Click">
                 <EventTrigger.Actions>
                     <BeginStoryboard Storyboard="{StaticResource p}" />
                 </EventTrigger.Actions>
             </EventTrigger>
         </Button.Triggers>
     </Button>
 </Grid>

image-20250730162704234

MediaElement 是一个看不见的元素.

注意, 如果 Storyboard 的 AutoReverse 为 true, 则无法播放媒体文件. 可以将媒体播放单独剥离出来处理, 让其他元素可以 Reverse 即可.

然后作者演示了一个弹球的案例, 利用 AutoReverse 来使得弹球与影子反复变化.

在案例中引入了 ParallelTimeline 组件, 允许在 Storyboard 中同时运行不同的时间线.

不使用 Storyboard 来实现动画

动画都是通过 XAML 来定义的, 如果是后台 C# 代码, 基本逻辑与标签结构一直, 需要手动创建对应的对象, 来执行代码.

很显然不建议这么实现, 太繁琐.

简单动画

即使简单的动画也需要繁琐的代码, 所以推荐使用工具来创建动画.

小结

略

Last Updated:
Contributors: jk