Creating 2-D and 3-D Dynamic Animations in Windows Presentation Foundation

 

Karsten Januszewski
Microsoft Corporation

January 2005
Updated June 2006

Summary: Karsten shows how to build an animated version of Task Manager using the 2-D animation infrastructure provided by Windows Presentation Foundation, then moves into the world of 3-D, showing how to create a dynamically animated 3-D mesh. (12 printed pages)

Applies to:
   Vista/WinFX Beta 2

Download the AvalonTaskMgr.msi sample file.

Contents

Getting Started
Accessing the CPU Performance Counter Data and Adding the Line Graph
Adding Animation
Adding an Animated Bar Graph
Conclusion

One of the more interesting scenarios and rationales for using Windows Presentation Foundation (WPF) is for the purpose of data visualization. Using the animation features of WPF opens up the possibility of representing information in a more visceral, intuitive way. One data visualization that most Windows users have experienced at one time or another is a graphical representation of their CPU utilization using Task Manager. As a way to demonstrate data visualization, I decided to implement this data visualization using WPF. In doing so, I highlight several features of the WPF platform. First, I'll show how easy it is to create a graph in WPF using the drawing brush. Second, the simplicity and elegance of the animation programming model will become evident. Third, I'll show a technique for doing dynamic animations, where the animation is governed by data that is captured on the fly by the application. Finally, I'll briefly step into the world of 3-D, showing how to create a dynamically animated 3-D mesh. Here's a screenshot of the final application:

Aa480159.avalon2d-3d01(en-us,MSDN.10).jpg

Getting Started

The first thing for the application is a layout framework. Because I have prevented resizing of the application by setting Window.ResizeMode to NoResize, I used a Canvas for placing my controls, as I don't need any of the resizing features of other WPF layout controls, like StackPanel or Grid. There are only four controls on the canvas: a TextBlock, a Viewport3d and two canvases for the graphs. Each control has been fixed on the canvas by setting its Canvas.Left property.

The next step will be to add the graph—horizontal and vertical lines that will provide visual coordinates. It turns out that WPF has a very handy feature for creating graphs, using the DrawingBrush control, which fills an area with a drawing. The drawing itself can be composed of different WPF shapes, such as rectangles, ellipses, and so on. The most important property of the DrawingBrush is the Drawing property, which is where you establish the shapes used by your brush. In this case, the shape used is simply two lines that, in combination, will form a graph.

Another important feature of the DrawingBrush is the ability to create tiles. The DrawingBrush supports a TileMode property that takes an enumeration. By setting TileMode to Tile, you can tile a surface repetitively. In such a way, you can declare a drawing that gets tiled iteratively. By setting the Viewport of the DrawingBrush, the size of each tile is established. Then, by setting the background of a canvas to this DrawingBrush, the entirety of the canvas will be painted with tiles. So, to create a green grid, the brush will have two lines, one for the vertical line and one for the horizontal line, as follows:

<DrawingBrush x:Name="gridBackgroundBrush" 
        Viewport="0,0,10,10" 
        ViewportUnits="Absolute"
        TileMode="Tile">
  <DrawingBrush.Drawing>
        <DrawingGroup>
          <DrawingGroup.Children>
            <GeometryDrawing Geometry="M0,0 L1,0 1,0.1, 0,0.1Z" Brush="Green" />
            <GeometryDrawing Geometry="M0,0 L0,1 0.1,1, 0.1,0Z" Brush="Green" />
          </DrawingGroup.Children>
        </DrawingGroup>
  </DrawingBrush.Drawing>
</DrawingBrush>

The next step is to apply this brush to the background of the canvas. Because Background is a property of Canvas, we could nest the DrawingBrush beneath the Background property in XAML as follows:

  <Canvas.Background>
    <DrawingBrush x:Name="gridBackgroundBrush">...</DrawingBrush>
  </Canvas.Background>

However, because we may want to reuse this background, it is more efficient to create our drawing brush as a resource and then create a style for any canvas that needs that background:

<Window.Resources>
  <DrawingBrush x:Name="gridBackgroundBrush">...</DrawingBrush>
  <Style x:Name="myCanvasStyle">
    <Canvas Background="{gridBackgroundBrush}" />
  </Style>
