Understanding the System.Drawing
namespace
Finding out how the drawing classes fit into the .NET Framework
Using System.Drawing
to create a simple game application
No one is going to write the next edition of Bioshock using C#. It just isn't the kind of language you use to write graphics-intensive applications like shoot-'em-up games.
Still, C# packs a fair amount of power into the System.Drawing
classes. Though these classes are somewhat primitive in some areas, and using them might cause you to have to write a few more lines of code than you should, there isn't much that these classes can't do with sufficient work.
The drawing capability provided by the .NET Framework is divided into four logical areas by the namespace design provided by Microsoft. All the general drawing capability is in the System.Drawing
namespace. Then there are some specialized namespaces:
System.Drawing.2D
has advanced vector drawing functionality.
System.Drawing.Imaging
is mostly about using bitmap graphic formats, like .bmp
and .jpg
files.
System.Drawing.Text
deals with advanced typography.
In this chapter, I focus on the base namespace and cover only the basics of drawing in C#. (Discussing every aspect of drawing could easily fill an entire book.)
Even at the highest level, graphics programming consists of drawing polygons, filling them with color, and labeling them with text — all on a canvas of some sort. Unsurprisingly, this leaves you with four objects that form the core of the graphics code you write: graphics, pens, brushes, and text.
Generally speaking, the Graphics
class creates an object that is your palette. It's the canvas. All the methods and properties of the Graphics
object are designed to make the area you draw upon more appropriate for your needs.
Also, most of the graphics- and image-related methods of other classes in the framework provide the Graphics
object as output. For instance, you can call the System.Web.Forms.Control.CreateGraphics
method from a Windows Forms application and get a Graphics
object back that enables you to draw in a form control in your project. You can also handle the Paint
event of a form, and check out the Graphics
property of the event.
Graphics
objects use pens and brushes (discussed later in this chapter, in the "Pens" and "Brushes" sections) to draw and fill. Graphics objects have methods such as these:
DrawRectangle
Fi HRectangle
DrawCircle
FillCircle
DrawBezier
DrawLine
These methods accept pens and brushes as parameters. You might think, "How can a circle help me?" but you must remember that even complex graphic objects such as the Covenant in Halo 3 are made up of circles and rectangles — thousands of them. The trick to useful art is using math to put together lots of circles and squares until you have a complete image. The sample application described later in this chapter is a simple example of just that.
You use pens to draw lines and curves. Complex graphics are made up of polygons, and those polygons are made of lines, and those lines are generated by pens. Pens have properties such as
Color
DashStyle
EndCap
Width
You get the idea: You use pens to draw things. These properties are used by the pens to determine how things are drawn.
Brushes paint the insides of polygons. Though you use the pens to draw the shapes, you use brushes to fill in the shapes with gradients, patterns, or colors. Brushes are usually passed in as parameters to a Draw
Whatever method
of the pen objects. When the pen draws the shape it was asked to draw, it uses the brush to fill in the shape — just as you did in kindergarten with crayons and coloring books. (The brush object always stays inside the lines, though.)
Don't look for the Brush
class, however. It's a holding area for the real brushes, which have kind of strange names. Brushes are made to be customized, but you can do a lot with the brushes that come with the framework as is. Some of the brushes include
Although the pens are used to pass into the Draw
methods of the Graphics
object, brushes are used to pass into the Fill
methods that form polygons.
Text is painted with a combination of fonts and brushes. Just like pens, the Font
class uses brushes to fill in the lines of a text operation.
System.Drawing.Text
has collections of all the fonts installed in the system running your program, or installed as part of your application. System.Drawing.Font
has all the properties of the typography, such as
Bold
Size
Style
Underline
The Graphics
object, again, provides the writing of the text on the palette.
The System.Drawing
namespace breaks drawing into two steps:
Create a System.Drawing.Graphics object.
Use the tools in the System.Drawing namespace to draw on it.
It seems straightforward, and it is. The first step is to get a Graphics
object. Graphics
objects come from two main places — existing images and Windows Forms.
To get a Graphics
object from an existing image, look at the Bitmap
object. The Bitmap
object is a great tool that enables you to create an object using an existing image file. This gives you a new palette that is based on a bitmap image (a JPEG file, for example) that is already on your hard drive. It's a convenient tool, especially for Web images.
Bitmap currentBitmap = new Bitmap(@"c:imagesmyImage.jpg"); Graphics palette = Graphics.FromImage(currentBitmap);
Now the object myPalette
is a Graphics
object whose height and width are based on the image in myBitmap
. What's more, the base of the myPalette
image looks exactly like the image referenced in the myBitmap
object.
You can use the pens, brushes, and fonts in the Graphics
class to draw directly on that image, as though it were a blank canvas. I use it to put text on images before I show them on Web pages and to modify the format of images on the fly, too.
Another way to get a Graphics
object is to get it from Windows Forms. The method you want is System.Windows.Forms.Control.CreateGraphics
. This method gives you a new palette that is based on the drawing surface of the control being referenced. If it's a form, it inherits the height and width of the form and has the form background color. You can use pens and brushes to draw right on the form.
When you have a Graphics
object, the options are endless. Sophisticated drawing isn't out of the question, though you would have to do a ton of work to create graphics like you see in Halo 2 using Visual Studio. (There isn't a Master Chief class that you can just generate automatically.)
Nonetheless, even the most complex 3D graphics are just colored polygons, and you can make those with the System.Drawing
class. In the following sections, I build a cribbage board with a Graphics
object, pens, brushes, and fonts.
Good applications come from strange places. Gabrielle (my wife) and I enjoy games, and one of our favorites is the card game cribbage. We were on vacation in Disney World when she had the urge to play, but we didn't have a cribbage board. We had cards, but not the board.
However, I did have my laptop, Visual Studio, and the System.Drawing
namespace. After just an hour or two of work, I built an application that serves as a working cribbage board!
It's shocking, I know, but somehow she still wanted to play me after watching me program for two hours!
This application is fairly complete, and I don't have enough pages to walk you through it step by step. Load the application from this book's Web site, and follow along with the rest of this chapter. This application isn't complex, but it's long.
Cribbage is a card game where hands are counted up into points, and the first player to score 121 points wins. It's up to the players to count up the points, and the score is kept on a board.
Cribbage boards are made up of two lines of holes for pegs, usually totaling 120, but sometimes 60 holes are used and you play through twice. Figure 5-1 shows a typical cribbage board. Cribbage boards come in a bunch of styles — check out www.cribbage.org
if you're curious; it has a great gallery of almost 100 boards, from basic to whimsical.
For this example, I create the board image for an application that keeps score of a cribbage game — but it wouldn't be beyond C# to write the cards into the game too!
So the board for this application has 40 holes on each of three pairs of lines, which is the standard board setup for two players playing to 120, as shown in Figure 5-2. The first task is to draw the board, and then to draw the pegs as the players' scores — entered in text boxes — change.
The premise is this: The players play a hand and enter the resulting scores in the text box below their respective names (refer to Figure 5-2). When the score for each hand is entered, the score next to the player's name is updated, and the peg is moved on the board. The next time that same player scores a hand, the peg is moved forward, and the back peg is moved into its place. Did I mention the back peg? Oh, yes, the inventor of cribbage was paranoid of cheating — if you're unfamiliar with cribbage, you may want to check out the rules at www.cribbage.org
.
To begin, create a playing surface. I set up the board shown in Figure 5-2 without drawing the board itself — I show you how to paint it on later with System.Drawing
. My board looked a lot like Figure 5-3 when I was ready to start with the business rules.
I used a little subroutine to handle score changes by calling it from the two text boxes' OnChange
events. Here's the code that calls the subroutine:
private void HandleScore(TextBox scoreBox, Label points, Label otherPlayer) { try { if (0 > (int)scoreBox.Text | (int)scoreBox.Text > 27) { ScoreCheck.SetError(scoreBox, "Score must be between 0 and 27"); scoreBox.Focus(); }
else { ScoreCheck.SetError(scoreBox, ""); //Add the score written to the points points.Text = (int)points.Text + (int)scoreBox.Text; } } catch (System.InvalidCastException ext) { //Something other than a number if (scoreBox.Text.Length > 0) { ScoreCheck.SetError(scoreBox, "Score must be a number"); } } catch (Exception ex) { //Eek! MessageBox.Show("Something went wrong! " + ex.Message); } //Check the score if ((int)points.Text > 120) { if ((int)points.Text / (int)otherPlayer.Text > 1.5) { WinMessage.Text = scoreBox.Name.Substring(0, scoreBox.Name.Length − 6) + " Skunked 'em!!!"; } else { WinMessage.Text = scoreBox.Name.Substring(0, scoreBox.Name.Length − 6) + " Won!!"; } WinMessage.Visible = true; } }
All this changing of screen values causes the Paint event of the form to fire — every time C# needs to change the look of a form for any reason, this event fires — so I just tossed a little code in that event handler that would draw my board for me:
private void CribbageBoard_Paint(object sender, PaintEventArgs e) { PaintBoard(BillsPoints, GabriellesPoints); }
From that point on, my largest concern is drawing the board itself.
I need to paint right on a form to create the image of the board for my crib-bage application, so I use the CreateGraphics
method of the form control. From there, I need to complete these tasks:
Paint the board brown using a brush.
Draw six rows of little circles using a pen.
Fill in the hole if that is the right score.
Clean up my supplies.
To that end, I came up with the PaintBoard
method, which accepts the labels that contain the standing scores for both players. It's shown in Listing 5-1.
Example 5-1. The PaintBoard Method
private void PaintBoard(ref Label Bill, ref Label Gabrielle) { Graphics palette = this.CreateGraphics; SolidBrush brownBrush = new SolidBrush(Color.Brown); palette.FillRectangle(brownBrush, new Rectangle(20, 20, 820, 180)); //OK, now I need to paint the little holes. //There are 244 little holes in the board. //Three rows of 40 times two, with the little starts and stops on either end. //Let's start with the 240. int rows = 0; int columns = 0; int scoreBeingDrawn = 0; Pen blackPen = new Pen(System.Drawing.Color.Black, 1); SolidBrush blackBrush = new SolidBrush(Color.Black); SolidBrush redBrush = new SolidBrush(Color.Red); //There are 6 rows, then, at 24 and 40, 80 and 100, then 140 and 160. for (rows = 40; rows <= 160; rows += 60) { //There are 40 columns. They are every 20 for (columns = 40; columns <= 820; columns += 20) { //Calculate score being drawn scoreBeingDrawn = ((columns - 20) / 20) + ((((rows + 20) / 60) - 1) * 40); //Draw Bill //If score being drawn = bill fill, otherwise draw if (scoreBeingDrawn == (int)Bill.Text) { palette.FillEllipse(blackBrush, columns - 2, rows - 2, 6, 6); } else if (scoreBeingDrawn == BillsLastTotal) { palette.FillEllipse(redBrush, columns - 2, rows - 2, 6, 6); { else { palette.DrawEllipse(blackPen, columns - 2, rows - 2, 4, 4); } //Draw Gabrielle //If score being drawn = Gabrielle fill, otherwise draw if (scoreBeingDrawn == (int)Gabrielle.Text) { palette.FillEllipse(blackBrush, columns - 2, rows + 16, 6, 6); } else if (scoreBeingDrawn == GabriellesLastTotal) { palette.FillEllipse(redBrush, columns - 2, rows + 16, 6, 6); } else { palette.DrawEllipse(blackPen, columns - 2, rows + 16, 4, 4); } } } palette.Dispose(); brownBrush.Dispose(); blackPen.Dispose(); }
Aside from the math, note the decision making. If the score being drawn is the score in the label, fill in the hole with a red peg. If it's the last score drawn, fill in the hole with a black peg. Otherwise, well, just draw a circle.
It's tough to fathom, but this is exactly how large-scale games are written. Admittedly, big graphics engines make many more If-Then decisions, but the premise is the same.
Also, large games use bitmap images sometimes, rather than drawing all the time. For the cribbage scoring application, for example, you could use a bitmap image of a peg rather than fill an ellipse with a black or red brush!