C# Graphics and Animation
So far our forms have been built from ready-made controls: labels, text boxes and buttons. This section draws on the form directly. Part 1 uses GDI+, the drawing system of Windows Forms, to draw lines, rectangles, ellipses, arcs, polygons and text. Part 2 explains when a drawing stays on the screen and when it disappears. Part 3 builds a small app that draws line patterns from four numbers. Parts 4 and 5 make things move: a Timer changes a position many times a second, and the form is redrawn after every change. Every program in this section was built and run on Windows, and every screenshot, number and log line below comes from those runs.
Objectives
By the end of the section you should be able to:
- Get a
Graphicsobject from thePaintevent and plan a drawing with the form's pixel coordinates. - Draw outlines with a
Penand fill shapes with aSolidBrush:DrawLine,DrawRectangle,DrawEllipse,DrawArc,DrawPolygon,DrawStringand theFillmethods. - Explain why a drawing made with
CreateGraphicsdisappears while a drawing made inPaintstays, and useInvalidateandRefresh. - Compute the end of a line from an angle and a length with
Math.CosandMath.Sin, and build a pattern from many lines. - Animate with a
Timer: setIntervalandEnabled, change a position inTick, and callInvalidate. - Make objects bounce off the edges of the form, move several objects from one timer, steer one with the arrow keys, and play a frame-by-frame animation.
Part 1: drawing with GDI+
The Graphics object
GDI+ is the drawing system behind Windows Forms. In C# it lives in the System.Drawing namespace, and its centre is the Graphics class: a Graphics object represents a drawing surface, and every drawing method (DrawLine, FillEllipse, DrawString and the rest) belongs to it. Working with graphics always has two steps: first get a Graphics object, then use it to draw.
There are three ways to get one:
| Way | Code | Use it when |
|---|---|---|
The Paint event of a form or control | Graphics g = e.Graphics; | you draw the picture of the form (the usual way) |
The CreateGraphics method | Graphics g = this.CreateGraphics(); | you want to draw on a form or control that already exists |
| From an image | Graphics g = Graphics.FromImage(myBitmap); | you want to change an existing image |
Next to Graphics you will use four more classes: a Pen draws lines and outlines, a Brush fills areas (and text), a Font describes the letters of text, and the Color structure gives the colors.
Create the Paint handler
We use the form like a canvas. Its Paint event is where the drawing code goes:
- Click anywhere on the form in the Design view.
- Open the Properties window (Alt+Enter, or right-click and choose Properties).
- Click the lightning bolt icon to switch from the property list to the Events list. The A to Z icon sorts the list alphabetically, which makes
Painteasy to find. - Double-click the box next to Paint.
Visual Studio creates an empty method, a method stub, and connects it to the event. We add one line that gives the drawing surface a short name:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
// draw here
}e is the PaintEventArgs passed to the handler, and e.Graphics is the surface for this repaint.
Coordinates on a form
Think of the form as a grid of pixels:
(0, 0)is the top-left corner of the client area (the part inside the title bar and the borders).xgrows to the right.ygrows down. This is the opposite of the y-axis in a geometry class.- The unit is the pixel. A line from
(2, 2)to(4, 1)is only a few pixels long and almost invisible, so real drawings use numbers in the hundreds.
To give ourselves room, the constructor sets the size of the window:
public Form1()
{
InitializeComponent();
this.Width = 500;
this.Height = 500;
}this is the form object being built. Width and Height are the size of the whole window, title bar and borders included. Our program printed its client size on Windows: 484 by 461 pixels. The area you draw on is 16 pixels narrower and 39 pixels shorter than the window.
Pen or Brush?
A pen draws the outline of a shape; a brush paints its inside. That is the main difference between the two.
Pen myPen = new Pen(Color.Black); // width 1
Pen bluePen = new Pen(Color.Blue, 3); // width 3
SolidBrush myBrush = new SolidBrush(Color.Red);
SolidBrush orangeBrush = new SolidBrush(Color.FromArgb(255, 255, 140, 0));The second argument of Pen is the width of the line. Color.FromArgb builds a color from four numbers from 0 to 255: alpha (255 means fully opaque), red, green and blue.
| Object | Paints | Used by |
|---|---|---|
Pen | the outline | DrawLine, DrawRectangle, DrawEllipse, DrawArc, DrawPolygon |
SolidBrush | the inside | FillRectangle, FillEllipse, FillPolygon, DrawString |
Pens, brushes, fonts and Graphics objects hold system resources. Call Dispose() on the ones you create when you have finished with them. Never dispose e.Graphics: the Paint event created it and it cleans it up itself.
Worked example: a line and a rectangle
Inside Form1_Paint:
g.DrawLine(myPen, 20, 20, 400, 420); // x1, y1, x2, y2
g.DrawRectangle(myPen, 100, 100, 200, 150); // x, y, width, heightPredict: which two points does the line join, and where are the four corners of the rectangle?
DrawLine connects two points given as coordinate pairs, so the line goes from (20, 20) to (400, 420).
DrawRectangle takes only one corner, the top-left (x, y), plus a width w and a height h; the computer works out the rest. With (x, y) the top-left corner and (x2, y2) the bottom-right one:
- x2 = x + w = 100 + 200 = 300
- y2 = y + h = 100 + 150 = 250
So the corners are (100, 100) and (300, 100) at the top, and (100, 250) and (300, 250) at the bottom.
Worked example: an ellipse in a bounding box
There is no DrawCircle method. An ellipse is defined by its bounding rectangle: DrawEllipse draws the ellipse that fits exactly inside the rectangle you give it. A rectangle longer than it is tall gives a flat ellipse; a square gives a circle.
g.DrawEllipse(myPen, 300, 200, 50, 50); // the bounding rectangle: x, y, width, heightPredict: what shape is it, where is its centre, and does it touch the rectangle from the previous example?
- Width and height are equal, so it is a circle with radius 50 / 2 = 25.
- Its centre is the centre of the box:
(300 + 25, 200 + 25)=(325, 225). - Its leftmost point is
(300, 225). The rectangle's right side is the linex = 300fromy = 100toy = 250, and 225 is inside that range. So the circle touches the rectangle from the outside. You can see it in the screenshot a little further down.
DrawArc: a part of an ellipse
An arc is a portion of an ellipse. DrawArc takes the same bounding rectangle as DrawEllipse, plus two angles in degrees:
g.DrawArc(myPen, x, y, width, height, startAngle, sweepAngle);startAngleis measured from the x-axis through the middle of the box.0is the point on the right.sweepAngleis how far the arc goes from there, clockwise. Clockwise becauseygrows down: 90 degrees from the right-hand point is the bottom point.- A negative sweep draws in the other direction, counter-clockwise.
Worked example: four arcs
We drew four arcs in four 100 by 100 boxes (the grey squares), with a black pen of width 3:
int[,] angles = { { 0, 90 }, { 0, 180 }, { 0, -90 }, { 90, 180 } };
for (int i = 0; i < 4; i++)
{
int x = 30 + i * 130;
g.DrawRectangle(boxPen, x, 30, 100, 100);
g.DrawArc(arcPen, x, 30, 100, 100, angles[i, 0], angles[i, 1]);
g.DrawString(angles[i, 0] + ", " + angles[i, 1], font, Brushes.Black, x + 25, 145);
}Predict the part of the circle that each pair of angles draws before you look.

0, 90: from the right-hand point, a quarter turn clockwise down to the bottom point: the bottom-right quarter.0, 180: from the right point, half a turn clockwise through the bottom to the left point: the bottom half.0, -90: from the right point, a quarter turn counter-clockwise up to the top: the top-right quarter.90, 180: from the bottom point, half a turn clockwise through the left to the top: the left half.
Worked example: four more arcs
The same boxes with four other pairs of angles:
int[,] angles = { { 90, 180 }, { 180, 90 }, { 270, -90 }, { -90, 180 } };Predict: two of these draw exactly the same quarter. Which two?

90, 180: the left half, as before.180, 90: from the left point, a quarter turn clockwise up to the top: the top-left quarter.270, -90:270is the top point; a quarter turn counter-clockwise goes to the left point: the same top-left quarter, drawn in the opposite direction.-90, 180:-90is also the top point (270 and -90 are the same direction); half a turn clockwise through the right to the bottom: the right half.
Text is drawn too: DrawString
g.DrawString("Hello World", new Font("Arial", 16), myBrush, 150, 400);DrawString draws the text at a location with a brush and a font. Its arguments are the text, a Font (a family name and a size in points), a brush (letters are filled shapes, so they need a brush, not a pen), and the (x, y) of the top-left corner of the text.
Worked example: the whole picture
Put all five calls in one Paint handler, with a black pen and a red brush, on the 500 by 500 form:
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen myPen = new Pen(Color.Black);
SolidBrush myBrush = new SolidBrush(Color.Red);
g.DrawLine(myPen, 20, 20, 400, 420);
g.DrawRectangle(myPen, 100, 100, 200, 150);
g.DrawEllipse(myPen, 300, 200, 50, 50);
g.DrawArc(myPen, 400, 100, 50, 50, 0, 90);
g.DrawString("Hello World", new Font("Arial", 16), myBrush, 150, 400);
myPen.Dispose();
myBrush.Dispose();
}Predict: sketch the window on paper, then compare with the real run.

