Condividi tramite


Working with Animations Programmatically

Microsoft Silverlight will reach end of support after October 2021. Learn more.

There are times when you may want to change the properties of an animation dynamically (on the fly). For example, you might want to adjust the behavior of an animation that is applied to an object, depending on the object's current location in the layout, what kind of content the object contains, and so on. You can manipulate animations dynamically by using procedural code (for example, C# or Visual Basic).

This topic contains the following sections.

  • Prerequisites
  • Accessing Animations by Name
  • Dynamically Changing TargetName
  • Creating an Animation in Procedural Code
  • Related Topics

Prerequisites

You should be familiar with Silverlight animations. For an introduction, see Animation Overview.

Accessing Animations by Name

The most direct way to access an animation object to change its properties is to give the animation object a name and then reference it by that name in code. The following example consists of an Ellipse that animates to wherever you click on the screen. To accomplish this animation, an event handler changes the To property of the PointAnimation object when the Canvas is clicked, and then it starts the animation.

Run this sample

<Canvas MouseLeftButtonDown="Handle_MouseDown"
  Background="Gray" Width="600" Height="500">
    <Canvas.Resources>
      <Storyboard x:Name="myStoryboard">

        <!-- The PointAnimation has a name so it can be accessed
             from code. The To property is left out of the XAML
             because the value of To is determined in code. -->
        <PointAnimation
          x:Name="myPointAnimation"
          Storyboard.TargetProperty="Center"
          Storyboard.TargetName="MyAnimatedEllipseGeometry"
          Duration="0:0:2"/>
      </Storyboard>
    </Canvas.Resources>

    <Path Fill="Blue">
      <Path.Data>

        <!-- Describes an ellipse. -->
        <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
         Center="200,100" RadiusX="15" RadiusY="15" />
      </Path.Data>
    </Path>

</Canvas>
Private Sub Handle_MouseDown(ByVal sender As Object, ByVal e As MouseButtonEventArgs)
    ' Retrieve current mouse coordinates.
    Dim newX As Double = e.GetPosition(Nothing).X
    Dim newY As Double = e.GetPosition(Nothing).Y
    Dim myPoint As Point = New Point
    myPoint.X = newX
    myPoint.Y = newY
    myPointAnimation.To = myPoint
    myStoryboard.Begin()
End Sub
private void Handle_MouseDown(object sender, MouseButtonEventArgs e)
{
    // Retrieve current mouse coordinates.
    double newX = e.GetPosition(null).X;
    double newY = e.GetPosition(null).Y;
    Point myPoint = new Point();
    myPoint.X = newX;
    myPoint.Y = newY;
    myPointAnimation.To = myPoint;
    myStoryboard.Begin();
}

It might not always be practical or convenient to have unique names for all your animations. Alternatively, you can access animations or keyframes of animations by using collections. For example, if you want to programmatically access all the keyframes within a DoubleAnimationUsingKeyFrames object, you could use code similar to the following.

<Canvas x:Name="parentCanvas" xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  Width="600" Height="500" Background="Gray">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <PointAnimationUsingKeyFrames
        x:Name="myPointAnimationUsingKeyFrames"
        Storyboard.TargetProperty="Center"
        Storyboard.TargetName="MyAnimatedEllipseGeometry"
        Duration="0:0:3">

          <!-- Set of keyframes -->
          <DiscretePointKeyFrame KeyTime="0:0:0" />
          <LinearPointKeyFrame KeyTime="0:0:0.5" />
          <SplinePointKeyFrame KeySpline="0.6,0.0 0.9,0.00" KeyTime="0:0:3" />
      </PointAnimationUsingKeyFrames>
    </Storyboard>

  </Canvas.Resources>

  <Path Fill="Blue">
    <Path.Data>

      <!-- Describes an ellipse. -->
      <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
        Center="200,100" RadiusX="15" RadiusY="15" />
    </Path.Data>
  </Path>
</Canvas>
public void Handle_MouseDown(object sender, MouseEventArgs e)
{
    int i;
    for (i = 0; i < myPointAnimationUsingKeyFrames.KeyFrames.Count; i++)
    {
        // Do something with each keyframe; for example, set values.
    }
}

