.
GDI+ FAQ
Skip Navigation LinksWelcome : Windows Forms Tips and Tricks : Animating objects

Animating graphic objects in Windows Forms.

A cool animation is great to look at but how do you go about writing one? Games in particular can use animations of hundreds of objects at once. What defines an object? How does an object move around?

These questions are often seen in the various online forums but a comprehensive answer is difficult to give in a short post. This article will explain all the nitty-gritty details. In it, you will see how to create an application that bounces a few hundred animated shapes around screen in real-time.

Graphical objects.

GDI+ has no concept of a persistent graphical object such as a square or line that you can move from place to place or touch with the mouse cursor. The graphics are written to the display surface and then forgotten about completely. In order to have an object that can be repositioned or made larger and smaller we must create a retained mode graphics engine.

The basic thing we need to do is to define some sort of entity, a generic shape perhaps, that has the attributes of position, size and colour. This entity will know how to draw itself to a graphics object. There may be more than one type of shape, such as a rectangle, a pentagon and a star but they will all generally obey the same rules so object oriented architectures enable us to create a "shape" base class with all the basic attributes and then override the small details when we create our specialized objects.

The architecture for this particular retained mode system will contain a shape with the following basic capabilities:

  • Location. The X, Y position of the center of the shape object.

  • Size. The width and height of the shape.

  • BackColor. The colour of the background or interior.

  • ForeColor. The colour of the foreground or the outline of the object

  • LineThickness. The width of the line that surrounds the object.

  • Transparency. A measure of opacity of the object.

  • Rotation. The angle, in degrees to which to object is rotated.

  • Vector. The direction and speed at which this object moves

  • RotationDelta. The number of degrees that the angle of rotation will change at each animation step.

  • Limits. A rectangle that the object is not allowed to go outside of.

Here are the beginnings of the Shape class.

  public class Shape

  {

    Point _location;

    public Point Location

    {

      get{return _location;}

      set{_location=value;}

    }

 

    Size _size;

    public Size Size

    {

      get{return _size;}

      set{_size=value;}

    }

 

    Color _backColor;

    public Color BackColor

    {

      get{return _backColor;}

      set{_backColor=value;}

    }

 

    Color _foreColor;

    public Color ForeColor

    {

      get{return _foreColor;}

      set{_foreColor=value;}

    }

 

    int _lineThickness;

    public int LineThickness

    {

      get{return _lineThickness;}

      set{_lineThickness=value;}

    }

 

    float _transparency;

    public float Transparency

    {

      get{return _transparency;}

      set

      {

        _transparency=(value>=0 ? (value<=1 ? value : 1) : 0);

      }

    }

 

    float _rotation;

    public float Rotation

    {

      get{return _rotation;}

      set{_rotation=value;}

    }

 

    Size _vector;

    public Size Vector

    {

      get{return _vector;}

      set{_vector=value;}

    }

 

    float _rotationDelta;

    public float RotationDelta

    {

      get{return _rotationDelta;}

      set{_rotationDelta=value;}

    }

 

    Rectangle _limits;

    public Rectangle Limits

    {

      get{return _limits;}

      set{_limits=value;}

    }

  }

 

Transforms and order.

When creating such a retained-mode system, the individual objects will position themselves before they draw the content of the object. The easiest way to do this is with a graphic transform represented by a matrix. Many beginners see the transform as something that is applied to the whole page to scale the drawing or modify the coordinate system in some way but in retained mode systems the transform is generally used on a per-object basis in addition to whatever global transform is applied. Usually the current state of the Graphics system should be saved, the transform applied, the drawing done and the previous state of the graphics system restored before drawing of other objects takes place. This lets your code maintain an orderly and progressive series of state-changes rather than trying to apply a transform to create a particular state an then apply another to get to a second state. More complex systems that have hierarchies of objects will maintain a graphics stack with many levels of transforms. This demonstration only requires one.

