Advanced GDI Graphics

As I've mentioned, GDI is horribly slow when compared to DirectX. However, GDI is good at everything and it's the native rendering engine for Windows itself. This means if you create any tools or standard GUI applications, knowing your way around GDI is an asset. Moreover, knowing how to mix GDI and DirectX is a way to leverage the power of GDI's functionality to emulate functions you haven't completed in your DirectX programming. Hence, GDI has utility as a slow software emulation for functions you might write down the road in your game design. Bottom line—you need to know it.

What I'm going to do now is cover a few basic GDI operations. You can always learn more by perusing the Win32 SDK, but the basic skill set you'll learn here will more than prepare you for figuring out any GDI function. It's like Comdex—if you've seen one, you've seen them all.

Under the Hood with the Graphics Device Context

InChapter 3, "Advanced Windows Programming," you saw the type handle to device context, or HDC, a number of times. This of course is the data type that represents a handle to a device context. In our case, the device context has been a graphics device context type, but there are others like printer contexts. Anyway, you might be wondering what exactly a graphics device context is? What does it really mean? Both are good questions.

A graphics device context is really a description of the video graphics card installed in your system. Therefore, when you have access to a graphics device context or handle this really means that stuffed away somewhere is an actual description of the video card in your system and its resolution and color capabilities. This information is needed for any graphics call you might make to GDI. In essence, the HDC handle you supply to any GDI function is used to reference whatever important information about your video system that a function needs to operate with. And that's why you need a graphics device context.

Furthermore, the graphics device context tracks software settings that you may change throughout the life of your program. For example, GDI uses a number of graphics objects such as pens,brushes,line styles, and more. These basic data descriptions are used by GDI to draw any graphics primitives that you may request. Therefore, even though the current pen color is something that you might set and isn't intrinsic to your video card, the graphics device context still tracks it. In this way, the graphics device context is not only a hardware description of your video system, but a repository of information that records your settings and stores them for you, so that the GDI calls you make can use those settings rather than explicitly sending them along with the call. This way you can save a lot of parameters for GDI calls. With that in mind, let's take a look at how to render graphics with GDI.

Color, Pens, and Brushes

If you think about it, there aren't that many types of objects that you can draw on a computer screen. Sure, there are an unlimited number of shapes and colors you can draw them with, but the types of objects are very limited. There are points,lines, and polygons. Everything else is really a combination of these types of primitive objects.

The approach that GDI takes is something like that of a painter. A painter paints pictures with colors, pens, and brushes—work with me on this <BG>. GDI works in the same manner, with the following definitions:

Pens—
These are used to draw lines or contours. They have color, thickness, and a line style.

Brushes—
These are used to fill in any closed objects. They have color, style, and can even be bitmaps. Take a look at Figure 4.1 for a detailed labeling.

Figure 4.1. A brush, labeled in detail.

Before we get into pens and brushes and actually using them, I want to take a minute to look at the situation. GDI likes to use only one pen, and one brush at a time. Sure, you can have many pens and brushes at your disposal, but only one of each is active in the current graphics device context. This means that you must "select objects" into the graphics device context to use them.

Remember, the graphics device context is not only a description of the video card and its services, but a description of the current drawing tools. Pens and brushes are primary examples of tools that the context tracks and that you must select in and out of the graphics context. This process is called selection. As your program runs, you'll select in a new pen and then select it out later, and maybe select in and out different brushes and so on. The thing to remember is that once a drawing object is selected into the context it's used until it is changed.

Finally, whenever you create a new pen or brush, you must delete it when you're done. This is important because Windows GDI has only so many slots for pen and brush handles and you could run out! But we'll get to that in a minute. Okay, so let's cover pens first, and then brushes.

Working with Pens

The handle to a pen is called HPEN. Here's how you would create a NULL pen.

HPEN pen_1 = NULL;

pen_1 is just a handle to a pen, but pen_1 hasn't been filled in or defined yet with the desired information. This operation is accomplished in one of two ways:

Using a stock object

Creating a user-defined pen

Remember, stock objects, or stock anything, are just objects that Windows has a few default types for to get you started. In the case of pens, there are a couple of pen types already defined, but they are very limited. You can use the GetStockObject() function shown in the following line to retrieve a number of different object handles, including pen handles, brushes, and fonts.

HGDIOBJ GetStockObject(int fnObject); // type of stock object

The function simply takes the type of stock object you desire and returns a handle to it. The types of pens that are pre-defined stock objects are shown in Table 4.1.