The following example is similar to the previous one in that the ellipse follows where the user clicks on the screen, except that keyframes are used in this example. The collection of keyframes is iterated, and values are dynamically set on the keyframes so that the ellipse animates to the proper location.

Run this sample

      <Canvas MouseLeftButtonDown="Handle_MouseDown"
  Width="600" Height="500" Background="Gray">
  <Canvas.Resources>
    <Storyboard x:Name="myStoryboard">
      <PointAnimationUsingKeyFrames
        x:Name="myPointAnimationUsingKeyFrames"
        Storyboard.TargetProperty="Center"
        Storyboard.TargetName="MyAnimatedEllipseGeometry"
        Duration="0:0:3">

        <DiscretePointKeyFrame KeyTime="0:0:0" />
        <LinearPointKeyFrame KeyTime="0:0:0.5" />
        <SplinePointKeyFrame KeySpline="0.6,0.0 0.9,0.00" KeyTime="0:0:2" />

      </PointAnimationUsingKeyFrames>

    </Storyboard>
  </Canvas.Resources>

  <Path Fill="Blue">
    <Path.Data>

      <!-- Describes an ellipse. -->
      <EllipseGeometry x:Name="MyAnimatedEllipseGeometry"
        Center="200,100" RadiusX="15" RadiusY="15" />
    </Path.Data>
  </Path>
</Canvas>
' Global variables that keep track of the end point
' of the last animation.
Private lastX As Double = 200
Private lastY As Double = 100

Private Sub Handle_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs)
    ' Retrieve current mouse coordinates.
    Dim newX As Double = e.GetPosition(Nothing).X
    Dim newY As Double = e.GetPosition(Nothing).Y
    Dim i As Integer
    i = 0
    Do While (i < myPointAnimationUsingKeyFrames.KeyFrames.Count)
        Dim keyFrame As PointKeyFrame = myPointAnimationUsingKeyFrames.KeyFrames(i)
        If (keyFrame.GetType.Name = "DiscretePointKeyFrame") Then
            keyFrame.SetValue(DiscretePointKeyFrame.ValueProperty, New Point(lastX, lastY))
        ElseIf (keyFrame.GetType.Name = "LinearPointKeyFrame") Then
            ' The LinearKeyFrame has a value that is part way to the 
            ' final end point. In addition, this value has to be on
            ' the correct line, therefore, you need to use the line 
            ' formula y = mx + b to find the values of x and y.
            ' Calculate the slope
            Dim m As Double = ((newY - lastY) / (newX - lastX))
            ' Calculate the y-intercept.
            Dim b As Double = (newY - (m * newX))
            ' Set X to a third of the way to the end point.
            Dim intermediateX As Double = (lastX + ((newX - lastX) / 3))
            ' Find the value Y from X and the line formula.
            Dim intermediateY As Double = ((m * intermediateX) + b)
            ' Set the keyframe value to the intermediate x and y value.
            keyFrame.SetValue(LinearPointKeyFrame.ValueProperty, New Point(intermediateX, intermediateY))
        ElseIf (keyFrame.GetType.Name = "SplinePointKeyFrame") Then
            keyFrame.SetValue(SplinePointKeyFrame.ValueProperty, New Point(newX, newY))
        End If
        i = (i + 1)
    Loop
    myStoryboard.Stop()
    myStoryboard.Begin()
    lastX = newX
    lastY = newY
End Sub
// Global variables that keep track of the end point
// of the last animation.
double lastX = 200;
double lastY = 100;
private void Handle_MouseDown(object sender, MouseEventArgs e)
{
    // Retrieve current mouse coordinates.
    double newX = e.GetPosition(null).X;
    double newY = e.GetPosition(null).Y;

    int i;
    for (i = 0; i < myPointAnimationUsingKeyFrames.KeyFrames.Count; i++)
    {
        PointKeyFrame keyFrame = myPointAnimationUsingKeyFrames.KeyFrames[i];
        if (keyFrame.GetType().Name == "DiscretePointKeyFrame")
        {
            keyFrame.SetValue(DiscretePointKeyFrame.ValueProperty, new Point(lastX, lastY));
        }
        else if (keyFrame.GetType().Name == "LinearPointKeyFrame")
        {

            // The LinearKeyFrame has a value that is part way to the 
            // final end point. In addition, this value has to be on
            // the correct line; therefore, you need to use the line 
            // formula y = mx + b to find the values of x and y.

            // Calculate the slope.
            double m = (newY - lastY) / (newX - lastX);

            // Calculate the y-intercept.
            double b = newY - (m * newX);

            // Set X to a third of the way to the end point.
            double intermediateX = lastX + (newX - lastX) / 3;

            // Find the value Y from X and the line formula.
            double intermediateY = (m * intermediateX) + b;

            // Set the keyframe value to the intermediate x and y value.
            keyFrame.SetValue(LinearPointKeyFrame.ValueProperty, 
                new Point(intermediateX, intermediateY));
        }
        else if (keyFrame.GetType().Name == "SplinePointKeyFrame")
        {
            keyFrame.SetValue(SplinePointKeyFrame.ValueProperty, new Point(newX, newY));
        }
    }
    myStoryboard.Stop();
    myStoryboard.Begin();
    lastX = newX;
    lastY = newY;
}
NoteNote:

Storyboard has a Children property that enables you to access all the animation objects in a given Storyboard.

Dynamically Changing TargetName

The most common scenario for dynamically changing the Storyboard.TargetName property is when you want to apply the same animation to more than one object. This is especially useful when you have a large number of objects that have similar animations applied to them. For example, you might be displaying rows of images and want to use an animation to highlight the image that currently has the mouse pointer over it. It is inconvenient and messy to create separate Storyboard objects for each image. It is better to reuse the same Storyboard.

The following example has a number of rectangles that fade out and back into sight when you click them. All of these rectangles use the same Storyboard, because the DoubleAnimation that animates the Opacity changes its TargetName to whichever rectangle is clicked.

Run this sample

<StackPanel Orientation="Horizontal">
  <StackPanel.Resources>
    <Storyboard x:Name="myStoryboard">
      <DoubleAnimation x:Name="myDoubleAnimation"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2"
        AutoReverse="True" />
    </Storyboard>
  </StackPanel.Resources>
  <Rectangle
    x:Name="MyAnimatedRectangle1"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle
    x:Name="MyAnimatedRectangle2"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle
    x:Name="MyAnimatedRectangle3"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle
    x:Name="MyAnimatedRectangle4"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />
</StackPanel>
Private Sub Start_Animation(ByVal sender As Object, ByVal e As MouseEventArgs)
    ' If the Storyboard is running and you try to change
    ' properties of its animation objects programmatically, 
    ' an error will occur.
    myStoryboard.Stop()
    ' Get a reference to the rectangle that was clicked.
    Dim myRect As Rectangle = CType(sender, Rectangle)
    ' Change the TargetName of the animation to the name of the
    ' rectangle that was clicked.
    myDoubleAnimation.SetValue(Storyboard.TargetNameProperty, myRect.Name)
    ' Begin the animation.
    myStoryboard.Begin()
End Sub
private void Start_Animation(object sender, MouseEventArgs e)
{

    // If the Storyboard is running and you try to change
    // properties of its animation objects programmatically, 
    // an error will occur.
    myStoryboard.Stop();

    // Get a reference to the rectangle that was clicked.
    Rectangle myRect = (Rectangle)sender;

    // Change the TargetName of the animation to the name of the
    // rectangle that was clicked.
    myDoubleAnimation.SetValue(Storyboard.TargetNameProperty, myRect.Name);

    // Begin the animation.
    myStoryboard.Begin();
}

In the preceding code, notice that you must stop the Storyboard before you dynamically change the properties of its animation objects; otherwise, an error will occur. In this example, it might not be desirable to stop an animation on one rectangle so that the animation can start on another rectangle. Perhaps you want both animations to run at the same time. However, you cannot use the same animation object to run two separate animations at the same time, because there is only one TargetName. This does not mean that you are back to creating a separate Storyboard for every object. Instead, you need one Storyboard for each animation that you want to run concurrently (synchronously). The following example is similar to the previous one, except that it contains three Storyboard objects instead of one. When you click a rectangle, the event handler looks for a Storyboard that is not currently in use and uses that one to create the animation.

Run this sample

<StackPanel Orientation="Horizontal">
  <StackPanel.Resources>
    <Storyboard x:Name="myStoryboard1" Completed="Storyboard_Completed">
      <DoubleAnimation x:Name="myDoubleAnimation1"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2" AutoReverse="True" />
    </Storyboard>
    <Storyboard x:Name="myStoryboard2" Completed="Storyboard_Completed">
      <DoubleAnimation x:Name="myDoubleAnimation2"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2"
        AutoReverse="True" />
    </Storyboard>
    <Storyboard x:Name="myStoryboard3" Completed="Storyboard_Completed">
      <DoubleAnimation x:Name="myDoubleAnimation3"
        Storyboard.TargetProperty="Opacity"
        From="1.0" To="0.0" Duration="0:0:2"
        AutoReverse="True" />
    </Storyboard>
  </StackPanel.Resources>
  <Rectangle x:Name="MyAnimatedRectangle1"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle x:Name="MyAnimatedRectangle2"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle x:Name="MyAnimatedRectangle3"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />

  <Rectangle x:Name="MyAnimatedRectangle4"
    Margin="3" Width="100" Height="100" Fill="Blue"
    MouseLeftButtonDown="Start_Animation" />
</StackPanel>
Private storyboard1Active As Boolean = False
Private storyboard2Active As Boolean = False
Private storyboard3Active As Boolean = False

Private Sub Start_Animation(ByVal sender As Object, ByVal e As MouseEventArgs)
    ' Get a reference to the rectangle that was clicked.
    Dim myRect As Rectangle = CType(sender, Rectangle)
    If Not storyboard1Active Then
        myStoryboard1.Stop()
        myDoubleAnimation1.SetValue(Storyboard.TargetNameProperty, myRect.Name)
        myStoryboard1.Begin()
        storyboard1Active = True
    ElseIf Not storyboard2Active Then
        myStoryboard2.Stop()
        myDoubleAnimation2.SetValue(Storyboard.TargetNameProperty, myRect.Name)
        myStoryboard2.Begin()
        storyboard2Active = True
    ElseIf Not storyboard3Active Then
        myStoryboard3.Stop()
        myDoubleAnimation3.SetValue(Storyboard.TargetNameProperty, myRect.Name)
        myStoryboard3.Begin()
        storyboard3Active = True
    End If
End Sub

Private Sub Storyboard_Completed(ByVal sender As Object, ByVal e As EventArgs)
    Dim myStoryboard As Storyboard = CType(sender, Storyboard)
    Select Case (myStoryboard.GetValue(NameProperty).ToString)
        Case "myStoryboard1"
            storyboard1Active = False
        Case "myStoryboard2"
            storyboard2Active = False
        Case "myStoryboard3"
            storyboard3Active = False
    End Select
End Sub
bool storyboard1Active = false;
bool storyboard2Active = false;
bool storyboard3Active = false;

private void Start_Animation(object sender, MouseEventArgs e)
{
    // Get a reference to the rectangle that was clicked.
    Rectangle myRect = (Rectangle)sender;
    if (!storyboard1Active)
    {
        myStoryboard1.Stop();
        myDoubleAnimation1.SetValue(Storyboard.TargetNameProperty, myRect.Name);
        myStoryboard1.Begin();
        storyboard1Active = true;
    }
    else if (!storyboard2Active)
    {
        myStoryboard2.Stop();
        myDoubleAnimation2.SetValue(Storyboard.TargetNameProperty, myRect.Name);
        myStoryboard2.Begin();
        storyboard2Active = true;
    }
    else if (!storyboard3Active)
    {
        myStoryboard3.Stop();
        myDoubleAnimation3.SetValue(Storyboard.TargetNameProperty, myRect.Name);
        myStoryboard3.Begin();
        storyboard3Active = true;
    }
}

private void Storyboard_Completed(object sender, EventArgs e)
{
    Storyboard myStoryboard = sender as Storyboard;
    switch (myStoryboard.GetValue(NameProperty).ToString())
    {
        case "myStoryboard1": storyboard1Active = false; break;

        case "myStoryboard2": storyboard2Active = false; break;

        case "myStoryboard3": storyboard3Active = false; break;
    }
}

In the preceding example, only three animations can run at the same time (equal to the number of Storyboard objects). This is fine if you do not anticipate a need for more concurrent animations, which would require more Storyboard objects. If you expect a lot of independent animations to be running at the same time, you might want to create your Storyboard objects dynamically. For an example of creating storyboard objects in code, see the next section.

Creating an Animation in Procedural Code

You can also create an animation completely in procedural code. The following example shows how to create an animation that animates Canvas.Top and Canvas.Left attached properties of a rectangle.

Run this sample

Private Sub Create_And_Run_Animation(ByVal sender As Object, ByVal e As EventArgs)
    ' Create a red rectangle that will be the target
    ' of the animation.
    Dim myRectangle As Rectangle = New Rectangle
    myRectangle.Width = 200
    myRectangle.Height = 200
    Dim myColor As Color = Color.FromArgb(255, 255, 0, 0)
    Dim myBrush As SolidColorBrush = New SolidColorBrush
    myBrush.Color = myColor
    myRectangle.Fill = myBrush
    ' Add the rectangle to the tree.
    LayoutRoot.Children.Add(myRectangle)
    ' Create a duration of 2 seconds.
    Dim duration As Duration = New Duration(TimeSpan.FromSeconds(2))
    ' Create two DoubleAnimations and set their properties.
    Dim myDoubleAnimation1 As DoubleAnimation = New DoubleAnimation
    Dim myDoubleAnimation2 As DoubleAnimation = New DoubleAnimation
    myDoubleAnimation1.Duration = duration
    myDoubleAnimation2.Duration = duration
    Dim sb As Storyboard = New Storyboard
    sb.Duration = duration
    sb.Children.Add(myDoubleAnimation1)
    sb.Children.Add(myDoubleAnimation2)
    Storyboard.SetTarget(myDoubleAnimation1, myRectangle)
    Storyboard.SetTarget(myDoubleAnimation2, myRectangle)
    ' Set the attached properties of Canvas.Left and Canvas.Top
    ' to be the target properties of the two respective DoubleAnimations
    Storyboard.SetTargetProperty(myDoubleAnimation1, New PropertyPath("(Canvas.Left)"))
    Storyboard.SetTargetProperty(myDoubleAnimation2, New PropertyPath("(Canvas.Top)"))
    myDoubleAnimation1.To = 200
    myDoubleAnimation2.To = 200
    ' Make the Storyboard a resource.
    LayoutRoot.Resources.Add("unique_id", sb)
    ' Begin the animation.
    sb.Begin()
End Sub
private void Create_And_Run_Animation(object sender, EventArgs e)
{
    // Create a red rectangle that will be the target
    // of the animation.
    Rectangle myRectangle = new Rectangle();
    myRectangle.Width = 200;
    myRectangle.Height = 200;
    Color myColor = Color.FromArgb(255, 255, 0, 0);
    SolidColorBrush myBrush = new SolidColorBrush();
    myBrush.Color = myColor;
    myRectangle.Fill = myBrush;

    // Add the rectangle to the tree.
    LayoutRoot.Children.Add(myRectangle);

    // Create a duration of 2 seconds.
    Duration duration = new Duration(TimeSpan.FromSeconds(2));

    // Create two DoubleAnimations and set their properties.
    DoubleAnimation myDoubleAnimation1 = new DoubleAnimation();
    DoubleAnimation myDoubleAnimation2 = new DoubleAnimation();

    myDoubleAnimation1.Duration = duration;
    myDoubleAnimation2.Duration = duration;

    Storyboard sb = new Storyboard();
    sb.Duration = duration;

    sb.Children.Add(myDoubleAnimation1);
    sb.Children.Add(myDoubleAnimation2);

    Storyboard.SetTarget(myDoubleAnimation1, myRectangle);
    Storyboard.SetTarget(myDoubleAnimation2, myRectangle);

    // Set the attached properties of Canvas.Left and Canvas.Top
    // to be the target properties of the two respective DoubleAnimations.
    Storyboard.SetTargetProperty(myDoubleAnimation1, new PropertyPath("(Canvas.Left)"));
    Storyboard.SetTargetProperty(myDoubleAnimation2, new PropertyPath("(Canvas.Top)"));

    myDoubleAnimation1.To = 200;
    myDoubleAnimation2.To = 200;

    // Make the Storyboard a resource.
    LayoutRoot.Resources.Add("unique_id", sb);

    // Begin the animation.
    sb.Begin();
}
NoteNote:

Do not attempt to call Storyboard members (for example, the Begin method) in the constructor of the page. This will cause your animation to fail silently.