The order that transforms are applied is of paramount importance. Applying two identical transforms in different orders will produce wildly different results. This animation system uses rotation to spin an object in space and displacement or translation to move it in an X or Y plane. Although it isn't implemented in this demo objects can also be scaled to make them larger or smaller or distort them along the axes. The order that transforms should be applied is generally Scale, Rotate, Translate. The code for the shape base-class provides a method for saving the state of the current Graphics object, applying a transform to the Graphics, drawing and then restoring the state of the Graphics object once more. The methods shown below show how the object sets up the transform, draws the object and then restores the state of the Graphics object.

    GraphicsState _state;

 

    /// <summary>

    /// Sets up the transform for each shape

    /// </summary>

    /// <remarks>

    /// As each shape is drawn the transform for that shape including rotation and

    /// location is made to a new Matrix object.

    /// This matrix is used to modify the graphics transform <i>For each shape</i>

    /// </remarks>

    /// <param name="g">The Graphics being drawn on</param>

    protected void SetupTransform(Graphics g)

    {

      _state=g.Save();    

      Matrix mx=new Matrix();

      mx.Rotate(_rotation,MatrixOrder.Append);

      mx.Translate(this.Location.X,this.Location.Y,MatrixOrder.Append);

      g.Transform=mx;

    }

 

    /// <summary>

    /// Simply restores the original state of the Graphics object

    /// </summary>

    /// <param name="g">The Graphics object being drawn upon</param>

    protected void RestoreTransform(Graphics g)

    {

      g.Restore(_state);

    }

 

    public void Draw(Graphics g)

    {

      SetupTransform(g);

      RenderObject(g);

      RestoreTransform(g);

    }

Animating the object.

Animation systems generally work from a timebase of some kind. In Windows Forms there are several sorts of timers that we can use to give the animated objects a "pulse". This system provides each object with a "Tick" method that uses the Vector to move a shape, the RotationDelta to rotate the shape, and the Limits to ensure that the shape remains inside the desired area while it bounces around the screen. The following listing shows how the Tick method performs the animations

    public virtual void Tick()

    {

      //ensure that the object is in the page.

      //this is in case the window was resized

      if(this.Location.X>this.Limits.Right)

        this.Location=new Point(this.Limits.Right-1,this.Location.Y);

      if(this.Location.Y>this.Limits.Bottom)

        this.Location=new Point(this.Location.X,this.Limits.Bottom-1);

 

      //Generate a new location adding in the vectors

      //check the limits and switch vector directions as needed

      int newx=this.Location.X+this.Vector.Width;

      if(newx>this.Limits.Right || newx<this.Limits.Left)

        this.Vector=new Size(-1*this.Vector.Width,this.Vector.Height);

      int newy=this.Location.Y+this.Vector.Height;

      if(newy>this.Limits.Bottom || newy<this.Limits.Top)

        this.Vector=new Size(this.Vector.Width,-1*this.Vector.Height);

 

      //This is the new position

      Location=new Point(

        this.Location.X+this.Vector.Width,

        this.Location.Y+this.Vector.Height);

 

      //Apply the rotation factor

      this.Rotation+=this.RotationDelta;

      //Limit just to be neat

      Rotation=(Rotation<360f ? (Rotation>=0 ? Rotation : Rotation+360f) : Rotation-360f);

    }

Drawing the object.

Finally, after all the setup work is done, the shape must draw itself onto the display. The Shape class provides a RenderObject virtual method to accomplish this. Derived classes can override this method and draw their own appearance when asked to do so. The Draw method provided by the base class performs the additional tasks of setting up and restoring the object.

    public virtual void RenderObject(Graphics g)

    {

    }

Real shapes.