</Window.Resources>

Then, we can set the background attribute with the style "myCanvasStyle".

Accessing the CPU Performance Counter Data and Adding the Line Graph

Now, with a graph available, I can start adding animations. First, I need to get the data that I will be viewing, in this case CPU utilization data. The .NET Framework provides a convenient class for accessing such data in the System.Diagnostics.PerformanceCounter. There are hundreds of different performance counters that I could mine using this class; I'm just going to grab the overall CPU percentage utilization.

Next, I need a timer that will fire incrementally that will be used to call the performance counter. WPF provides such a timer, called the UITimer, in the System.Timer namespace. Whenever the application wants to get the current CPU utilization, calling the NextValue() method from the performance counter will provide the value. I will set the timer to fire every half-second. Wiring up the Tick event from the timer allows me to update the animation based on the new CPU percentage.

Note that with Windows Vista, getting diagnostic information with this API requires elevated permissions. There are a few methods for acquiring those elevated permissions. The most brute force way—and the most inelegant—is to right-click the application and "Run As Administrator". However, some users may not know this trick or know that the reason the application is failing is because it hasn't been granted the right permissions. A more elegant way to get elevated privileges is to launch the standard Windows Vista dialog for requesting elevated permissions. I did this by adding a side-by-side manifest with the application, which requests the elevated permissions by specifying the requestedExecutionLevel with the requireAdministrator value. I've provided the entire manifest below for your convenience:

<?xml version="1.0" encoding="utf-8" ?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <assemblyIdentity version="1.0.0.0" 
      processorArchitecture="X86"
      name="WPF Performance Monitor"
      type="win32" />
  <description>WPF Performance Monitor</description>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
    <security>
      <requestedPrivileges>
        <requestedExecutionLevel level="requireAdministrator" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</assembly>

With this addition, two things will happen to the application. First, a special icon will be associated with the application that indicates to the user it requires special privileges. Second, when the application is launched, the user will be prompted. Screenshots of both are provided below:

Aa480159.avalon2d-3d02(en-us,MSDN.10).gif

Aa480159.avalon2d-3d03(en-us,MSDN.10).gif

It is worth noting a few other things. First, there is a way to embed this manifest information into the .exe itself as opposed to the side-by-side methodology I used. Expect forthcoming documentation on this shortly. Second, adding this manifest information will not affect the application's ability to run on Windows XP. Lastly, adding the manifest will not help with the debugging experience. The only way to debug an application that requires elevated permissions is to run Visual Studio itself with elevated permissions so that when Visual Studio spawns a process to run your application, it will have elevated privileges. As such, on Windows Vista, you will need to run Visual Studio with elevated permissions in order to debug this project.

I will initialize these controls as well as an X and Y int in the Loaded event of the window by wiring up the Loaded event of the Window in XAML and a correlating event handler in code.

public partial class Window1 : Window
{
private int x = 0;
private int y = 0;
private int cpuInt;
private System.Diagnostics.PerformanceCounter performanceCounter;
private System.Threading.UITimer uiTimer;

private void WindowLoaded(object sender, EventArgs e) 
{

  //initialize perf counter
  performanceCounter = new System.Diagnostics.PerformanceCounter("Processor", "% Processor Time", "_Total");
  //start the uitimer
  uiTimer = new System.Threading.UITimer();
  uiTimer.Interval = new TimeSpan(5000000);
  uiTimer.Tick += new EventHandler(uiTimer_Tick);
  uiTimer.Start();

}

void uiTimer_Tick(object sender, EventArgs e)
{
  cpuInt = Convert.ToInt32(performanceCounter.NextValue());
  Do2DAnimation();
  CpuText.TextContent = cpuInt.ToString();
  y = cpuInt;
  x = x + 1;
}
}

Now, I need to implement the code within my Do2DAnimation method. To create the line graph, we need to hold on to the prior percentage value (80 percent, for example) and draw a line to the new value (20 percent, for example). Our CPU percentage values thus are represented by the Y axis, with each pixel representing a percentage. Time will be represented by the X axis and every 10 pixels will represent a half second on the X axis.

