How to Write Animations In C#

This project will be written in C# because of its ease in creating windowed applications. This application demonstrates one of the simplest algorithms that I can never seem to remember. It presents the solution in a simple way, if you can abstract the windowed boilerplate. The boilerplate provides the tool to test whether the animation algorithm has been implemented correctly.

I’ll name my class “AnimationViewer” and include all the needed “usings” and the complete Main().

class AnimationViewer

/**
The MIT License (MIT)
Copyright © 2019 <John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial 
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Timers;
 
namespace Animation
{
    public class AnimationViewer : Form
    {
        public AnimationViewer()
        {
             
        }
 
        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new AnimationViewer());
        }
    }
}

I’m going to create a primitive GUI to display the Window encompassing a list of sprites, a box to display the animation and a Trackbar to control the speed of the animation. For simplicity I will do this all in one file.

Next I’ll introduce the member fields used to complete this project

  1. A ListView is needed to contain the individual images to be made into an animation.
  2. An ImageList is used to make the ListView store images instead of strings
  3. A Button is used to provide a file dialog to select images to be used as frames in the animation
  4. A Trackbar is used to throttle the speed of the animation in the preview pane
  5. A Picture box is where the animation will be displayed
  6. A Timer is instrument in making an animation work
  7. An int is needed for a index used to keep track of which frame is being displayed while the animation is running
  8. As a matter of preference I opted to add a constant to use as a scaling factor which enlarges the image for effect

This is how the code looks once it has been updated with member fields. I use fields rather than properties, because for this example everything is self contained. There is no need for anything to be more than private.

private ListView animListView;
private ImageList imageList;
private Button openButton;
private TrackBar slider;
private PictureBox frameBox;
private System.Timers.Timer animationTimer;
private int currentIndex;
private const float factor = 2.0f;

Next is another matter of preference. I prefer to initialize all my variables in the constructor rather than in the declaration line. It’s a habit I developed while programming C++ for so many years. I would would initialize in the constructor and do all the disposing or freeing tasks in the destructor.

public AnimationViewer()
{
    animListView = new ListView();
    openButton   = new Button();
    imageList    = new ImageList();
    slider       = new TrackBar();
    frameBox     = new PictureBox();
 }

Next I create another initializing type of method I name “Init().” Instead of declaring, initializing or allocating variables, I set the attributes, properties, and state of them. Things like naming buttons and windows or positioning them and then add the GUI elements to the Window. I call Init() from the constructor(s).

private void Init()
{
    Text = "Animation Viewer";
    Size = new Size(800, 600);
    imageList.ImageSize = new Size(64, 64);
    openButton.Text = "Add Frame(s)";
    openButton.Location = new Point(200, 435);
    openButton.Size = new Size(150, 35);
    animListView.BorderStyle = BorderStyle.Fixed3D;
    animListView.Location = new Point(200, 25);
    animListView.Size = new Size(150, 400);
    animListView.MultiSelect = false;
    frameBox.BorderStyle = BorderStyle.FixedSingle;
    slider.Dock = DockStyle.Bottom;
    frameBox.Size = new Size(128, 128);
    frameBox.Location = new Point(400, 25);
    frameBox.Image = new Bitmap(128, 128);

    Controls.Add(animListView);
    Controls.Add(openButton);
    Controls.Add(slider);
    Controls.Add(frameBox);
}

Add the call to Init() to the end of the constructor.

public AnimationViewer()
{
    animListView = new ListView();
    openButton   = new Button();
    imageList    = new ImageList();
    slider       = new TrackBar();
    frameBox     = new PictureBox();
    Init();
}

Next I work on the first event handler. I create a generic EventHandler for the “Add Frame” button. I named the handler ClickOnButton(object sender, EventArgs e). The Button.Click handler starts by creating and initializing an OpenFileDialog object, after which I set it to allow the selection of multiple files. Then there is a check to see if the users chooses to accept (rather than cancel) his selection. If accepted, the following tasks are performed

  1. Declare an integer index variable to keep track of the index of the ImageIndex of the ListviewItem variable
  2. Retreive and store the list of file names that were selected in the dialog
  3. Declare a loop to iterate through the list of names one string at a time
  4. Generate an image based on the name of the file being passed through this iteration
  5. Add that image to the image list
  6. Create an ListViewItem variable;
  7. Set the ImageIndex property of the ListViewItem variable to the index variable
  8. Increment the index variable by 1
  9. End the loop
  10. Setup the trackbar
    1. Set the member variable representing the value of frames per second to an initial value
    2. Set the trackbar to the minimum allowable fps
    3. Set the trackbar to the maximum allowable fps
    4. Enable the trackbar
  11. Set the LargeImageList property of the List view to the ImageList (this is how the ListView is told to display images instead of strings)
private void ClickOnButton(object sender, EventArgs e)
{
    OpenFileDialog dlg = new OpenFileDialog();
    dlg.Multiselect = true;
    if (dlg.ShowDialog() == DialogResult.OK)
    {
        int count = 0;
        string[] imgs = dlg.FileNames;
        foreach(string str in imgs)
        {
            Image bit = Image.FromFile(str, true);
            imageList.Images.Add(bit);
            ListViewItem item = new ListViewItem();
            
            item.ImageIndex = count++;
            animListView.Items.Add(item);
        }
        currentIndex = 0;
        slider.Minimum = 0;
        slider.Maximum = 100;
        slider.Enabled = true;
        animListView.LargeImageList = imageList;
    }
}

Now I add the OnButtonClick handler to the Click listener of the button in the Init method.

private void Init()
{
    Text = "Animation Viewer";
    Size = new Size(800, 600);
    imageList.ImageSize = new Size(64, 64);
    openButton.Text = "Add Frame(s)";
    openButton.Location = new Point(200, 435);
    openButton.Size = new Size(150, 35);
    openButton.Click += new EventHandler(ClickOnButton);
    animListView.BorderStyle = BorderStyle.Fixed3D;
    animListView.Location = new Point(200, 25);
    animListView.Size = new Size(150, 400);
    animListView.MultiSelect = false;
    frameBox.BorderStyle = BorderStyle.FixedSingle;
    slider.Dock = DockStyle.Bottom;
    frameBox.Size = new Size(128, 128);
    frameBox.Location = new Point(400, 25);
    frameBox.Image = new Bitmap(128, 128);

    Controls.Add(animListView);
    Controls.Add(openButton);
    Controls.Add(slider);
    Controls.Add(frameBox);
    slider.Enabled = false;
}

By this point, you should be able to execute the program and see the full GUI (such as it is), and be able to click the “Add Frame(s)” button and load images into the ListView, but that is the existent.

The next task will be to implement the code that will scale the selected image or the images in the animation.

To scale an image I create a Bitmap object passing in the width and height of the original image (the image passed into the method) times them each by my chosen scaling factor. I then create a Graphics object from aforementioned bitmap. Then two Rectangle (or RectangleF if using floating point numbers) The source rectangle is given a starting point of (0, 0) and its width and height as the width and height arguments. The destination rectangle also starts at (0,0) but its width and height are eached multiplied by the scaling factor. Lastly, the graphics object calls DrawImage to paint the source image using the starting point and the dimensions of the source rectangle onto the starting point of the destination rectangle using the scaled dimensions of the Bitmap object created earlier in this method. Finally return that object.

private Image ResizeImage(Image image)
{
    Bitmap bit = new Bitmap(image.Width*(int)factor, image.Height*(int)factor);
    Graphics g = Graphics.FromImage(bit);
    RectangleF src  = new RectangleF(0.0f, 0.0f, image.Width, image.Height);
    RectangleF dest = new RectangleF(0.0f, 0.0f, (factor * image.Width), (factor *image.Height));
    g.DrawImage(image, dest, src, GraphicsUnit.Pixel);

    return bit;
}

A helper method is created next to display a selected image at the scaled size in the PictureBox form element/widget.

private void SelectImage(int index)
{
    Image pic = imageList.Images[index];
    frameBox.Image = ResizeImage(pic);
}

Another event handler is introduced now, OnSelectedImage (object sender, EventArgs e). Utilizing an instance of ListView.SelectedLindexCollection retrieve the indexes of the selected images in the ListView. Check if that the currentIndex is greater than 0; Once verified set the currentIndex to the first element in the list (there should only be one, because I set the ListView’s MultiSelect property to false. Then I call the SelectImage() method passing in the currentIndex.

private void OnSelectedImage(object sender, EventArgs e)
{
    ListView.SelectedIndexCollection indexes = 
        animListView.SelectedIndices;
    if(indexes.Count &gt; 0)
    {
        currentIndex = indexes[0];
        SelectImage(currentIndex);
    }
}

The OnSelectedImage() handler needs to be attached to the ListView through the ItemSelectionChanged handler. Instead of using the base EventHandler delegate, the ListViewItemSelectionChangedEventHandler is used. So, I add the OnSelectedImage() method to the ListView in the Init() method.

private void Init()
{
    Text = "Animation Viewer";
    Size = new Size(800, 600);
    imageList.ImageSize = new Size(64, 64);
    openButton.Text = "Add Frame(s)";
    openButton.Location = new Point(200, 435);
    openButton.Size = new Size(150, 35);
    openButton.Click += new EventHandler(ClickOnButton);
    animListView.BorderStyle = BorderStyle.Fixed3D;
    animListView.Location = new Point(200, 25);
    animListView.Size = new Size(150, 400);
    animListView.MultiSelect = false;
    animListView.ItemSelectionChanged += new ListViewItemSelectionChangedEventHandler(OnSelectedImage);
    frameBox.BorderStyle = BorderStyle.FixedSingle;
    slider.Dock = DockStyle.Bottom;
    frameBox.Size = new Size(128, 128);
    frameBox.Location = new Point(400, 25);
    frameBox.Image = new Bitmap(128, 128);

    Controls.Add(animListView);
    Controls.Add(openButton);
    Controls.Add(slider);
    Controls.Add(frameBox);
    slider.Enabled = false;
}

Now when the program is executed. You should be able to load images, then select one at time each time the scaled image displaying in the PictureBox.

This Dispose() is the last method before we arrive at the method that actually perform the animation. I apologize for the long journey, but it seem necessary to provide a mechanism to see the end result. As a result Dispose() is defined primarily to correctly terminate use of the Timer object and then let the parent class dispose of the rest.

protected override void Dispose (bool disposing)
{
    if (animationTimer != null)
    {
        animationTimer.Dispose();
    }
    base.Dispose(disposing);
}

Now the good stuff!

I’ll start with Update() because it has no dependencies on methods that I haven’t covered yet.

First there is a check that the number of items in the ListView isn’t equal to 0 because that would mean there are no images in the list and it prevents a divide by zero error. So if that is the case exit the method

Next get the selected indexes from the ListView). (Fun fact: indexes(US), indices(UK)). Increase the current index by 1, but if that causes the current index to exceed the size of the list set the current index back to the beginning. Lastly call SelectImage() passing in the current index.

To be as brief as possible, the whole point of this method is to update the index and display the image at that index.

private void Update(object sender, EventArgs e)
{
    if (animListView.Items.Count == 0)
    {
        return;
    }
    var selected = animListView.SelectedIndices;
    currentIndex = (currentIndex + 1) % animListView.Items.Count;
    SelectImage(currentIndex);
}

The Start(object sender, EventArgs e) EventHandler starts by checking if the member variable Timer is not null. If that is the case, dispose of the Timer, because we want a new instance of a Timer every time Start() is called. This is because the Start() is only called when the rate of animation is called. The next check to preform is to see if the value of the Trackbar is equal to 0, this is because in this implementation of the application the Trackbar value determines the rate of animation and 0 would indicate that there is no animation, so the Timer is set to null.

While the value is not 0, the member Timer object is initialized through its constructor to have an interval of 1 second divided by the Value property of the Trackbar member object. I set the Timer to synchronize to the UI thread. NOTE: There is a UI Timer, but I opted not to use it because it lacked precision I required.

The Final steps are to set the Timer’s AutoReset property to true, meaning the Timer will reset after its interval has past. An ElapsedEventHandler delegate must be assigned to the Elapsed property of the Timer. Update will be used, so that every interval the current index in the ViewList will be updated/incremented. Finally the Start() method of the Timer member object must be called.

private void Start(object sender, EventArgs e)
{
    if (animationTimer != null)
    {
        animationTimer.Dispose();
    }
    if (slider.Value == 0)
    {
        animationTimer = null;
    }
    else
    {
        animationTimer = new System.Timers.Timer(1000 / slider.Value);
        animationTimer.SynchronizingObject = this;
        animationTimer.AutoReset = true;
        animationTimer.Elapsed += new ElapsedEventHandler(Update);
        animationTimer.Start();
    }
}

With the Start() method completed. There are two places it must be applied. It has to be used as the event handler for the Trackbar’s Scroll delegate. This restarts the frames per second every time the Trackbar’s slider scrolls.

private void Init()
{
    Text = "Animation Viewer";
    Size = new Size(800, 600);
    imageList.ImageSize = new Size(64, 64);
    openButton.Text = "Add Frame(s)";
    openButton.Location = new Point(200, 435);
    openButton.Size = new Size(150, 35);
    openButton.Click += new EventHandler(ClickOnButton);
    animListView.BorderStyle = BorderStyle.Fixed3D;
    animListView.Location = new Point(200, 25);
    animListView.Size = new Size(150, 400);
    animListView.MultiSelect = false;
    animListView.ItemSelectionChanged += new ListViewItemSelectionChangedEventHandler(OnSelectedImage);
    frameBox.BorderStyle = BorderStyle.FixedSingle;
    slider.Dock = DockStyle.Bottom;
    frameBox.Size = new Size(128, 128);
    frameBox.Location = new Point(400, 25);
    frameBox.Image = new Bitmap(128, 128);

    Controls.Add(animListView);
    Controls.Add(openButton);
    Controls.Add(slider);
    Controls.Add(frameBox);
    slider.Enabled = false;
    slider.Scroll += new EventHandler(Start);
}

The final change to the AnimationViewer class is to make a manual call to Start() with null for both arguments (i.e. Start(null, null)) at the last line of ClickOnButton(). The purpose of this is so that the animation can continue when you load new images as long as the Trackbar is not at 0.

private void ClickOnButton(object sender, EventArgs e)
{
    OpenFileDialog dlg = new OpenFileDialog();
    dlg.Multiselect = true;
    if (dlg.ShowDialog() == DialogResult.OK)
    {
        int count = 0;
        string[] imgs = dlg.FileNames;
        foreach(string str in imgs)
        {
            Image bit = Image.FromFile(str, true);
            imageList.Images.Add(bit);
            ListViewItem item = new ListViewItem();
            
            item.ImageIndex = count++;
            animListView.Items.Add(item);
        }
        currentIndex = 0;
        slider.Minimum = 0;
        slider.Maximum = 100;
        slider.Enabled = true;
        animListView.LargeImageList = imageList;
        Start(null, null);
    }
}

The Complete Source

/**
The MIT License (MIT)
Copyright © 2019 <John L. Mooney>

Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the “Software”), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial 
portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;
using System.Timers;

namespace AnimationViewer
{
    
    public class AnimationViewer : Form
    {
        private ListView animListView;
        private ImageList imageList;
        private Button openButton;
        private List<Image> images;
        private TrackBar slider;
        private PictureBox frameBox;
        private System.Timers.Timer animationTimer;
        private int currentIndex;
        private const float factor = 2.0f;

        public AnimationViewer()
        {
            animListView = new ListView();
            openButton   = new Button();
            imageList    = new ImageList();
            slider       = new TrackBar();
            frameBox     = new PictureBox();
            Init();
        }

        private void Init()
        {
            Text = "Animation Viewer";
            Size = new Size(800, 600);
            imageList.ImageSize = new Size(64, 64);
            openButton.Text = "Add Frame(s)";
            openButton.Location = new Point(200, 435);
            openButton.Size = new Size(150, 35);
            openButton.Click += new EventHandler(ClickOnButton);
            animListView.BorderStyle = BorderStyle.Fixed3D;
            animListView.Location = new Point(200, 25);
            animListView.Size = new Size(150, 400);
            animListView.MultiSelect = false;
            animListView.ItemSelectionChanged += new ListViewItemSelectionChangedEventHandler(OnSelectedImage);
            frameBox.BorderStyle = BorderStyle.FixedSingle;
            slider.Dock = DockStyle.Bottom;
            frameBox.Size = new Size(128, 128);
            frameBox.Location = new Point(400, 25);
            frameBox.Image = new Bitmap(128, 128);

            Controls.Add(animListView);
            Controls.Add(openButton);
            Controls.Add(slider);
            Controls.Add(frameBox);
            slider.Enabled = false;
            slider.Scroll += new EventHandler(Start);
        }


        private void ClickOnButton(object sender, EventArgs e)
        {
            OpenFileDialog dlg = new OpenFileDialog();
            dlg.Multiselect = true;
            if (dlg.ShowDialog() == DialogResult.OK)
            {
                int count = 0;
                string[] imgs = dlg.FileNames;
                foreach(string str in imgs)
                {
                    Image bit = Image.FromFile(str, true);
                    imageList.Images.Add(bit);
                    ListViewItem item = new ListViewItem();
                    
                    item.ImageIndex = count++;
                    animListView.Items.Add(item);
                }
                currentIndex = 0;
                slider.Minimum = 0;
                slider.Maximum = 100;
                slider.Enabled = true;
                animListView.LargeImageList = imageList;
                Start(null, null);
            }
        }

        private Image ResizeImage(Image image)
        {
            Bitmap bit = new Bitmap(image.Width*(int)factor, image.Height*(int)factor);
            Graphics g = Graphics.FromImage(bit);
            RectangleF src  = new RectangleF(0.0f, 0.0f, image.Width, image.Height);
            RectangleF dest = new RectangleF(0.0f, 0.0f, (factor * image.Width), (factor *image.Height));
            g.DrawImage(image, dest, src, GraphicsUnit.Pixel);

            return bit;
        }

        private void SelectImage(int index)
        {
            Image pic = imageList.Images[index];
            frameBox.Image = ResizeImage(pic);
        }

        private void OnSelectedImage(object sender, EventArgs e)
        {
            ListView.SelectedIndexCollection indexes = 
                animListView.SelectedIndices;
            if(indexes.Count > 0)
            {
                currentIndex = indexes[0];
                SelectImage(currentIndex);
            }
        }

        protected override void Dispose (bool disposing)
        {
            if (animationTimer != null)
            {
                animationTimer.Dispose();
            }
            base.Dispose(disposing);
        }

        private void Update(object sender, EventArgs e)
        {
            if (animListView.Items.Count == 0)
            {
                return;
            }
            var selected = animListView.SelectedIndices;
            currentIndex = (currentIndex + 1) % animListView.Items.Count;
            SelectImage(currentIndex);
        }

        private void Start(object sender, EventArgs e)
        {
            if (animationTimer != null)
            {
                animationTimer.Dispose();
            }
            if (slider.Value == 0)
            {
                animationTimer = null;
            }
            else
            {
                animationTimer = new System.Timers.Timer(1000 / slider.Value);
                animationTimer.SynchronizingObject = this;
                animationTimer.AutoReset = true;
                animationTimer.Elapsed += new ElapsedEventHandler(Update);
                animationTimer.Start();
            }
        }

        [STAThread]
        static void Main()
        {
            Application.SetHighDpiMode(HighDpiMode.SystemAware);
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new AnimationViewer());
        }
    }
}

Here Is a Spritesheet Entitled “Sara”

I split the 11th row from the image into 9 frames to create an animation to where the character appears to be walking toward the right.

Attribution For The Spritesheet
Site: https://opengameart.org
Revised Illustration of “Sara”
Graphic Artist: Stephen “Redshrike” Challener
Contributor: William.Thompsonj
Page: https://opengameart.org/content/lpc-sara
Original Artwork of “Sara”:
Graphic artist: Mandi Paugh
Page: https://opengameart.org/content/sara-wizard

One thought on “How to Write Animations In C#

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.