Now that the groundwork is laid we can construct real shapes. These override the Shape class and draw themselves in the RenderObject override. The following listing shows three shapes, a Square, a Star and a Pentagon that render themselves after the base class Draw routine has done all the setup work.

  public class Square : Shape

  {

    /// <summary>

    /// Draws a square. Note that the square is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      g.FillRectangle(sb,

                 -this.Size.Width/2,

                 -this.Size.Height/2,

                 this.Size.Width,

                 this.Size.Height);

      g.DrawRectangle(p,

                 -this.Size.Width/2,

                 -this.Size.Height/2,

                 this.Size.Width,

                 this.Size.Height);

      sb.Dispose();

      p.Dispose();

    }

 

  }

 

  public class Star : Shape

  {

    /// <summary>

    /// Draws a star. Note that the star is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      Point[] pts=new Point[11];

      bool pointy=true;

      float a=0;

      for(int c=0;c<10;c++)

      {

        float dist=pointy ? 1 : 0.6f;

        pts[c]=new Point(

            (int)(dist* (this.Size.Width/2)*Math.Cos(a)),

            (int)(dist *(this.Size.Height/2)*Math.Sin(a)));

        a+=(float)Math.PI*2/10;

        pointy=!pointy;

      }

      pts[10]=pts[0];

      g.FillPolygon(sb,pts);

      g.DrawPolygon(p,pts);

      sb.Dispose();

      p.Dispose();

    }

  }

 

  public class Pentagon : Shape

  {

    /// <summary>

    /// Draws a pentagon. Note that the pentagon is drawn about the origin.

    /// </summary>

    /// <param name="g">The graphics to draw on.</param>

    public override void RenderObject(Graphics g)

    {

      Pen p=new Pen(ForeColor,LineThickness);

      SolidBrush sb=new SolidBrush(

                 Color.FromArgb((int)(255*this.Transparency),

                 this.BackColor));

      Point[] pts=new Point[6];

      float a=0;

      for(int c=0;c<5;c++)

      {

        pts[c]=new Point(

            (int)((this.Size.Width/2)*Math.Cos(a)),

            (int)((this.Size.Height/2)*Math.Sin(a)));

        a+=(float)Math.PI*2/5;

      }

      pts[5]=pts[0];

      g.FillPolygon(sb,pts);

      g.DrawPolygon(p,pts);

      sb.Dispose();

      p.Dispose();

    }

 

  }

 

Collect your thoughts.

Now a shape had been defined and a family of objects derived from that basic object. The shape knows how to move, rotate, limit it's area of movement to remain in a given area and can be used to provide literally thousands of different possibilities for their visual appearance. Now the application needs to know how to deal with these objects. For this we'll create a collection of Shape objects that can hold as many of them that we like.

  /// <summary>

  /// Manages a collection of shape objects

  /// </summary>

  public class ShapeCollection : CollectionBase

  {

    public void Add(Shape s)

    {

      List.Add(s);

    }

 

    public void Remove(Shape s)

    {

      List.Remove(s);

    }

 

    public Shape this[int index]

    {

      get{return (Shape)List[index];}

      set{List[index]=value;}

    }

  }

 

Integrating with the application.

The components include shapes and a collection in which to store them. Now the application has to provide a timing method to create the animating ticks once every few milliseconds. This demo uses 40 millisecond intervals because that gives about 25 frames per second. The value of "about" 25 frames per second should be qualified by the information that the standard Windows.Forms.Timer isn't a particularly accurate beast.

The application has a ShapeCollection which it populates with some shapes, randomly initialized. It has a timer that fires every 40 milliseconds to update all the shapes by calling their Tick methods and an OnPaint handler that draws all the shapes. The following listing shows the Form based object that does all this. Note also that the OnSize method is overidden to redefine the limits of each of the objects so that they remain in sight as you resize the form.

  /// <summary>

  /// Manages a list of animated objects.

  /// </summary>

  public class Form1 : System.Windows.Forms.Form

  {

 

    /// <summary>

    /// Required designer variable.

    /// </summary>

    private System.ComponentModel.Container components = null;

 

    /// <summary>

    /// A collection of Shape based objects

    /// </summary>

    ShapeCollection _shapes=new ShapeCollection();

 

    /// <summary>

    /// The message-driven timer

    /// </summary>

    System.Windows.Forms.Timer wt=new System.Windows.Forms.Timer();

 

    /// <summary>

    /// Constructs a new form

    /// </summary>

    public Form1()

    {

      //

      // Required for Windows Form Designer support

      //

      InitializeComponent();

 

      //This form is double buffered

      SetStyle(

        ControlStyles.AllPaintingInWmPaint |

        ControlStyles.DoubleBuffer |

        ControlStyles.ResizeRedraw |

        ControlStyles.UserPaint,

        true);

 

 

      //A random number generator for the initial setup

      Random r=new Random();

 

      //We create 100 objects

      for(int c=0; c<100; c++)

      {

        Shape s=null;

        //using 1 of 3 randomly chosen shape types

        switch(r.Next(3))

        {

          case 0:

            s=new Square();

            break;

          case 1:

            s=new Pentagon();

            break;

          case 2:

            s=new Star();

            break;

        }

 

        //The shape is initialized with random parameters.

        s.Limits=this.ClientRectangle;

        s.Location=new Point(r.Next(this.ClientRectangle.Width),r.Next(this.ClientRectangle.Height));

        s.Size=new Size(1+r.Next(100), 1+r.Next(100));

        s.BackColor=Color.FromArgb(r.Next(255),r.Next(255),r.Next(255));

        s.ForeColor=Color.FromArgb(r.Next(255),r.Next(255),r.Next(255));

        s.RotationDelta=(float)r.Next(20);

        s.Transparency=(float)r.NextDouble();

        s.LineThickness=r.Next(10);

        s.Vector=new Size(-10+r.Next(20),-10+r.Next(20));

 

        //and added to the list of shapes

        this._shapes.Add(s);

      }

 

      //set up the timer so that animation can take place

      wt.Interval=40;

      wt.Tick+=new EventHandler(wt_Tick);

      wt.Enabled=true;

 

    }

 

    /// <summary>

    /// Clean up any resources being used.

    /// </summary>

    protected override void Dispose( bool disposing )

    {

      if( disposing )

      {

        if (components != null)

        {

          components.Dispose();

        }

      }

      base.Dispose( disposing );

    }

 

    #region Windows Form Designer generated code

    /// <summary>

    /// Required method for Designer support - do not modify

    /// the contents of this method with the code editor.

    /// </summary>

    private void InitializeComponent()

    {

      //

      // Form1

      //

      this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

      this.BackColor = System.Drawing.Color.White;

      this.ClientSize = new System.Drawing.Size(292, 266);

      this.Name = "Form1";

      this.Text = "Form1";

 

    }

    #endregion

 

    /// <summary>

    /// The main entry point for the application.

    /// </summary>

    [STAThread]

    static void Main()

    {

      Application.Run(new Form1());

    }

 

    private void wt_Tick(object sender, EventArgs e)

    {

      foreach(Shape s in this._shapes)

        s.Tick();

      Invalidate();

    }

 

    protected override void OnPaint(PaintEventArgs e)

    {

      foreach(Shape s in this._shapes)

        s.Draw(e.Graphics);

    }

 

    protected override void OnClosing(CancelEventArgs e)

    {

      wt.Enabled=false;

      wt.Dispose();

      base.OnClosing (e);

    }

 

    protected override void OnClosed(EventArgs e)

    {

      base.OnClosed (e);

    }

 

 

    /// <summary>

    /// Updates the limits of all current shapes so that they don't disappear off-screen

    /// </summary>

    /// <param name="e"></param>

    protected override void OnSizeChanged(EventArgs e)

    {

      foreach(Shape s in this._shapes)

        s.Limits=this.ClientRectangle; 

      base.OnSizeChanged (e);

    }

 

  }

 

Summary.

As you study the code for the application you will notice the simplicity of the OnPaint, OnSize and wt_Tick methods. They all perform the simple action of iterating over the objects in the list and performing some very simple function. The complication of transforming the Graphics object or managing movement is left entirely to the objects themselves. This is how Object Oriented Programming is used at its best in a graphical system.

The application source code can be downloaded in ZIP form from this link. The project contains both C# and VB versions. Try adjusting the number of objects created in the initial Form1 constructor by changing the maximum value of "c" in the for loop. Depending on your system speed and graphics card capabilities you will find that many hundreds or even a few thousand shapes will bounce around the screen quite happily. Figure 1 shows the Funimation application in action.

Figure 1. Fun with animation.

Return to Windows Forms Tips and Tricks.

 

Sponsored By
DaraizeTechnologies.com

&
Proteus Groupe

Bob Powell

Create your badge

Copyright © Bob Powell 2000-2012.  All rights reserved.