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
In Chapter 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:
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
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:
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.
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 called CreatePen() and is shown here:
HPEN CreatePen(int fnPenStyle, // style of the pen int nWidth, // width of pen in pixels COLORREF crColor); // color of pen
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.
// the red pen, notice the use of the RGB macro HPEN red_pen = CreatePen(PS_SOLID, 1, RGB(255,0,0)); // the green pen, notice the use of the RGB macro HPEN green_pen = CreatePen(PS_SOLID, 1, RGB(0,255,0)); // the blue pen, notice the use of the RGB macro HPEN blue_pen = CreatePen(PS_SOLID, 1, RGB(0,0,255));
And let's also make a white dashed pen:
HPEN white_dashed_pen = CreatePen(PS_DASHED, 1, RGB(255,255,255));
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.
HGDIOBJ SelectObject(HDC hdc, // handle of device context HGDIOBJ hgdiobj); // handle of object
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:
HDC hdc; // the graphics context, assume valid // create the blue HPEN blue_pen = CreatePen(PS_SOLID, 1, RGB(0,0,255)); HPEN old_pen = NULL; // used to store old pen // select the blue pen in and save the old pen old_pen = SelectObject(hdc, blue_pen); // do drawing... // restore the old pen SelectObject(hdc, old_pen);
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
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.
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:
DeleteObject(red_pen); DeleteObject(green_pen); DeleteObject(blue_pen); DeleteObject(white_dashed_pen);
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:
HBRUSH CreateSolidBrush(COLORREF crColor); // brush color
To create a green solid brush all you have to do is this:
HBRUSH green_brush = CreateSolidBrush(RGB(0,255,0));
HBRUSH old_brush = NULL; old_brush = SelectObject(hdc, green_brush); // draw something with brush // restore old brush SelectObject(hdc, old_brush);
HBRUSH CreateHatchBrush(int fnStyle, // hatch style COLORREF clrref); // color value
The style of the brush can be one of the values listed in Table 4.3.
HBRUSH red_hbrush = CreateHatchBrush(HS_CROSS, RGB(255,0,0));
Select it into the device context:
HBRUSH old_brush = SelectObject(hdc, red_hbrush);
Finally, restore the old brush and delete the red brush we created:
SelectObject(hdc, old_brush); DeleteObject(red_hbrush);