Using GDI To Draw Graphics

This tutorial takes you through the basics of using GDI+ to draw your own graphics to a form or control.

Download: GDIDraw.zip - Demonstrates drawing, antialiasing, coordinate translation, and repainting.
Download: GDIAnimation.zip - Demonstrates double buffering, animation, and factory drawing.

Unzip all files (executables plus dependencies) into a single folder

Frequently there just isn't a widget that does what you need. Using GDI+ to draw your own user interface components helps to open up the possibilities for user interaction in your application. GDI+ requires a basic understanding of how to use primitives (lines, ellipses, and rectangles) in a coordinate system to compose your desired output.

The GDI+ Coordinate System

If you've created visual C# applications you've already used the GDI+ coordinate system. A component's Location property is the X and Y coordinate relative to the parent form.

Several data structures exist in the System.Drawing namespace to store coordinates, dimensions, and areas.

  • The Point object stores an integer X and Y coordinate.
  • The PointF object stores a floating-point X and Y coordinate.
  • The Size object stores an integer Width and Height value.
  • The SizeF object stores a floating-point Width and Height value.
  • The Rectangle object is an aggregation of a Point (X, Y) and a Size (Width, Height) to give a sense of area.
  • The RectangleF object is an aggregation of a PointF (X, Y) and SizeF (Width, Height) to give a sense of area.

Every Form or Control has a main panel called a Client Area, where sub-controls are placed, or the control itself is drawn. Think of this panel as a blank canvas for you to paint on. In order to specify the location of primitives, you must specify an X and Y coordinate.

  • These coordinates are relative to the client area of the form or control, so the top lefthand corner is (0, 0). * X coordinates increase from Left to Right, Y coordinates increase from Top to Bottom.
  • You can use negative X and Y coordinates to place something beyond the top or left boundaries of the panel.
  • The furthest right and bottom coordinate can be found by accessing the panel's Width and Height properties respectively.
  • A control's Bounds property returns a Rectangle object representing the area of the control.

C# provides methods for translating Control Coordinates to Screen Coordinates. Screen Coordinates are absolute coordinates on the screen where the top lefthand corner is (0, 0). For systems that utilize more than one monitor, the screen area is assumed to be a continuous rectangular area that spans both monitors.

  • The PointToScreen function takes a Point object (assumed relative to the control) and converts it to absolute coordinates on the screen, returning another Point object.
  • The PointToClient function takes a Point object (assumed relative to the screen) and convertis it to a coordinate relative to the control, returning another Point object.

The Paint Event

Forms and Controls have a Paint event which is called every time the control needs to be redrawn. For instance, when part of a control is occluded by some other object on the screen, the area that has been occluded needs to be redrawn when the occluding object is removed - thus the control is asked to repaint itself. The Paint event is called whenever this repaint is requested, thus it is the place where you should place your code to draw the control.

  • You can force a repaint event by calling the control's Refresh() function.
  • Repainting of the current control and all sub-controls can be suspended by calling the SuspendLayout() function, and resumed again by calling ResumeLayout().

The Graphics Interface

Forms and controls each have a CreateGraphics() function, which returns an instance of System.Drawing.Graphics, an object used to conduct drawing to the client area of the control.

Graphics g = this.CreateGraphics();

OR

Graphics g = MyControl.CreateGraphics();

The Graphics object also has a handy function for clearing the entire area of the canvas with a given color. You may wish to specify your own color, or use the default control color.

g.Clear( Color.Black );
g.Clear( Control.BackColor );

By default, the Graphics object draws without antialiasing - it is computationally cheaper, but results in jagged edges on drawn shapes. To turn on antialiasing, do the following function call before any drawing operations:

g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

Drawing Styles

Several objects exist to control color for different types of drawing:

  • The Color enumeration has a list of named web colors. This can be nice for quick access to colors whose names that you know (Red, Green, Blue, Yellow, Purple...), but you can create your own color from RGB values using the Color.FromArgb( r, g, b );, where r, g, and b are integers between 0 (lowest intensity) and 255 (highest intensity).
  • For lines/outlines, the Pen object stores a Color and thickness in pixels for the line. You can also use the Pens enumeration which gives a 1 pixel thick pen in any color from the Color enumeration.
  • For solids/fills, the SolidBrush object represent a solid Color. You can also use the Brushes enumeration which gives a SolidBrush in any color from the Color enumeration.
  • For text, the Font object represents the Font Face, Point Size, and optional font styles such as Bold and Italic. Styles are applied using a boolean OR (| operator) on the FontStyles enumeration.
Color c1 = Color.NavyBlue;
Color c2 = Color.FromArgb( 64, 64, 255 )// A darkish blue-gray
Pen p = new Pen( c1, 2 );
Brush b = new SolidBrush( c2 );
Font f = new Font("Times", 12, FontStyle.Bold | FontStyle.Italic | FontStyle.Underline);

Drawing Primitives