The circle touches the rectangle at (300, 225), the arc is the bottom-right quarter of its 50 by 50 box, and the text is red because DrawString uses the brush.
Worked example: draw, fill, or both?
Each Draw method that takes a pen has a Fill twin that takes a brush. We drew the same rectangle, ellipse and triangle three times each, with a blue pen of width 3 and an orange brush. Column 1 only draws, column 2 only fills, column 3 fills and then draws:
Pen bluePen = new Pen(Color.Blue, 3);
SolidBrush orangeBrush = new SolidBrush(Color.FromArgb(255, 255, 140, 0));
g.DrawRectangle(bluePen, 20, 20, 120, 80);
g.FillRectangle(orangeBrush, 170, 20, 120, 80);
g.FillRectangle(orangeBrush, 320, 20, 120, 80);
g.DrawRectangle(bluePen, 320, 20, 120, 80);
g.DrawEllipse(bluePen, 20, 120, 120, 80);
g.FillEllipse(orangeBrush, 170, 120, 120, 80);
g.FillEllipse(orangeBrush, 320, 120, 120, 80);
g.DrawEllipse(bluePen, 320, 120, 120, 80);
Point[] triangle = { new Point(80, 220), new Point(140, 300), new Point(20, 300) };
g.DrawPolygon(bluePen, triangle);
Point[] triangle2 = { new Point(230, 220), new Point(290, 300), new Point(170, 300) };
g.FillPolygon(orangeBrush, triangle2);
Point[] triangle3 = { new Point(380, 220), new Point(440, 300), new Point(320, 300) };
g.FillPolygon(orangeBrush, triangle3);
g.DrawPolygon(bluePen, triangle3);A polygon is given as an array of Point values; DrawPolygon joins them in order and closes the shape back to the first point.
Predict the nine shapes, then look:

Draw gives the outline only, Fill gives the inside only. To get both, fill first and draw second: in the other order the fill would paint over the inner half of the 3-pixel outline.
Interactive: the GDI+ playground
Pick a drawing method, drag on the form to place the shape (or type the numbers), and read the C# call that would draw it. Turn on the bounding rectangle for ellipses and arcs, and try negative sweep angles.
Part 2: when is the picture drawn?
The Paint event comes again and again
Windows does not keep a copy of what you drew. Whenever part of the form must be shown again, Windows asks the form to draw itself by raising the Paint event. That happens:
- when the form is first shown;
- when a covered part of the form is uncovered, or the form is resized;
- when code calls
Invalidate(), which means "this area is out of date, repaint it when you can"; - when code calls
Refresh(), which clears the area and repaints it immediately.
So the drawing code belongs in Paint: it runs again every time the picture is needed.
Worked example: Paint, Load and a button
This form draws three things in three places:
// Paint: a blue rectangle
private void Form1_Paint(object sender, PaintEventArgs e)
{
SolidBrush blueBrush = new SolidBrush(Color.RoyalBlue);
e.Graphics.FillRectangle(blueBrush, 20, 70, 150, 100);
blueBrush.Dispose();
}
// Load: a green circle, drawn with CreateGraphics
private void Form1_Load(object sender, EventArgs e)
{
SolidBrush greenBrush = new SolidBrush(Color.Green);
Graphics formGraphics = this.CreateGraphics();
formGraphics.FillEllipse(greenBrush, 200, 70, 100, 100);
greenBrush.Dispose();
formGraphics.Dispose();
}
// Draw circle button: a red circle, drawn with CreateGraphics
private void drawButton_Click(object sender, EventArgs e)
{
SolidBrush redBrush = new SolidBrush(Color.Red);
Graphics formGraphics = this.CreateGraphics();
formGraphics.FillEllipse(redBrush, 330, 70, 100, 100);
redBrush.Dispose();
formGraphics.Dispose();
}
// Refresh button
private void refreshButton_Click(object sender, EventArgs e)
{
this.Refresh();
}Predict: what does the window show when it opens? Then after a click on Draw circle? Then after a click on Refresh?
When it opens:

The green circle never appears. In Load the form is not on the screen yet, so there is nothing to draw on, and the first Paint then draws the form's real picture.
After Draw circle:

After Refresh:

Refresh() cleared the form and raised Paint. Paint draws the blue rectangle again, but nobody draws the red circle again, so it is gone. The same happens when the window is resized or covered: a drawing made with CreateGraphics is not redrawn.
| Drawn in | After the next repaint |
|---|---|
Paint, with e.Graphics | drawn again: it stays |
a click handler, with CreateGraphics | erased: nobody draws it again |
Load, with CreateGraphics | never seen |
CreateGraphics is still useful for a quick, temporary drawing on a form that is already visible. But anything that must stay on the screen belongs in Paint, and the code that changes it should call Invalidate() or Refresh() instead of drawing directly.
Part 3: patterns from numbers
The line patterns app
The next app asks the user for four numbers and draws a pattern of lines from them:
- Number of lines: how many lines to draw.
- Angle: after each line, the direction turns by this many degrees.
- Length: the length of the first line, in pixels.
- Increment: after each line, the length grows by this many pixels.
The form holds two panels. The top one, controlPanel, has its Dock property set to Top: it snaps to the top edge and keeps the four labels, the four text boxes (numLines, angle, length, increment, with TextAlign set to Right because they hold numbers) and the Go button. The second one, canvas, has Dock set to Fill: it takes the rest of the form, and its BackColor is white. Docked panels stay in place when the window is resized. We handle the canvas's Paint event (select the panel, then the lightning bolt), not the form's.

The Go button only asks for a repaint:
private void goButton_Click(object sender, EventArgs e)
{
canvas.Refresh(); // clear the canvas and raise its Paint event
}One line from an angle and a length
Every line starts at (x1, y1), the end of the previous line, and has a length L and a direction θ in degrees. Its end (x2, y2) is:
x2 = x1 + L · cos(θ · π / 180)
y2 = y1 + L · sin(θ · π / 180)After each line the direction turns by α (the Angle box) and the length grows by d (the Increment box):
θ(next) = θ + α
L(next) = L + dMath.Cos and Math.Sin take the angle in radians, not degrees, so the degrees are multiplied by π / 180 (Math.PI / 180) first. Because y grows down on a form, an angle of 90 degrees points down.
The Paint handler
private void canvas_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
Pen myPen = new Pen(Color.Black, 1);
int lines = int.Parse(numLines.Text);
int turn = int.Parse(angle.Text);
int myLength = int.Parse(length.Text);
int step = int.Parse(increment.Text);
int startX = canvas.Width / 2; // start at the centre of the canvas
int startY = canvas.Height / 2;
int myAngle = 0; // the first line points right
for (int i = 0; i < lines; i++)
{
int endX = (int)(startX + Math.Cos(myAngle * Math.PI / 180) * myLength);
int endY = (int)(startY + Math.Sin(myAngle * Math.PI / 180) * myLength);
Point[] points = { new Point(startX, startY), new Point(endX, endY) };
g.DrawLines(myPen, points);
startX = endX; // the next line starts where this one ended
startY = endY;
myAngle = myAngle + turn;
myLength = myLength + step;
}
myPen.Dispose();
}Text is a string, so each value goes through int.Parse first. DrawLines joins the points of an array in order; with two points it draws one line. The (int) cast turns the double result of Math.Cos and Math.Sin into a whole pixel number, and it does that by cutting off the fraction, not by rounding. Because the values are read inside Paint, the canvas also draws the default pattern when the form first opens.
Worked example: 4 lines, angle 90, length 100, increment 0
Our canvas is 560 by 400 pixels, so the centre is (280, 200). Predict the four end points.
| Line | θ | From | To |
|---|---|---|---|
| 1 | 0 | (280, 200) | (380, 200) |
| 2 | 90 | (380, 200) | (380, 300) |
| 3 | 180 | (380, 300) | (280, 300) |
| 4 | 270 | (280, 300) | (280, 200) |
The table is the program's own output (it prints the first lines of every pattern). Right, down, left, up: a square, drawn clockwise because y grows down. It is the square in the screenshot above.
Worked example: 100 lines, angle 74, length 5, increment 1
Line 1 has θ = 0 and L = 5, so it goes from (280, 200) to (285, 200). Predict the end of line 2, where θ = 74 and L = 6.
x2 = 285 + 6 · cos(74°) = 285 + 1.65 = 286.65 → 286
y2 = 200 + 6 · sin(74°) = 200 + 5.77 = 205.77 → 205The program printed line 2: angle 74, length 6: (285, 200) -> (286, 205). Each line is one pixel longer than the one before and turns 74 degrees, so the lines wind outwards:

With 500 lines instead of 100 the same rule continues until the lines run off the canvas:

And 100 lines at 90 degrees, starting at length 5 with increment 3, give a square spiral:

Worked example: 3 lines, angle 120, length 150, increment 0
Predict: does the third line end exactly where the first one began?
| Line | From | To |
|---|---|---|
| 1 | (280, 200) | (430, 200) |
| 2 | (430, 200) | (355, 329) |
| 3 | (355, 329) | (279, 199) |

It is a triangle, but it ends at (279, 199), one pixel away from (280, 200) in each direction. The exact end of line 2 is (355.0, 329.9), cut to (355, 329), and every (int) cut adds a little error. On a pattern of hundreds of lines those small errors are part of what you see.
Interactive: patterns line by line
Type the four numbers (or pick a preset), then play the pattern one line at a time. The panel shows the calculation of the next line with the same formula and the same (int) cut as the C# program; its first lines match the program's output for every pattern above.
Part 4: animation with a Timer
The Timer control
Animation is a series of still pictures, each a little different from the one before, shown quickly one after another. In Windows Forms the tool for "do something again every few milliseconds" is the Timer control. Drag it from the Toolbox onto the form: it does not appear on the form itself but in a tray below the design surface.
| Member | Meaning |
|---|---|
Interval | the time between two Tick events, in milliseconds |
Enabled | true: the timer is running; false: it is stopped |
Tick | the event raised every Interval milliseconds; double-click the timer to create its handler |
A square that moves
int x = 10;
int y = 70;
private void Form1_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
SolidBrush redBrush = new SolidBrush(Color.Red);
g.FillRectangle(redBrush, x, y, 50, 50);
g.DrawString("x = " + x, this.Font, Brushes.Black, 10, 10);
redBrush.Dispose();
}
private void timer1_Tick(object sender, EventArgs e) // Interval = 50, Enabled = True
{
x += 5;
this.Invalidate(); // ask Windows to repaint the form
}The position of the square is kept in fields, x and y, so that both handlers can use them: Paint reads them and Tick changes them. Every Tick moves x 5 pixels to the right and calls Invalidate(), and the Paint that follows draws the square at the new x. Brushes.Black is a ready-made black brush.
Worked example: three moments
Predict: x starts at 10. What is x after 20 ticks?
x = 10 + 5 · 20 = 110. Our program logs every tenth tick, and it printed tick 20 at 1263 ms: x = 110. Three screenshots of the running form, taken about one second apart:

Worked example: delete Invalidate()
Now the Tick handler is only:
private void timer1_Tick(object sender, EventArgs e)
{
x += 5;
}Predict: what does the window show?

