AddThis Social Bookmark Button

Print

Basic Drawing with GDI+

by Niklas Gustavsson
05/05/2003

The .NET framework and GDI+ introduce a new paradigm for drawing in Windows. It drastically simplifies your code, while still providing you with powerful access to the underlying Win32 routines through a rich set of classes. Most developers will find all they need in the classes provided by the framework, and advanced users can still use the old GDI via P/Invoke. And, possibly the best of all, you can use the same code for drawing in any type of application; for example, WinForms, ASP.NET, or web services.

So, without further ado ...

Enter the System.Drawing Namespace

All classes involved in drawing are collected into the System.Drawing namespace. It is in turn divided into a few additional namespaces: System.Drawing.Design for design-time image handling (in, for example, Visual Studio.NET), System.Drawing.Drawing2D for advanced two-dimensional drawing, System.Drawing.Imaging for bitmap and metafile handling, System.Drawing.Printing for drawing to a printer, and System.Drawing.Text for advanced font support.

In this article, we will try out some of the basic techniques for drawing to a bitmap image. We will create a console application written in C# that, based on a few parameters, draws an ellipse using different sizes and colors. In this sample, we use it from a command line, but it should be simple and straightforward to reuse this code in, for example, a web application.

Note: to get this code to compile, you need to add a reference to System.Drawing.dll.

To start off, we create a class called EllipseDrawer. This class could be part of a library of shape-drawing classes. The class has a single method, Draw:


public Image Draw(int width, int height, int strokeWidth, 
                  Color strokeColor, Color fillColor)
{
  // create the bitmap we will draw to
  Image image = new Bitmap(width + strokeWidth, 
                           height + strokeWidth);

  // calculate the half of the stroke width for use later
  float halfStrokeWidth = strokeWidth / 2F;

  // create a rectangle that bounds the ellipse we want to draw
  RectangleF ellipseBound = new RectangleF(
    halfStrokeWidth, halfStrokeWidth, 
    width, height);

  // create a Graphics object from the bitmap
  using(Graphics graphics = Graphics.FromImage(image))
  {
    // create a solid color brush
    using(Brush fillBrush = new SolidBrush(fillColor))
    {
      // fill the ellipse specified by the 
      // rectangle calculated above
      graphics.FillEllipse(fillBrush, ellipseBound);
    }

    // create a pen
    using(Pen pen = new Pen(strokeColor, strokeWidth))
    {
      // draw the stroke of the ellipse specified by 
      // the rectangle calculated above
      graphics.DrawEllipse(pen, ellipseBound);
    }
  }

  return image;
}

Let's dissect this method in detail and begin with its signature.

public Image Draw(int width, int height, int strokeWidth, Color strokeColor, Color fillColor)

The width and height parameters specify the size of the ellipse that will be drawn. strokeWidth and strokeColor set the width and color for the outline of the shape, and fillColor determines the color to use to fill the interior of the ellipse. A Color is defined by four values: red, green, blue, and an alpha value that sets the opacity. The framework also provides you with a large set of predefined colors (you might recognize them from, for example, HTML or SVG).

Also notice that we return an Image. Image is an abstract base class for all types of images. In the .NET framework, we have two types: bitmaps and metafiles. A bitmap is built up by a grid of pixels, while a metafile is a sequence of drawing instructions (a vector image). In this case, we use a bitmap, but by returning an Image, we make our method a bit more generic, in case we would like to change it and instead return a metafile in a future version.

Now let's move on to our actual code.

Image image = new Bitmap(width + strokeWidth, height + strokeWidth);

This creates our image, more specifically a bitmap image. The values we use in the constructor specify the size of the bitmap, measured in pixels. The reason why we're adding the stroke width to our width and height probably deserves some explaining.

When GDI+ draws our stroke, it puts half of the stroke's thickness on the outside of the ellipse we specify, and half on the inside. The figure below shows how it's done.

An ellipse with a stroke that is partially on the outside and inside
Figure 1. An ellipse with a stroke that is partially on the outside and inside

The methods for drawing ellipses use a rectangle to define the position and size.

An ellipse with a rectangle showing the bounds of the shape
Figure 2. An ellipse with a rectangle showing the bounds of the shape

So, to ensure that the entire shape fits in our image, we need to add the stroke width to both the width and height of the ellipse. And, to position the ellipse so the entire shape is visible, we need to calculate half of the stroke width and use that for our starting coordinates.

float halfStrokeWidth = strokeWidth / 2F;

We then create a rectangle that bounds the ellipse.

RectangleF ellipseBound = new RectangleF( halfStrokeWidth, halfStrokeWidth, width, height);

Now we're getting into the real magic.

using(Graphics graphics = Graphics.FromImage(image))

Graphics is the class around which the entire process of drawing revolves. Graphics is the drawing surface for any kind of drawing, whether on a WinForm control, a printer, or a bitmap. One of the great things about this design is that once you have your code for drawing, you can easily switch to drawing on a different target; for example, to add printing to your graphics application.

The Graphics class doesn't have any public constructor. Instead, you can create an instance using one of the static methods that starts with "From." In this case, we use FromImage.

When using an instance of the Graphics class, it is very important to always release the resources it uses after we're done with it. A good thing is that Graphics implements the IDisposable interface, which means we can use the using statement to automatically dispose of the object when it's no longer needed. The same goes for other classes in the System.Drawing namespace, as we will see below.

Next, we want to draw the actual ellipse. The Graphics class has two sets of methods, starting with either "Fill" or "Draw." In our case, we're interested in FillEllipse and DrawEllipse. FillEllipse will do just that, fill the area defined by the ellipse with a color (or a more advanced pattern). In the same way, DrawEllipse will draw the stroke of the ellipse.

GDI+ uses a "painter's model," which means that each thing you draw will simply be put on top of what's already on the drawing surface. This is a simple and easy to understand model, but it requires us to do some extra thinking about the order in which we do our drawing. As we saw in the figure above, half of the stroke will be on the inside of the ellipse, so to make sure that our fill doesn't cover half of the stroke, we must paint the fill first.

Filling is done, just like in non-digital painting, by using a brush. The framework contains an abstract base class, Brush, for the different types of brushes. We just want to paint with a single color, so our best choice is to use a SolidBrush. Other useful brush classes include LinearGradientBrush, for filling with a gradient, or TextureBrush, which can tile an image to fill an area. Haven't you always dreamed of a brush like the one use to paint a chessboard in Santa's Workshop?

Brushes also implement IDisposable, so we will use the using statement again.

using(Brush fillBrush = new SolidBrush(fillColor))

Next, we finally get to fill our ellipse.

graphics.FillEllipse(fillBrush, ellipseBound);

The only thing we have left to do is to draw the stroke. To do that, we use a Pen, which is basically a brush with a width. In our case, we use a constructor that takes a color and width, but you can also create it from a brush, which means that you can draw your stroke using a gradient or pattern.

By this time, you're probably well familiar with the using statement.

using(Pen pen = new Pen(strokeColor, strokeWidth))

And using our pen, we now draw our final piece of art.

graphics.DrawEllipse(pen, ellipseBound);

Last, but not least, we return the bitmap we have just created.

return image;

We're now all done with our code. For the full listing of the code, click here. The image below shows an example output from the program using the following command:

A red ellipse with a yellow stroke
Figure 3. A red ellipse with a yellow stroke

ellipseDrawer 50 100 20 yellow red

What's Next?

If you want to continue exploring GDI+, I would suggest taking a look at the many properties and methods in the Graphics class. For example, you can make the result of our drawing look nicer with anti-aliasing by using the SmoothingMode property. Try out some of the other draw and fill methods.

In my next article, I will look into the Graphics.DrawPath() method and the very powerful GraphicsPath class. Also, please email me suggestions for what you want me to cover in future articles, and I'll try to make your wishes come true.

Niklas Gustavsson is a molecular biologist that took a extended break and is now working as a system architect/developer with web, Java and NET development.


Return to ONDotnet.com