Although the pens defined as stock objects are certainly convenient, you are limited to only a solid black pen, a solid white pen, or no pen at all. If you want to get fancier than that, you must create your own pens. Here's the general procedure: You create a ”logical pen,“ which is merely the description of a pen, using the function CreatePen or CreatePenIndirect. These functions return a handle to the logical pen. You select the pen into the device context by calling SelectObject. You can then draw lines with this new pen. Only one pen can be selected into the device context at any one time. After you release the device context (or after you select another pen into the device context), you can delete the logical pen you've created by calling DeleteObject. When you do so, the handle to the pen is no longer valid.
A logical pen is a ”GDI object.“ You create and use the pen, but the pen doesn't belong to your program. The pen really belongs to the GDI module. A pen is one of six GDI objects that you can create. The other five are brushes, bitmaps, regions, fonts, and palettes.
This brings me to a very important point: Normally, Windows cleans up thoroughly when a program terminates. The one big exception is for GDI objects. When a program terminates, Windows doesn't automatically delete GDI objects that the program has created. The program itself is responsible for deleting GDI objects.
Three rules govern the use of GDI objects such as pens:
Delete all GDI objects that you create.
Don't delete GDI objects while they are selected in a valid device context.
Don't delete stock objects.
These are not unreasonable rules, but they can be a little tricky sometimes. We'll run through some examples to get the hang of how the rules work.
The general syntax for the CreatePen function looks like this:
hPen = CreatePen (nPenStyle, nWidth, rgbColor) ;
The nPenStyle parameter determines whether the pen draws a solid line or a line made up of dots or dashes. The parameter can be one of the following identifiers defined in WINDOWS.H: PS_SOLID, PS_DASH, PS_DOT, PS_DASHDOT, PS_DASHDOTDOT, PS_NULL, and PS_INSIDEFRAME. Figure 12-1 shows the kind of line that each style produces.
For the PS_SOLID, PS_NULL, and PS_INSIDEFRAME styles, the nWidth parameter is the width of the pen in logical units. For instance, if the mapping mode is MM_LOENGLISH, a pen with an nWidth of 10 will be 0.1 inch wide. When you draw a line with a PS_SOLID or PS_NULL pen, the width of the pen will extend 0.05 inch on either side of the line. (The PS_INSIDEFRAME style is a little different.) For the MM_ANISOTROPIC mapping mode, Windows uses logical units on the x-axis to determine the physical width of the pen. An nWidth value of 0 directs Windows to use one physical unit (1 pixel) for the pen width. The stock pens are 1 pixel wide. If you specify a dotted or dashed pen style with a physical width greater than 1, Windows will use a solid pen instead.
The rgbColor parameter to CreatePen is an unsigned long integer specifying the color of the pen. For all the pen styles except PS_INSIDEFRAME, when you select the pen into the device context, Windows converts this parameter to the nearest pure color that the device can represent. The PS_INSIDEFRAME style is the only pen style that can use a dithered color, and then only when the width is greater than 1. (The PS_INSIDEFRAME style has another peculiarity, which I'll discuss later in this chapter in the section on the ”bounding box.“)
You can also create a pen by setting up a structure of type LOGPEN (”logical pen“) and calling CreatePenIndirect. If your program uses a lot of different pens that you can initialize in your source code, this method is more efficient. First you define a structure variable of type LOGPEN—for instance, logpen:
LOGPEN logpen ;
This structure has three members: lopnStyle (WORD) is the pen style, lopnWidth (POINT) is the pen width in logical units, and lopnColor (DWORD) is the pen color. The lopnWidth member is a structure of type POINT, but Windows uses only the lopnWidth.x value for the pen width and ignores lopnWidth.y. Then you create the pen by passing the address of the structure to CreatePenIndirect:
hPen = CreatePenIndirect (&logpen) ;
You can also obtain the logical pen information for an existing pen. If you already have a handle to a pen, you can copy the data that defines the logical pen into a structure of type LOGPEN by using the GetObject call:
GetObject (hPen, sizeof (LOGPEN), (LPSTR) &logpen) ;
Note that the CreatePen and CreatePenIndirect functions do not require a handle to a device context. These functions create logical pens that have no connection with a device context until you call SelectObject. For instance, you can use the same logical pen for several different devices, such as the screen and a printer. Logical pens with a non- zero nWidth have a logical width; they have a physical width only when you select the pen into a device context, and then the physical width depends on the device context's mapping mode.
Here's one method for creating, selecting, and deleting pens. Suppose your program uses three pens—a black pen of width 1, a red pen of width 3, and a black dotted pen. You can first define variables for storing the handles to these pens:
static HPEN hPen1, hPen2, hPen3 ;
During processing of WM_CREATE, you can create the three pens:
hPen1 = CreatePen (PS_SOLID, 1, 0L) ;
hPen2 = CreatePen (PS_SOLID, 3, RGB (255, 0, 0)) ;
hPen3 = CreatePen (PS_DOT, 0, 0L) ;
During processing of WM_PAINT (or any other time you have a valid handle to a device context), you can select one of these pens into the device context and draw with it:
SelectObject (hdc, hPen2) ;
[LineTo, PolyLine, or Arc calls]
SelectObject (hdc, hPen1) ;
[other LineTo, PolyLine, or Arc calls]
During processing of WM_DESTROY, you can delete the three pens you created:
DeleteObject (hPen1) ;
DeleteObject (hPen2) ;
DeleteObject (hPen3) ;
This is the most straightforward method for creating, selecting, and deleting pens, but it requires that the logical pens take up memory space during the entire time your program is running. You might instead want to create the pens during each WM_PAINT message and delete them after you call EndPaint. (You can delete them before calling EndPaint, but you have to be careful not to delete the pen currently selected in the device context.)
You might also want to create pens on the fly and combine the CreatePen and the SelectObject calls in the same statement:
SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
Now when you draw lines, you'll be using a red dashed pen. When you're finished drawing the red dashed lines, you can delete the pen. Whoops! How can you delete this pen when you haven't saved the pen handle? Recall that SelectObject returns the handle to the pen previously selected in the device context. So you can delete the pen by selecting the stock BLACK_PEN into the device context and deleting the value returned from SelectObject:
DeleteObject (SelectObject (hdc, GetStockObject (BLACK_PEN))) ;
Here's another method. When you select a newly created pen into the device context, save the handle to the pen that SelectObject returns:
hPen = SelectObject (hdc, CreatePen (PS_DASH, 0, RGB (255, 0, 0))) ;
What is hPen? If this is the first SelectObject call you've made since obtaining the device context, hPen is a handle to the BLACK_PEN stock object. You can now select that pen into the device context and delete the pen you created (the handle returned from this second SelectObject call) in one statement:
DeleteObject (SelectObject (hdc, hPen)) ;
If you delete a GDI object while it is selected in a device context and then try to draw lines, Windows will respond with a fatal error because the device context doesn't contain a valid pen. This is a fairly obvious bug to track down. Failing to delete GDI objects that you create can be a more difficult bug to discover, because the program will appear to work fine. If your program creates the same logical pen for every WM_PAINT message, you might want to cause the client area to be repainted over and over and check to see if free memory starts to drop. The FREEMEM program shown in Chapter 5 can identify problems related to dropping memory. If HEAPWALK shows a lot of small GDI segments after your program has terminated, some of them may be GDI objects you have failed to delete properly.