One catch here is that zero on the Y axis starts at the top of the page, but we want 0 to start at the bottom and 100 to be at the top, as that is how we typically expect charts to work. As such, we will need to subtract the CPU value by 100 to get the corollary value. So, by setting X1, X2, Y1, and Y2 on a Line shape, each line segment would look as follows:

private void Do2DAnimation()
{


  //create a line with X2 and Y2 equivalent, so that it is really just a point.
  Line segmentAnimation = new Line();
  segmentAnimation.X1 = new System.Windows.Length(x * 10);
  segmentAnimation.Y1 = new System.Windows.Length(100 - y);
  segmentAnimation.X2 = new System.Windows.Length((x * 10) + 10);
  segmentAnimation.Y2 = new System.Windows.Length(100 - cpuInt);
  segmentAnimation.Stroke = System.Windows.Media.Brushes.LimeGreen;
  segmentAnimation.StrokeThickness = new System.Windows.Length(1);
  CpuGraphAnimation2d.Children.Insert(0, segmentAnimation);
}

Notice that I give the line a color by setting the Stroke property with a brush and specify its thickness with the StrokeThickness property. Lastly, I add the line to the visual tree by calling the Insert method on the canvas' children collection.

You could add the above code and have a working solution, which simply inserts the entire line upon each tick. However, there is a potential problem with this code, namely that it doesn't account for the end of the canvas. If you run the code, you will notice that WPF attempts to compensate for the fact that the line's X coordinate falls outside of the bounds of the canvas by creating more width for the canvas and pushing the other canvases to the left. I could potentially take advantage of this behavior by WPF, but I'd have another problem on my hands: I'd be creating an infinite amount of new lines, for as long as the application ran.

Consequently, it makes more sense to create a fixed number of lines that are based on the size of the screen and then recycle them as needed. As such, I will need to keep track of the X value and clear it when the line reaches the end of the canvas. Since the canvas is 200px, I know we will reach the end after 20 lines. With this methodology, I can update the code in our uiTimer_Tick method as follows:

void uiTimer_Tick(object sender, EventArgs e)
{
  cpuInt = Convert.ToInt32(performanceCounter.NextValue());
  Do2DAnimation();
  CpuText.TextContent = cpuInt.ToString();
  y = cpuInt;
  x = x + 1;
  if (x == 20)
  {
    CpuGraphAnimation2d.Children.RemoveRange(0, 20);
    x = 0;
  }
}

I also need to update the Loaded event and create an array of 20 lines that my graph will reuse, solving my earlier problem of creating an infinite number of lines, by adding them to an array of lines in my Loaded event:

for (int i = 0; i < 20; i++)
{
  Line line = new Line();
  line.Stroke = System.Windows.Media.Brushes.LimeGreen;
  line.StrokeThickness = new System.Windows.Length(1);
  lines[i] = line;
}

And finally, I can update the code with in the Do2DAnimation() method to use our array of lines instead of instantiating a new line each time:

//Line segmentAnimation = new Line();
Line segmentAnimation = lines[x];

Adding Animation

This is all well and good, but we are performing a clunky kind of animation, inserting entire lines into the graph. The visual effect is rather crude. What we would like to see is the points that make up the line actually drawn between the two coordinates, so that the resultant visual effect is much smoother. It turns out that adding this type of animation is pretty easy using the 2-D animation infrastructure provided by WPF. I recommend reading up on animation in the WinFX SDK to understand the basics.

Here's how I went about animating the lines. The first thing to do is change our X2 and Y2 to be the same as the X1 and Y1, so that when the line is added to the visual tree, only a point is drawn. We will then create a WPF animation using the actual X2 and Y2 values. I need to create two DoubleAnimation classes from the System.Windows.Media.Animation namespace. You might be wondering how I knew to create DoubleAnimation classes. I knew to create them because the datatype of the values I wanted animated—X2 and Y2—were both of type Double. If you browse the System.Windows.Media.Animation namespace, you will see that there are specific animation classes for many different object types, including ByteAnimation, BooleanAnimation, and so on. All of these Animation classes derive from the abstract AnimationTimeline class, providing a common paradigm for doing any kind of animation. At first, it may seem confusing to do animations this way, but the more I work with WPF, the more sense it makes. By providing this common paradigm, once you understand WPF animation, you can animate almost anything, reusing your skills.

So, to create the animations, I need to set some properties on my DoubleAnimation classes and then associate them with line itself. Typically, animation classes support the From, To, and By properties, which syntactically are used to specify how to animate from a value to a value by how much. In this case, I set only the To value, based on the endpoint of the line, in the constructor of the DoubleAnimation. The From and By values are interpolated for me. I also pass a Duration property, declaring the time span of the animation. In this case, Duration is set to half second, so that the animations will be in synchronization with the timer.

With these properties set on the DoubleAnimation classes, I now need to create a specific instance of this animation. I do so by calling CreateClock() method of my animations. I then apply that clock to a given FrameworkElement of the user interface, in this case the Line, using the ApplyAnimationClock() method, specifying the property I want to animate, such as Line.X2Property. The ApplyAnimationClock() method takes two arguments: the dependency property I seek to animate, in this case the Line.X2Property or Line.Y2Property, and the clock created from the animation. With my animations wired up, I can then control the clock itself by manipulating its Controller property.

Note that because I am recycling the lines, I have to clear the animations by setting them to null before adding them.

Here's the code:

//add animation to X
//add animation to X
segmentAnimationX = new DoubleAnimation((x * 10) + 10, new Duration(TIME_SPAN));
//null out prior animation
segment.ApplyAnimationClock(Line.X2Property, null); 
clockSegmentX = segmentAnimationX.CreateClock();
segment.ApplyAnimationClock(Line.X2Property, clockSegmentX); 

//add animation to Y
segmentAnimationY = new DoubleAnimation(100 - cpuInt, new Duration(TIME_SPAN));
//null out prior animation
segment.ApplyAnimationClock(Line.Y2Property, null);
clockSegmentY = segmentAnimationY.CreateClock();
segment.ApplyAnimationClock(Line.Y2Property, clockSegmentY);


//insert the line and begin animations
CpuGraphAnimation2d.Children.Insert(0, segment);
clockSegmentX.Controller.Begin();
clockSegmentY.Controller.Begin();

Voilà! The animated line looks much smoother than the jerky technique of simply inserting a line.

Adding an Animated Bar Graph

With the line graph out of the way, the next thing I need to tackle is replicating the bar graph that you see in Task Manager. Again, I can improve the experience of Task Manager by growing and shrinking the rectangle using WPF animations, as opposed to the way Task Manager does it by repainting the rectangle. I also will add some transparency to the rectangle, which antes up the visual appeal.

To create the bar graph, the Rectangle shape will work perfectly—well, almost. There is a catch with the Rectangle. The behavior I would like to see in the application is for the bar to move upward vertically. However, the WPF coordinate system moves downward on the Y axis and to the right on the X axis. So, in order to have our bar graph animate vertically upward, the rectangle must be rotated and repositioned. WPF has some handy classes to do this using standard transform classes, which can be applied to any element in a visual tree. In this case, I use the RotateTransform class to flip the rectangle 180 degrees and then reposition the rectangle on the canvas. So, within the initialization routine, I create the appropriate rotation and translation transforms, add them to a transform collection, create the rectangle, render the transforms with the RenderTransform method and finally add the rectangle to the visual tree. Animating the rectangle turns out to be very similar to animating the line. Again, I use a DoubleAnimation and apply it to the RectangleWidth property—remember, I rotated the rectangle 180 degrees.

At this point, I might declare victory, but why stop here? I would like to enhance this application with 3-D by creating a 3-D bar to animate instead of the 2-D rectangle. The Viewport3D will sit on top of the 2-D canvas.

The Viewport3D uses an OrthographicCamera to "look" at the models placed within the viewport. I chose to use an OrthographicCamera instead of a PerspectiveCamera because I wanted the column to remain fixed as it scaled. We then add a single DirectionalLight to illuminate our scene. I also provide an empty ModelVisual3D to which I will add a cylinder via code. Here's the XAML to set up our 3-D environment:

  <Viewport3D>
  <Viewport3D.Camera>
        <OrthographicCamera 
          Width="2.5"
          UpDirection="0,1,0"
          Position="0.0,-0.2,-5.0"
          LookDirection="0,0.05,1"
            />
      </Viewport3D.Camera>
      <ModelVisual3D>
        <ModelVisual3D.Content>
          <Model3DGroup >
            <DirectionalLight Color="White" Direction="-7, -4, -10" />
          </Model3DGroup>
        </ModelVisual3D.Content>
      </ModelVisual3D>
      <ModelVisual3D/>
    </Viewport3D>

To add the cylinder, I'm using a class called Mesh3DObjects that was created by a developer on the WPF team but isn't officially part of the class library. This class provides a set of primitives for use in 3-D space. (There are other primitives in this class as well, including a sphere and a torus.) You may be wondering why I don't add the cylinder via XAML. It would certainly be possible to do so if I compiled the class as a separate component, declared the class within the XAML, and added the cylinder via markup, but instead I chose to add them programmatically. I've included this class (Mesh3DObjects.cs) in the sample project that accompanies this article so that you can see how the geometry was created.

Let's look at creating the cylinder in code. In the startup routine, I create a cylinder by first creating a cylinder factory class from the class of primitives. Then, I create a specific cylinder from the factory, providing a brush material for our specific cylinder, in this case a SolidColorBrush of green, which I then transform using a ScaleTranform3D to resize the cylinder such that the cylinder's Y value starts at 0. Finally, I add the cylinder to the viewport via its Models collection.

//add the initial 3d cylinder
Mesh3DObjects.Cylinder cylinderFactory = new Mesh3DObjects.Cylinder();
Material materialGreen = new DiffuseMaterial(new SolidColorBrush(Colors.LimeGreen));
cylinder = new GeometryModel3D(cylinderFactory.Mesh, materialGreen);
ModelVisual3D mv3d = CpuPumpAnimation3dViewPort.Children[1] as ModelVisual3D;
mv3d.Content = cylinder;

All that is left is to animate the 3-D object. A fabulous feature of the WPF animation platform is that the technique used to animate 3-D is identical to animating 2-D. To resize the cylinder, I need to scale it using the ScaleTransform3D property of the mesh. The ScaleTransform3D is of type Vector3D, so I will create a Vector3DAnimation. Just like in the 2-D space, I will animate the ScaleTransform3D.ScaleYProperty dependency property of the mesh. In this case, when Y=1, the cylinder will be at 100 percent. So, we multiply the Y value, which represents the CPU utilization, by .01 to get a relative value, which will represent the To value. The From value is then the cpuInt multiplied by .01.

Also, I need to specify that the Y value of the ScaleCenter of this transform is -1 so that the cylinder will animate upward. This may seem unintuitive, but if I don't set the ScaleCenter of the ScaleTransform, the cylinder will grow both from the top and the bottom of the cylinder. The effect we want, however, is to only "grow" upward, thus the need to set the ScaleCenter property.

I clear the previous animations and then add the new animation to add the collection. That's it!

private void Do3DAnimation()
{

    vector3DAnimation = new DoubleAnimation();
    vector3DAnimation.From = (double)(y * .01);
    vector3DAnimation.To = (double)(cpuInt * .01);
    vector3DAnimation.Duration = new Duration(TIME_SPAN);
    //need to set Scale center to y = -1
    scaleTransform3D.CenterY = -1; 
    clock3D = vector3DAnimation.CreateClock();
    scaleTransform3D.ApplyAnimationClock(ScaleTransform3D.ScaleYProperty,clock3D);
    ModelVisual3D mv3d = CpuPumpAnimation3dViewPort.Children[1] as ModelVisual3D;
    mv3d.Transform = scaleTransform3D;
    clock3D.Controller.Begin();
    CpuPumpAnimation3dViewPort.Visibility = Visibility.Visible;

}

With that, we have a dynamically animated 3-D mesh.

Aa480159.avalon2d-3d01(en-us,MSDN.10).jpg

Conclusion

You can imagine a lot of places to go with this application. Certainly, an entire Task Manager or Performance Monitor could be written in WPF. Or, other data sources could feed the graph, say through a WCF Web service. In general, there are many ways to use WPF to provide interesting data visualization.

Acknowledgements

Thanks to Catherine Heller for helping me with the elevated permissions on Vista.