The system font. By default, Windows uses the system font to draw menus, dialog box controls, and text. In Windows versions 3.0 and later, the system font is a proportionally spaced font; earlier versions of Windows used a monospace system font.

SYSTEM_FIXED_FONT

Fixed-pitch (monospace) system font used in Windows versions earlier than 3.0. This stock object is provided for compatibility with earlier versions of Windows.

As you can see from Table 4.1, there aren't a whole lot of pens to select from (that's a little GDI humor—get it?). Anyway, here's an example of how you would create a white pen:

HPEN white_pen = NULL;
white_pen = GetStockObject(WHITE_PEN);

Of course, GDI knows nothing about white_pen because it hasn't been selected into the graphics device context, but we're getting there.

A more interesting method of creating pens is to create them yourself by defining their color, line style, and width in pixels. The function used to create a pen is calledCreatePen() and is shown here:

The nWidth and crColor parameters are easy enough to understand, but the fnPenStyle needs a little explanation.

In most cases you probably want to draw solid lines, but in some cases you might need a dashed line to represent something in a charting program. You could draw a number of lines all separated by a little space to make a dashed line, but why not let GDI do it for you? The line style facilitates this functionality. GDI logically ANDs or masks a line style filter as it's rendering lines. This way, you can draw lines that are composed of dots and dashes, or solid pixels, or whatever one-dimensional entity you want. Table 4.2 contains the valid line styles that you can choose from.

Table 4.2. Line Styles for CreatePen()

Style

Description

PS_NULL

Pen is invisible.

PS_SOLID

Pen is solid.

PS_DASH

Pen is dashed.

PS_DOT

Pen is dotted.

PS_DASHDOT

Pen has alternating dashes and dots.

PS_DASHDOTDOT

Pen has alternating dashes and double dots.

As an example, let's create three pens, each 1 pixel wide, with solid style:

Simple enough? Now, that we have a little to work with, let's take a look at how to select pens into the graphics device context. We still don't know how to draw anything, but now is a good time to see the concept.

To select any GDI object into the graphics device context use the SelectObject() function shown here:

SelectObject() takes the handle to the graphics context along with the object to be selected. Notice that SelectObject() is polymorphic, meaning that it can take many different handle types. The reason for this is that all handles to graphics objects are also subclasses of the data type HGDIOBJs (handles to GDI objects), so everything works out. Also, the function returns the current handle of the object you are de-selecting from the context. In other words, if you select a new pen into the context, obviously you must select the old one out. Therefore, you can save the old handle and restore it later if you wish. Here's an example of selecting a pen into the context and saving the old one:

And then finally, when you are done with pens that you have created either with GetStockObject() or CreatePen(), you must destroy them. This is accomplished with DeleteObject(), which, similar to SelectObject(), is polymorphic and can delete many object types. Here's its prototype:

BOOL DeleteObject(HGDIOBJ hObject); // handle to graphic object

WARNING

Be careful when you destroy pens. If you delete an object that is currently selected or try to select an object that is currently deleted chances are you will cause an error and possibly a GP Fault.

NOTE

I haven't been doing too much error checking, but obviously this is an issue. In a real program, you should always check the return type of your function calls to see if they are successful; otherwise, there could be trouble.

The next question is when to actually call DeleteObject() on graphics objects. Typically, you will do this at the end of the program. However, if you create hundreds of objects, use them, and won't use them for the remainder of the program, you should delete them then and there. This is because Windows GDI only has limited resources. As an example, here's how to release and destroy the group of pens we created in the earlier example:

Try not to delete objects you have already deleted. It can cause unpredictable results.

Painting with Brushes

Let's talk more about brushes. Brushes are similar to pens in most ways except how they look. Brushes are used to fill in graphic objects, whereas pens are used to outline objects or draw simple lines. However, all the same principles are in flux. The handle to a brush is called an HBRUSH. And to define a blank brush object you would do something like:

HBRUSH brush_1 = NULL;

To actually make the brush look like something, you can either use a stock brush type from Table 4.1 via GetStockObject() or define one yourself. For example, here's how to create a light gray stock brush:

brush_1 = GetStockObject(LTGRAY_BRUSH);

Bam, baby! Too easy, huh? To create more interesting brushes you can select the fill pattern type and color just as you can for pens. Unfortunately GDI broke brushes up into two classes: solid and hatched. I think this is stupid—GDI should allow all brushes to be hatched and then simply have a solid type, but whatever! The function to create a solid fill brush is called CreateSolidBrush()and is shown here: