Introduction to GDI (Graphics Device Interface)Thus far, the only experience you've had with GDI is the processing of the WM_PAINT message in the main event handler. Remember that GDI, or the Graphics Device Interface, is how all graphics are drawn under Windows when DirectX is not in use. Alas, you haven't yet learned how to actually draw anything on the screen with GDI, but this is very key because rendering on the screen is one of the most important parts of writing a video game. Basically, a game is just logic that drives a video display. In this section, I'm going to revisit the WM_PAINT message, cover some basic video concepts, and teach you how to draw text within your window. The next chapter will focus more heavily on GDI. Understanding the WM_PAINT message is very important for standard GDI graphics and Windows programming because most Windows programs' displays revolve around this single message. In a DirectX game this isn't true, because DirectX, or more specifically DirectDraw or Direct3D, will do the drawing, but you still need to know GDI to write Windows applications. The WM_PAINT Message Once AgainThe WM_PAINT message is sent to your window's WinProc() whenever the window's client area needs repainting. Until now, you haven't done much processing on this event. Here's the standard WM_PAINT handler you have been using: PAINTSTRUCT ps; // used in WM_PAINT HDC hdc; // handle to a device context case WM_PAINT: { // simply validate the window hdc = BeginPaint(hwnd,&ps); // you would do all your painting here EndPaint(hwnd,&ps); // return success return(0); } break; Refer to Figure 3.12 for the following explanation. When a window is moved, resized, or in some way graphically obscured by another window or event, some or all of the window's client area must be redrawn. When this happens, a WM_PAINT message is sent and you must deal with it. Figure 3.12. The WM_PAINT message.In the case of the preceding code example, the calls to BeginPaint() and EndPaint() accomplish a couple of tasks. First, they validate the client area, and second, they fill the background of your window with the background brush defined in the Windows class that the window was originally created with. Now, if you want to do your own graphics within the BeginPaint()—EndPaint() call, you can. However, there is one problem: You will only have access to the portion of the window's client area that actually needs repainting. The coordinates of the invalid rectangle are stored in the rcPaint field of the ps (PAINSTRUCT) returned by the call to BeginPaint(): typedef struct tagPAINTSTRUCT { HDC hdc; // graphics device context BOOL fErase; // if TRUE then you must draw background RECT rcPaint; // the RECT containing invalid region BOOL fRestore; // internal BOOL fIncUpdate; // internal BYTE rgbReserved[32]; // internal } PAINTSTRUCT; And to refresh your memory, here's the definition of RECT: typedef struct _RECT { LONG left; // left edge if rectangle LONG top; // upper edge of rectangle LONG right; // right edge of rectangle LONG bottom; // bottom edge of rectangle } RECT; In other words, referring back to Figure 3.12, the window is 400x400, but only the lower region of the window—300,300 to 400,400—needs repainting. Thus, the graphics device context returned by the call to BeginPaint() is only valid for this 100x100 region of your window! Obviously, this is a problem if you want to have access to the entire client area. The solution to the problem has to do with gaining access to the graphics device context for the window directly without it being sent as part of a window update message via BeginPaint(). You can always get a graphics context for a window or hdc using the GetDC() function, as shown here: HDC GetDC(HWND hWnd); // handle of window You simply pass the window handle of the graphics device context you want to access, and the function returns a handle to it. If the function is unsuccessful, it returns NULL. When you're done with the graphics device context handle, you must give it back to Windows with a call to ReleaseDC(), as shown here: int ReleaseDC(HWND hWnd, // handle of window HDC hDC); // handle of device context ReleaseDC() takes the window handle and the handle to the device context you previously acquired with GetDC(). NOTE Windows-speak gets confusing when it comes to graphics device contexts. Technically, a handle to a device context can refer to more than one output device. For example, a device context could be a printer. Therefore, I usually refer to a graphics-only device context as a graphics device context. But the data type is HDC or handle to device context. So typically, I will define a graphics device context variable as HDC hdc, but sometimes I will also use HDC gdc because it makes more sense to me. In any case, just be aware that for this book, a graphics device context and a device context are interchangeable, and variables with the names hdc and gdc are of the same type. Here's how you would use GetDC()—ReleaseDC() to do graphics: HDC gdc = NULL; // this will hold the graphics device context // get the graphics context for the window if (!(gdc = GetDC(hwnd))) error(); // use the gdc here and do graphics – you don't know how yet! // release the dc back to windows ReleaseDC(hwnd, gdc); Of course, you don't know how to do any graphics yet, but I'm getting there…. The important thing is that you now have another way to process a WM_PAINT message. However, there is one problem: When you make a call to GetDC()—ReleaseDC(), Windows has no idea that you have restored or validated the client area of your window. In other words, if you use GetDC()—ReleaseDC() in place of BeginPaint()—EndPaint(), you'll create another problem! The problem is that BeginPaint()—EndPaint() sends a message to Windows indicating that the window contents have been restored (even if you don't make any graphics calls). Hence, Windows won't send any more WM_PAINT messages. On the other hand, if you replace BeginPaint()—EndPaint() with GetDC()—ReleaseDC() in the WM_PAINT handler, WM_PAINT messages will continue to be sent forever! Why? Because you must validate the window. To validate the area of a window that needs repainting and tell Windows that you have restored the window, you could call BeginPaint()—EndPaint() after the call to GetDC()—ReleaseDC(), but this would be inefficient. Instead, use the call specifically designed for this, called ValidateRect(): BOOL ValidateRect(HWND hWnd, // handle of window CONST RECT *lpRect); // address of validation rectangle coordinates To validate a window, send the handle of the window along with the region you want to be validated in lpRect. In most cases, the region to validate would be the entire window. Thus, to use GetDC()—ReleaseDC() in the WM_PAINT handler, you would have to do something like this: PAINTSTRUCT ps; // used in WM_PAINT HDC hdc; // handle to a device context RECT rect; // rectangle of window case WM_PAINT: { // simply validate the window hdc = GetDC(hwnd); // you would do all your painting here ReleaseDC(hwnd,hdc); // get client rectangle of window – use Win32 call GetClientRect(hwnd,&rect); // validate window ValidateRect(hwnd,&rect); // return success return(0); } break; NOTE Notice the call to GetClientRect(). All this does is get the client rectangle coordinates for you. Remember, because a window can move around, it has two sets of coordinates: window coordinates and client coordinates. Window coordinates are relative to the screen, and client coordinates are relative to the upper left-hand corner of the window (0,0). Figure 3.13 shows this more clearly. Figure 3.13. Window coordinates versus client coordinates.You must be saying, "Does it really need to be this hard?" Of course it does—it's Windows <BG>. Remember, the whole reason for all this drama in the WM_PAINT message handler is that you need to make sure that you can draw graphics anywhere you want in the client area of the window. This is only possible if you use GetDC()—ReleaseDC() or BeginPaint()—EndPaint() with a completely invalid window. However, we are trying to get the best of both worlds, and we're almost done. The final trick I want to show you is how to invalidate a window manually. Consider this: If you could somehow invalidate the entire window within your WM_PAINT handler, you would be sure that the rcPaint field of the ps PAINTSTRUCT returned by BeginPaint() and the associated gdc would give you access to the entire client area of the window. To make this happen, you can manually enlarge the invalidated area of any window with a call to InvalidateRect(), as shown here: BOOL InvalidateRect(HWND hWnd, // handle of window with // changed update region CONST RECT *lpRect, // address of rectangle coordinates BOOL bErase); // erase-background flag If bErase is TRUE, the call to BeginPaint() fills in the background brush; otherwise, it doesn't. Simply call InvalidateRect() before the BeginPaint()—EndPaint() pair, and then, when you do call BeginPaint(), the invalid region will reflect the union of what it was and what you added to it with the InvalidatRect(). However, in most cases, you will use NULL as the lpRect parameter of InvalidateRect(), which will invalidate the entire window. Here's the code: PAINTSTRUCT ps; // used in WM_PAINT HDC hdc; // handle to a device context case WM_PAINT: { // invalidate the entire window InvalidateRect(hwnd, NULL, FALSE); // begin painting hdc = BeginPaint(hwnd,&ps); // you would do all your painting here EndPaint(hwnd,&ps); // return success return(0); } break; In most of the programs in this book, you'll use GetDC()—ReleaseDC() in places other than the WM_PAINT message, and BeginPaint()—EndPaint() solely in the WM_PAINT handler. Now let's move on to some simple graphics so you can at least print out text. Video Display Basics and ColorAt this point, I want to take time to discuss some concepts and terminology that relate to graphics and color on the PC. Let's start with some definitions:
These elements are shown in Figure 3.14. Figure 3.14. The mechanics of a video display.
Of course, Table 3.2 is only a sampling of possible video modes and color depths. Your card may support many more. The important thing is to understand that it's pretty easy to eat up 2MB to 4MB of video RAM. The good news is that most DirectX Windows games that you'll write will run in 320x240 or 640x480, which, depending on the color depth, a 2MB card can support. RGB and Palletized ModesThere are two ways to represent color on a video display: directly or indirectly. Direct color modes, or RGB modes, represent each pixel on the screen with either 16, 24, or 32 bits that represent the red, green, and blue components of the color (see Figure 3.15). This is possible due to the additive nature of the primary colors red, green, and blue. Figure 3.15. Color encoding for RGB modes.Referring to Figure 3.15, you can see that for each possible color depth (16, 24, 32), there are a number of bits assigned to each color channel. Of course, with 16-bit and 32-bit color, these numbers aren't evenly divisible by 3; therefore, there might be an unequal amount of one of the color channels. For example, with 16-bit color modes, there are three different RGB encodings you might find:
The 24-bit mode is almost always eight bits per channel. However, the 32-bit mode can be weird, and in most cases there are eight bits for alpha (transparency) and eight bits each for the red, green, and blue channels. Basically, RGB modes give you control over the exact red, green, and blue components of each pixel on the screen. Palettized modes work on a principle called indirection. When there are only eight bits per pixel, you could decide to allocate the three bits for red, three bits for green, and maybe two bits for blue or some combination thereof. However, this would leave you with only a few shades of each of the primary colors, and that wouldn't be very exciting. Instead, 8-bit modes use a palette. As shown in Figure 3.16, a palette is a table that has 256 entries, one for each possible value of a single byte—0 to 255. However, each of these entries is really composed of three 8-bit entries of red, green, and blue. In essence, it's a full RGB 24-bit descriptor. The color lookup table (CLUT) works like this: When a pixel in an 8-bit color mode is read from the screen, say value 26, the 26 is used as an index into the color table. Then the 24-bit RGB value for color descriptor index 26 is used to drive the red, green, and blue channels for the actual color that is sent to the display. In this way, you can have just 256 colors on the screen at once, but they can be from among 16.7 million colors or 24-bit RGB values. Figure 3.16 illustrates the lookup process. Figure 3.16. How 256-color palettized modes work.We are getting a little ahead of ourselves with all this color stuff, but I want to let you chew on the concepts a bit so that when you see them again during the DirectDraw discussion, it won't be for the first time. In fact, color is such a complex problem to work with in normal GDI-based Windows graphics that Windows has abstracted color to a 24-bit model no matter what. That way you don't have to worry about the details of color depth and such when you're programming. Of course, you will get better results if you do worry about them, but you don't have to. Basic Text PrintingWindows has one of the most complex and robust text-rendering systems of any operating system I have ever seen. Of course, for most game programmers, printing the score is all we want to do, but it's nice to have nonetheless. In reality, the GDI text engine is usually too slow to print text in a real-time game, so in the end you will need to design our own DirectX-based text engine. For now, though, let's learn how to print text with GDI. At the very least, it will help with debugging and output with demos. There are two popular functions for printing text: TextOut() and DrawText(). TextOut() is the "ghetto ride" version of text output, and DrawText() is the Lexus. I usually use TextOut() because it's faster and I don't need all the bells and whistles of DrawText(), but we'll take a look at both. Here are their prototypes: BOOL TextOut(HDC hdc, // handle of device context int nXStart, // x-coordinate of starting position int nYStart, // y-coordinate of starting position LPCTSTR lpString,// address of string int cbString); // number of characters in string int DrawText( HDC hDC, // handle to device context LPCTSTR lpString, // pointer to string to draw int nCount, // string length, in characters LPRECT lpRect, // ptr to bounding RECT UINT uFormat); // text-drawing flags Most of the parameters are self-explanatory. For TextOut(), you simply send the device context, the x,y coordinates to print to, and the ASCII string, along with the length of the string in bytes. DrawText(), on the other hand, is a little more complex. Because it does word wrapping and formatting, it takes a different approach to printing via a rendering RECT. Thus, DrawText() doesn't take an x,y for the place to start printing; instead, it takes a RECT that defines where the printing will take place within the window (see Figure 3.17). Along with the RECT of where to print it, you send some flags that describe how to print it (such as left-justified). Please refer to the Win32 documentation for all the flags, because there are a ton of them. I'll just stick to DT_LEFT, which is the most intuitive and justifies all text to the left. Figure 3.17. The drawing RECT of DrawText().The only problem with both calls is that there's no mention of color. Hmmmm. That's almost as strange as Boogie Nights, but who cares? Anyway, thankfully there is a way to set both the foreground color of the text and the background color behind it, in addition to the transparency mode of the text. The transparency mode of the text dictates how the characters will be drawn. Will the characters be stamped down with rectangular regions or drawn pixel by pixel as an overlay? Figure 3.18 illustrates transparency as it relates to printing. As you can see, when text is printed with transparency, it looks as if it was drawn right on top of the graphics. Without transparency, you can actually see that there is an opaque block surrounding each character, which obscures everything—very ugly. Figure 3.18. Opaque and transparent text printing.TIP Rendering without transparency is faster, so if you're printing on a monochrome background and you can get away with it, do it! Let's take a look at the functions to set the foreground and background colors of text: COLORREF SetTextColor(HDC hdc, // handle of device context COLORREF Color); // foreground character color COLORREF SetBkColor(HDC hdc, // handle of device context COLORREF color); // background color Both functions take the graphics device context (from a call to GetDC() or BeginPaint()) along with the color to use in COLORREF format. Once you set these colors, they stay in flux until you change them. In addition, when you do set the colors, each function returns the current value so you can restore the old one when you're done or when your application exits. You're almost ready to print, but this new COLORREF type has to be dealt with, don't you think? Okay, then! Here's the definition of COLORREF: typedef struct tagCOLORREF { BYTE bRed; // the red component BYTE bGreen; // the green component BYTE bBlue; // the blue component BYTE bDummy; // unused } COLORREF; So in memory, a COLORREF looks like 0x00bbggrr. Remember, PCs are Little Endian—that is, low BYTE to high BYTE. To create a valid COLORREF, you can use the RGB() macro, like this: COLORREF red = RGB(255,0,0); COLORREF yellow = RGB(255,255,0); And so forth. While we're looking at color descriptor structures, we might as well look at PALETTEENTRY because it is absolutely identical: typedef struct tagPALETTEENTRY { BYTE peRed; // red bits BYTE peGreen; // green bits BYTE peBlue; // blue bits BYTE peFlags; // control flags } PALETTEENTRY; peFlags can take on the values in Table 3.3. In most cases you will use PC_NOCOLLAPSE and PC_RESERVED, but for now just know they exist. The interesting thing that I wanted to point out, though, is the similarity between COLORREFs and PALETTEENTRYs. They are identical except for the interpretation of the last BYTE. Hence, in many cases they're interchangeable. Now you're almost ready to print, but remember that there was the issue of transparency and how to set it. The function used to set the transparency mode is SetBkMode(), and here's its prototype: int SetBkMode(HDC hdc, // handle to device context int iBkMode); // transparency mode The function takes the graphics device context along with the new transparency mode to switch to, which can be either TRANSPARENT or OPAQUE. The function returns the old mode so you can save it for later restoration. Now you're ready to kick the tires and light the fires, big daddy. Here's how you would print some text: COLORREF old_fcolor, // old foreground text color old_bcolor; // old background text color int old_tmode; // old text transparency mode // first get a graphics device context HDC hdc = GetDC(hwnd); // set the foreground color to green and save old one old_fcolor = SetTextColor(hdc, RGB(0,255,0)); // set the background color to black and save old one old_bcolor = SetBkColor(hdc, RGB(0,0,0)); // finally set the transparency mode to transparent old_tmode = SetBkMode(hdc, TRANSPARENT); // draw some text at (20,30) TextOut(hdc, 20,30, "Hello",strlen("Hello")); // now restore everything SetTextColor(hwnd, old_fcolor); SetBkColor(hwnd, old_bcolor); SetBkMode(hwnd, old_tmode); // release the device context ReleaseDC(hwnd, hdc); Of course, there is no law that you have to restore the old values, but I did it here just to show you how. Also, the color and transparency settings are valid as long as you have the handle to the device context. Let's say you wanted to draw some blue text in addition to the green text. You'd only have to change the text color to blue and then draw the text. You wouldn't have to set all three values again. For an example of printing text using the preceding technique, take a look at DEMO3_5.CPP and the executable DEMO3_5.EXE. The demo creates a display of randomly positioned text strings in different colors all over the screen, as shown in Figure 3.19. Figure 3.19. Random text output from DEMO3_5.EXE.The following is an excerpt from the program's WinMain(), where all the action takes place: // get the dc and hold it HDC hdc = GetDC(hwnd); // enter main event loop, but this time we use PeekMessage() // instead of GetMessage() to retrieve messages while(TRUE) { // test if there is a message in queue, if so get it if (PeekMessage(&msg,NULL,0,0,PM_REMOVE)) { // test if this is a quit if (msg.message == WM_QUIT) break; // translate any accelerator keys TranslateMessage(&msg); // send the message to the window proc DispatchMessage(&msg); } // end if // main game processing goes here // set the foreground color to random SetTextColor(hdc, RGB(rand()%256,rand()%256,rand()%256)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // finally set the transparency mode to transparent SetBkMode(hdc, TRANSPARENT); // draw some text at a random location TextOut(hdc,rand()%400,rand()%400, "GDI Text Demo!", strlen("GDI Text Demo!")); } // end while // release the dc ReleaseDC(hwnd,hdc); As a second example of printing text, let's try doing something like updating a counter in response to the WM_PAINT message. Here's the code to do that: char buffer[80]; // used to print string static int wm_paint_count = 0; // track number of msg's case WM_PAINT: { // simply validate the window hdc = BeginPaint(hwnd,&ps); // set the foreground color to blue SetTextColor(hdc, RGB(0,0,255)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // finally set the transparency mode to transparent SetBkMode(hdc, OPAQUE); // draw some text at (0,0) reflecting number of times // wm_paint has been called sprintf(buffer,"WM_PAINT called %d times ", ++wm_paint_count); TextOut(hdc, 0,0, buffer, strlen(buffer)); EndPaint(hwnd,&ps); // return success return(0); } break; Take a look at DEMO3_6.CPP and the executable DEMO3_6.EXE on the CD-ROM to see the program in action. Notice that nothing will print until you move or overwrite the window. This is because WM_PAINT is generated only when there is some reason to restore or redraw the window, such as a movement or resize. That's about it for basic printing. Of course, the DrawText() function does a lot more, but that's up to you. Also, you might want to look into fonts and that whole can of worms, but stuff like that is normally for full Windows GUI programming and is not really what we're trying to do in this book. |