tick 10 at 642 ms: x = 60
tick 30 at 1909 ms: x = 160
tick 50 at 3177 ms: x = 260The log of the same run shows that the timer runs and x keeps growing. But nothing asks for a new Paint, so the screen keeps showing the old picture, including the old text x = 10. A change to a variable is not a change on the screen until something repaints.
How fast does it move?
If every tick came exactly on time, the speed would be:
v = s · 1000 / Twhere v is the speed in pixels per second, s is the step per tick in pixels, and T is the Interval in milliseconds (there are 1000 milliseconds in a second, so 1000 / T ticks come per second). For our square: v = 5 · 1000 / 50 = 100 pixels per second.
We measured the real time between ticks in our programs, on the Windows machine that ran them:
| Interval | Measured time between ticks | Ticks measured | Program |
|---|---|---|---|
| 10 ms | 16.4 ms | 250 | growing panel |
| 25 ms | 31.7 ms | 180 | three labels |
| 50 ms | 62.6 ms | 40 | moving square |
| 50 ms | 62.8 ms | 59 | running figure |
| 100 ms | 110.8 ms | 28 | running figure |
Every tick came later than the Interval. So the square really moved 5 pixels every 62.6 ms, about 80 pixels per second, not 100. Interval is the time you ask for between ticks; the program should not count on getting exactly that.
Smoother motion: DoubleBuffered
Every Paint first clears the form and then draws the square again. On some screens the eye catches the moment in between, and the moving object flickers. Setting the form's DoubleBuffered property to True (in the Properties window, or this.DoubleBuffered = true; in the constructor) makes the form draw the whole new picture off screen first and then show it in one step. All our moving programs set it.
Interactive: Interval, step and Invalidate
Change the Interval and the step while the square moves, watch the ticks on the one-second ruler, and switch Invalidate() off to see x change while the screen does not. Step runs one Tick at a time.
Part 5: bounce, keys and frames
Move a control and bounce
We do not always need Paint to animate. A control has a position of its own, its Left and Top properties, and changing them moves the control. This form has three large labels (bold, size 20) and a Timer with Interval 25 and Enabled True. Each moving label gets its own pair of speed fields:
int L1x = 5; // label1: speed on x
int L1y = 5; // label1: speed on y
private void timer1_Tick(object sender, EventArgs e)
{
label1.Left -= L1x; // this will move it horizontally
label1.Top -= L1y; // this will move it vertically
if (label1.Left + label1.Width > ClientSize.Width || label1.Left < 0)
{
L1x = -L1x; // bounce from the left or right border
}
if (label1.Top + label1.Height > ClientSize.Height || label1.Top < 0)
{
L1y = -L1y; // bounce from the top or bottom border
}
}The fields are declared inside the class but outside every method, so every method of the form can use them. The first if asks: has the label's right edge (Left + Width) passed the right side of the client area, or (||) has its left edge passed the left side? If so, the horizontal speed changes sign, and the next ticks move the label the other way. The second if does the same for the top and bottom.
Worked example: trace the bounce
A narrow form with ClientSize.Width = 300. labelA starts at Left = 230, is 50 pixels wide, and moves with speed Ax = 10, using the same if as above:
labelA.Left += Ax;
if (labelA.Left + labelA.Width > ClientSize.Width || labelA.Left < 0)
{
Ax = -Ax;
}Predict Left and the speed after each of ticks 1 to 5.
| Tick | Left after the move | Left + Width | Greater than 300? | Speed after the tick |
|---|---|---|---|---|
| 1 | 240 | 290 | no | 10 |
| 2 | 250 | 300 | no (equal is not greater) | 10 |
| 3 | 260 | 310 | yes | -10 |
| 4 | 250 | 300 | no | -10 |
| 5 | 240 | 290 | no | -10 |
The program logged exactly these values, for example tick 3: labelA.Left = 260, Ax = -10. Notice the order: the label moves first, then the check runs. On tick 3 the label's right edge is at 310, 10 pixels past the edge, for one tick before it comes back. A faster object pokes out further.
Many objects, one timer
The same Tick handler can move many objects. label2 gets its own speed pair and the same two if statements:
int L2x = 5;
int L2y = 5;
// in timer1_Tick, after label1
label2.Left += L2x;
label2.Top += L2y;
if (label2.Left + label2.Width > ClientSize.Width || label2.Left < 0)
{
L2x = -L2x;
}
if (label2.Top + label2.Height > ClientSize.Height || label2.Top < 0)
{
L2y = -L2y;
}label1 subtracts its speeds and label2 adds them, so they start in opposite directions: label1 up and to the left, label2 down and to the right. label3 has no code yet, so it stays where it is. Four screenshots of the running form:

Each object needs its own speed variables: if label2 reused L1x, a bounce of label1 would also turn label2 around. The labels are 92 by 32 pixels and the client area is 420 by 300, as the program printed when it started.
Steer with the arrow keys
Instead of bouncing, the user can choose the direction. An enum lists the four directions, a field remembers the current one, and the form's KeyDown event (Events list, lightning bolt) changes it:
enum Position
{
Left, Right, Up, Down
}
Position objectPosition = Position.Right;
private void Form1_KeyDown(object sender, KeyEventArgs e)
{
if (e.KeyCode == Keys.Left) objectPosition = Position.Left;
else if (e.KeyCode == Keys.Right) objectPosition = Position.Right;
else if (e.KeyCode == Keys.Up) objectPosition = Position.Up;
else if (e.KeyCode == Keys.Down) objectPosition = Position.Down;
}
private void timer1_Tick(object sender, EventArgs e)
{
if (objectPosition == Position.Right) x += 5;
else if (objectPosition == Position.Left) x -= 5;
else if (objectPosition == Position.Up) y -= 5;
else if (objectPosition == Position.Down) y += 5;
this.Invalidate();
}KeyDown only chooses the direction; Tick still does the moving, at a steady speed, whether a key is held or not.
A picture instead of a shape
Instead of a red square we can draw a picture from a file with DrawImage:
Bitmap picture = new Bitmap("mushroom.png"); // loaded once, drawn on every Paint
private void Form1_Paint(object sender, PaintEventArgs e)
{
e.Graphics.DrawImage(picture, x, y, 64, 64); // the image, where, and its size
}- Add the image to the project (right-click the project, Add, Existing Item, and choose "All Files" to see image files).
- In the image's Properties, set Copy to Output Directory to Copy if newer. Without this the file is not copied next to the
.exe, and the program cannot find it.
We load the Bitmap once, in a field, rather than inside Paint: Paint runs many times a second, and loading the file every time would repeat the same work again and again.
We ran this program and pressed Down, Left and Up about 1.5 seconds apart. (The lab machine has no keyboard, so a small helper program sent the same key messages Windows sends for real key presses.) The program printed KeyDown Down at x = 170, y = 20, KeyDown Left at x = 170, y = 140 and KeyDown Up at x = 50, y = 140:

Frame-by-frame animation
A second kind of animation changes the picture instead of the position. A running figure is eight pictures, run0.png to run7.png, shown one after another in a loop. The form has a PictureBox named frameImage (with SizeMode set to StretchImage), a TrackBar named speedBar, and a Timer named animationTimer with Interval 50 and Enabled True.
private string[] frames = { "frames/run0.png", "frames/run1.png", "frames/run2.png", "frames/run3.png",
"frames/run4.png", "frames/run5.png", "frames/run6.png", "frames/run7.png" };
private Bitmap[] images;
private int currentFrame = 0;
public Form1()
{
InitializeComponent();
images = new Bitmap[frames.Length];
for (int i = 0; i < frames.Length; i++)
images[i] = new Bitmap(frames[i]);
frameImage.Image = images[0];
}
private void animationTimer_Tick(object sender, EventArgs e)
{
currentFrame = (currentFrame + 1) % frames.Length; // 0, 1, ..., 7, 0, 1, ...
frameImage.Image = images[currentFrame];
infoLabel.Text = "frame " + currentFrame + ", interval " + animationTimer.Interval + " ms";
}The % operator gives the remainder of a division. (7 + 1) % 8 is 0, so after the last frame the counter goes back to the first one without any if. The eight frame files are set to Copy if newer, like the mushroom.
![]()
Worked example: move the TrackBar
The TrackBar has Minimum 50 and Maximum 100. Its handler copies its value into the timer's interval:
private void speedBar_ValueChanged(object sender, EventArgs e)
{
animationTimer.Interval = speedBar.Value;
}(ValueChanged runs whenever the value changes. The Scroll event, which runs only when the user moves the bar with the mouse or keyboard, works just as well here.)
Predict: the user moves the bar to the right end, 100. Does the figure run faster or slower?
![]()
Slower. A bigger interval means a longer wait between ticks, so fewer frames per second. In our run the time between frames went from 62.8 ms at 50 to 110.8 ms at 100. On this bar, the right end is the slowest setting.
Worked example: a while loop instead of a Timer
Two ways to grow a blue panel, pnlMain, from 0 by 0 to 300 by 200 pixels, one pixel at a time. The first uses a Timer with Interval 10, started by the Timer button:
private void btnAnimation1_Click(object sender, EventArgs e)
{
tmrMain.Enabled = true;
}
private void tmrMain_Tick(object sender, EventArgs e)
{
if (pnlMain.Width < targetWidth) pnlMain.Width++;
if (pnlMain.Height < targetHeight) pnlMain.Height++;
}The second uses a while loop inside the click handler of the While loop button:
private void btnAnimation2_Click(object sender, EventArgs e)
{
pnlMain.Height = targetHeight;
while (pnlMain.Width < targetWidth)
{
pnlMain.Width++;
}
}Predict: both change Width one pixel at a time. Do both look like animation?