The Graphics object has methods for drawing operations.

  • The Graphics.DrawLine Function draws a line from:
    • A Pen, a starting (X,Y) and ending (X,Y).
    • A Pen, a starting Point, and an ending Point.
  • The Graphics.DrawRectangle Function draws the outline of a rectangle from:
    • A Pen and X, Y, Width, Height values.
    • A Pen, a Point, and a Size.
    • A Pen and a Rectangle
  • The Graphics.FillRectangle Function draws a solid rectangle from:
    • A Brush and X, Y, Width, Height values.
    • A Brush , a Point, and a Size.
    • A Brush and a Rectangle
  • The Graphics.DrawEllipse Function draws the outline of an ellipse. Note that the (X,Y) coordinates indicate the top left anchor for the ellipse, NOT the center point as you would probably expect. This is a minor annoyance that you must deal with. This primitive is drawn using the same parameters as from DrawRectangle.
  • The Graphics.FillEllipse Function draws a solid ellipse. Once again the (X,Y) coordinates indicate the top left anchor for the ellipse, NOT the center point. This primitive is drawn using the same parameters as from FillRectangle.
g.DrawLine( new Pen( Color.Blue, 3 ), 0, 0, 100, 100 );
g.DrawLine( new Pen( Color.Blue, 3 ), new Point(0, 0), new Point(100, 100) );

g.DrawRectangle( Pens.Red, 10, 10, 20, 20 )// Note the bottom corner is at (30,30).
g.DrawRectangle( Pens.Red, new Point(10, 10), new Size(20, 20) );
g.DrawRectangle( Pens.Red, new Rectangle(10, 10, 20, 20) );

g.FillRectangle( new SolidBrush( Color.Lime ), 50, 50, 20, 20 );

g.DrawEllipse( new Pen( Color.Purple, 10 ), 100, 100, 100, 100 );

g.FillEllipse( Brushes.Yellow, 10, 100, 20, 30 );

Drawing Text

The Graphics.DrawString Function draws text to the canvas using:

  • The String of text that you wish to draw.
  • A Font object.
  • A Brush.
  • A PointF object indicating the top lefthand corner of the text bounds OR a RectangleF representing a rectangular clipping area for the text.
  • OPTIONALLY: A System.Drawing.StringFormat object, which contains the style for horizontal and vertical alignment, text direction, clipping, line spacing, etc..
g.DrawString( "Hello world!", new Font("Times", 12, FontStyle.Regular), new SolidBrush( Color.Black ), new PointF(0, 0) );

StringFormat sf = new StringFormat();
sf.Alignment = StringAlignment.Center; // Horizontally center.  Near = left align, Far = right align.
sf.LineAlignment = StringAlignment.Center; // Vertically center.  Near = top align, Far = bottom align.

g.DrawRectangle( Pens.Red );
g.DrawString( "Hello world!", new Font("Times", 12, FontStyle.Regular), new SolidBrush( Color.Black ), new RectangleF(0, 0, 100, 100), sf ); // Text will wrap to horizontal bounds, clip to vertical bounds

You can measure the width of a graphically rendered string in pixels as follows:

int stringwidth = g.MeasureString( "Hello world!", new Font("Times", 12, FontStyle.Regular) );

Using Images

When you want to use an image in your control rather than drawing it manually, GDI+ lets you easily paste images to the canvas.

First load the desired image into your application as follows:

Image myimage = Image.FromFile("mypicture.jpg");

For simple image drawing, take a look at the Graphics.DrawImageUnscaled function. For more advanced options, such as image scaling, use Graphics.DrawImage. There are many overloads for both of these functions, so I leave it to the reader to investigate which overload satisfies their desired outcome.

GDI+ also lets you draw to images as if they were a canvas. To do this, you must create a Graphics object from the Image as follows:

Graphics gi = Graphics.FromImage( myimage );

You can use all the same GDI+ drawing functions as mentioned above - it functions exactly the same as drawing to a control's client area except your drawings are stored to an off-screen buffer. HINT: You can use Images as off-screen buffers for double-buffering your controls. Double buffering can be used to reduce flickering when drawing.

Drawing en Masse

Hard-coding GDI+ commands can only take you so far. Say you have a control that draws a 10x10 grid of circles - you're not going to enjoy writing out 100 DrawEllipse commands, especially when you have to manually enter coordinates for each one. Naturally you can use a nested loop to iterate over X and Y positions, but what if they don't conform to some pattern.

It's time to dredge up the old Software Engineering Factory Design pattern. Let's use an object oriented approach to store properties such as location, color, size etc. for primitives. We can have each one implement an interface so it is a simple matter to call on the object to render itself. Take a look at the following code:

// Our interface for easy drawing
public interface IDrawable
{
  void Draw( Graphics g );
}

public class Ellipse : IDrawable
{
  public Color c = Color.Black;
  public Point l = Point(0, 0);
  public Size s = Size(0, 0);
  public int stroke = 0;

  public Ellipse( Color c, Point l, Size s, int stroke )
  {
    this.c = c;
    this.l = l;
    this.s = s;
    this.stroke = stroke;
  }

  public void Draw( Graphics g )
  {
    if ( stroke == 0 )
    {
      g.FillEllipse( SolidBrush( c ), l, s );
    }
    else
    {
      g.DrawEllipse( new Pen( c, stroke ), l, s );
    }
  }
}

We can create similar objects that implement the Drawable interface for rectangles and lines. We can then store an ArrayList of these objects, and draw them whenever the Paint event is triggered.

private ArrayList alDrawingObjects = new ArrayList();
...

alDrawingObjects.Add( new Ellipse( Color.Red, new Point(100, 100), new Size(10, 10), 5 ) );
alDrawingObjects.Add( new Ellipse( Color.Blue, new Point(50, 50), new Size(20, 20), 0 ) );
...

Graphics g = this.CreateGraphis();

foreach ( IDrawable d in alDrawingObjects )
{
  d.Draw( g );
}