No. The Timer version grows visibly: its log shows 50 by 50 after 818 ms and the full 300 by 200 after 4912 ms. The while loop finished in 67.8 ms and the panel appeared full at once. The form can repaint only after the click handler returns, and by then the loop has already reached 300. A Timer gives the form a chance to repaint between every two steps, and that is what makes the steps visible.
Quick check
g.DrawRectangle(p, 50, 40, 100, 60): where is the bottom-right corner?- Which call draws the top half of a circle in the bounding rectangle
r:g.DrawArc(p, r, 0, 180)org.DrawArc(p, r, 180, 180)? - Text is drawn in a button's Click handler with
CreateGraphics. What can happen to it when the form is resized? Interval = 40and the step is 4 pixels. What is the speed if every tick comes on time?- A Tick handler changes
x, but the square does not move on the screen. Which line is missing?
Practice
About 30 minutes. Answer on paper first, then build the program and run it to check.
Practice 1: draw a house (about 7 minutes)
Pen myPen = new Pen(Color.Black, 2);
SolidBrush doorBrush = new SolidBrush(Color.SaddleBrown);
SolidBrush windowBrush = new SolidBrush(Color.Gold);
g.DrawRectangle(myPen, 60, 120, 200, 140); // walls
Point[] roof = { new Point(60, 120), new Point(160, 40), new Point(260, 120) };
g.DrawPolygon(myPen, roof); // roof
g.FillRectangle(doorBrush, 140, 190, 40, 70); // door
g.FillEllipse(windowBrush, 90, 150, 30, 30); // round window
g.DrawString("Home", new Font("Arial", 14), Brushes.Black, 135, 8);- Give the four corners of the walls and the peak of the roof.
- Does the door touch the bottom wall? Show the sum.
- Give the centre and the radius of the window.
Practice 2: bounce to the left (about 7 minutes)
In the same narrow form as the bounce trace (ClientSize.Width = 300), labelB starts at Left = 20, is 40 pixels wide, and moves with speed Bx = -8, with the same if as label1.
- Write
Leftand the speed after each of ticks 1 to 5. - On which tick does the speed change sign, and which half of the
||condition is true then? - What is the smallest
Leftthe label ever has?
Practice 3: predict the pattern (about 6 minutes)
In the line patterns app, type 5 lines, Angle 144, Length 150, Increment 0.
- Compute the end of line 1 and of line 2. The centre is
(280, 200). - Name the shape.
- Does line 5 end exactly at
(280, 200)?
Practice 4: move label3 (about 10 minutes)
Start from the three-label program, where label1 and label2 already bounce.
- Add two fields,
L3x = 3andL3y = 7, and make label3 move with+=and bounce off all four edges. - label3 starts at
Left = 150andTop = 160, it is 92 by 32 pixels, and the client area is 420 by 300. Predict: on which tick does label3 bounce for the first time, and off which edge?
Answers
Quick check
(150, 100): 50 + 100 and 40 + 60.g.DrawArc(p, r, 180, 180): it starts at the left point and sweeps clockwise over the top to the right point.0, 180draws the bottom half.- It can be erased: resizing raises Paint, and Paint does not draw it again.
- 4 · 1000 / 40 = 100 pixels per second.
this.Invalidate();at the end of the Tick handler.
Practice 1
- Walls:
(60, 120)and(260, 120)at the top,(60, 260)and(260, 260)at the bottom (60 + 200 = 260, 120 + 140 = 260). The roof's peak is(160, 40), the middle point of the polygon. - The door's bottom edge is at 190 + 70 = 260, the same as the bottom of the walls, so it stands on the bottom wall.
- The window's bounding box is 30 by 30 at
(90, 150): a circle with centre(105, 165)and radius 15.

Practice 2
| Tick | Left | Speed after the tick |
|---|---|---|
| 1 | 12 | -8 |
| 2 | 4 | -8 |
| 3 | -4 | 8 |
| 4 | 4 | 8 |
| 5 | 12 | 8 |
On tick 3 Left becomes -4, so the second half of the condition (Left below 0) is true, and the speed changes to 8. The smallest Left is -4: for one tick, 4 pixels of the label are outside the form. These are the values our program logged, for example tick 3: labelB.Left = -4, Bx = 8.
Practice 3
- Line 1:
θ = 0, from(280, 200)to(430, 200). - Line 2:
θ = 144. x2 = 430 + 150 · cos(144°) = 430 - 121.35 = 308.65, cut to 308; y2 = 200 + 150 · sin(144°) = 200 + 88.17 = 288.17, cut to 288. So line 2 goes from(430, 200)to(308, 288). - It is a five-pointed star. The five turns add up to 5 · 144 = 720 degrees, two full turns, so the pen crosses its own path.
- No. Line 5 ends at
(278, 198), as the program printed: every(int)cut loses a little.

Practice 4
int L3x = 3;
int L3y = 7;
// in timer1_Tick, after label2
label3.Left += L3x;
label3.Top += L3y;
if (label3.Left + label3.Width > ClientSize.Width || label3.Left < 0)
{
L3x = -L3x;
}
if (label3.Top + label3.Height > ClientSize.Height || label3.Top < 0)
{
L3y = -L3y;
}After k ticks the top is at 160 + 7k, and the bottom edge 32 pixels lower. The bottom check is true when 160 + 7k + 32 is greater than 300, that is when k is greater than 15.4. So label3 bounces on tick 16, off the bottom edge: Top = 272, and 272 + 32 = 304 is greater than 300. The right edge is still far away: Left is only 198 then, and the right check needs 150 + 3k + 92 greater than 420, that is k greater than 59.3. Our program's log agrees: tick 16 at 811 ms: label3 (198, 272) L3x 3 L3y -7, and on tick 17 the top is back at 265.
