Game Programming Genesis Part III : Tracking Your Window and Using GDI

Published December 07, 2000 by Joseph Farrell, posted by Myopic Rhino
Do you see issues with this article? Let us know.
Advertisement
(Editor's Note - this article requires additional formatting work and is incomplete)

[size="5"]Introduction

If you've been with me for the last two articles, you've probably been asking yourself when I'm going to show you something useful. Well, the wait is over! Today I'll be showing you the basics of Windows GDI (Graphical Device Interface), and a few other things along the way, like responding to user input and dealing with some more of the messages that Windows generates. As far as actually displaying graphics, I'm going to go over three basic topics: showing text, plotting pixels, and displaying bitmaps. Before getting into too much of that though, I'm going to cover several more Windows messages in detail so you will be sure to know what's going on when the user starts messing with things. They always do. :)

As always, you need only a basic knowledge of the C language, and the information that was covered in previous articles of this series. Since this article will enable you to make some working graphical demos, there is a sample program available along with the article. The code used for this program was written and compiled in Visual C++, but it is simple enough that you shouldn't have to change it to get it working with other compilers. All right, enough with the disclaimers, and on to the fun stuff!


[size="5"]Device Contexts

In the first article in this series, we defined and registered a window class. One of the lines in that definition, giving the window's capabilities, was this:

sampleClass.style = CS_DBLCLKS | CS_OWNDC |
CS_HREDRAW | CS_VREDRAW; // standard settings
Three of those attributes are fairly self-explanatory, but the other -- [font="Courier New"][color="#000080"]CS_OWNDC[/color][/font] -- requires some explanation. If you recall, I told you that this attribute allowed for the window to have its own unique device context, and that device contexts would not be covered just yet. Well, grasshopper, the time has come.

A device context is a structure that represents a group of graphic objects and their attributes, as well as some output device and its attributes and settings. Using device contexts allows you to manipulate graphics in a very straightforward manner, without having to worry about a lot of low-level details. Windows GDI is a graphics-rendering system which takes Windows graphics calls and passes the information to the appropriate device driver. To make use of GDI graphics, you must use device contexts. Thankfully, it's very easy to do. You can get a device context for a window using a simple function call:

HDC GetDC( HWND hWnd // handle to a window );
That looks pretty harmless, doesn't it? All you do is pass a handle to the window for which you want a device context (or DC), and the return value is a handle to that device context. If you pass [font="Courier New"][color="#000080"]NULL[/color][/font], the handle returned is for a DC to the entire screen. If the function call fails, the return value is [font="Courier New"][color="#000080"]NULL[/color][/font].

Now is a good place to mention that device contexts are a little more general than dealing with graphics calls only. The type of DC we'll be talking about is called a display device context, because it deals with displaying graphics. In addition, there are printer device contexts, which use a printer as the output device; memory device contexts, which allow for manipulation of bitmap data; and information device contexts, for retrieving data for a specified device. Don't worry if this all sounds complicated. It's Windows -- its primary function is to confuse people. :) Once we get into some code, I think you'll find that it's actually not that difficult.

When you're finished with a device context, you have to release it. This frees up any memory that was being used by the object -- you'll come across the concept of releasing objects a lot more in the future. Once again, this is done by using a simple function call:

int ReleaseDC(
HWND hWnd, // handle to window
HDC hDC // handle to device context );
The return value is 1 if the DC was successfully released, or 0 if something went wrong. The parameters are self-explanatory, but I'll list them here anyway.

[font="Courier New"][color="#000080"]HWND hWnd[/color][/font]: This is the handle to the window which is referred to by the DC you're trying to release. If you have a DC for the whole desktop, pass [font="Courier New"][color="#000080"]NULL[/color][/font].

[font="Courier New"][color="#000080"]HDC hDC[/color][/font]: The handle to the device context you want to release.

Before we get into doing some graphics displays with device contexts and GDI, I want to talk about some of the important messages you'll encounter when creating a windowed application. The four messages I want to cover briefly are [font="Courier New"][color="#000080"]WM_MOVE[/color][/font], [font="Courier New"][color="#000080"]WM_SIZE[/color][/font], [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font], and [font="Courier New"][color="#000080"]WM_PAINT[/color][/font].


[size="5"]Tracking the Status of Your Window

The first two are relatively simple. [font="Courier New"][color="#000080"]WM_MOVE[/color][/font] is called whenever the window is moved by the user. The new window coordinates are stored in [font="Courier New"][color="#000080"]lparam[/color][/font]. (Remember, messages are further specified by the contents of [font="Courier New"][color="#000080"]lparam[/color][/font] and [font="Courier New"][color="#000080"]wparam[/color][/font], which are parameters received by your message-handling function.) The low word of [font="Courier New"][color="#000080"]lparam[/color][/font] is the x-coordinate of the upper-left corner of the window's client area. The high word of [font="Courier New"][color="#000080"]lparam[/color][/font] is the y-coordinate.

The [font="Courier New"][color="#000080"]WM_SIZE[/color][/font] message is sent when the window is resized. Like the [font="Courier New"][color="#000080"]WM_MOVE[/color][/font] message, its parameterization is held in [font="Courier New"][color="#000080"]lparam[/color][/font]. The low word is the client area's width, and the high word is its height. But unlike [font="Courier New"][color="#000080"]WM_MOVE[/color][/font], the [font="Courier New"][color="#000080"]wparam[/color][/font] parameter also holds some significant. It can take any of the following values:

SIZE_MAXHIDESome other window has been maximized.SIZE_MAXIMIZEDWindow has been maximized.SIZE_MAXSHOWSome other window has been restored.SIZE_MINIMIZEDWindow has been minimized.SIZE_RESTOREDWindow has been resized, but neither maximized nor minimized.When I'm writing windowed applications, I usually like to keep a few global variables that give the window's current position and size. If these variables were called [font="Courier New"][color="#000080"]xPos[/color][/font], [font="Courier New"][color="#000080"]yPos[/color][/font], [font="Courier New"][color="#000080"]xSize[/color][/font], and [font="Courier New"][color="#000080"]ySize[/color][/font], you'd handle the [font="Courier New"][color="#000080"]WM_SIZE[/color][/font] and [font="Courier New"][color="#000080"]WM_MOVE[/color][/font] messages something like this:

if (msg == WM_SIZE)
{
xSize = LOWORD(lparam);
ySize = HIWORD(lparam);
}

if (msg == WM_MOVE)
{
xPos = LOWORD(lparam);
yPos = HIWORD(lparam);
}
Next up is the [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] message, which tells you when a new window becomes the active window. This can be useful because you may not want to be processing all of your program's logic if some other application has the focus. Sometimes, such as in writing fullscreen DirectX programs, ignoring the [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] message can cause your program to experience a fatal error by doing something it's not supposed to be doing. In any case, it's good to watch the [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] messages and take action accordingly.

The [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] message is sent to both the window being activated, and the window being deactivated. You can determine which is the case by looking at the low word of [font="Courier New"][color="#000080"]wparam[/color][/font]. It will be set to one of three possible values:

WA_CLICKACTIVEWindow was activated by a mouse click.WA_ACTIVEWindow was activated by some other means (keyboard, function call, etc.)WA_INACTIVEWindow was deactivated.For dealing with this message, I'll keep another global variable called bFocus, and change its value when a [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] message is received. The code would look something like this:

if (msg == WM_ACTIVATE)
{
if (LOWORD(wparam) == WA_INACTIVE)
focus = FALSE;
else
focus = TRUE;

// tell Windows we handled it
return(0);
}
There are two related messages called [font="Courier New"][color="#000080"]WM_KILLFOCUS[/color][/font] and [font="Courier New"][color="#000080"]WM_SETFOCUS[/color][/font], which a window receives immediately before it loses or gains the keyboard focus, respectively. Since it's possible for no window to have the keyboard focus, I suggest using the [font="Courier New"][color="#000080"]WM_ACTIVATE[/color][/font] message to track your window's status. Now, on to the biggie.


[size="5"]The WM_PAINT Message

A window receives this important message when part of its client area has become invalidated. Suppose your program doesn't have the focus, and the active window is on top of your window. If the user moves that active window, it's going to reveal a part of your window. Since that part of the window needs to be refreshed, it is said to be invalidated. To handle this, there are a couple of things you can do. The first involves a pair of functions designed exclusively for use with the [font="Courier New"][color="#000080"]WM_PAINT[/color][/font] message. The first is [font="Courier New"][color="#000080"]BeginPaint()[/color][/font]. Here's the prototype:

HDC BeginPaint(
HWND hwnd, // handle to window
LPPAINTSTRUCT lpPaint // pointer to structure for paint information );
Before I tell you exactly what the return value is, let's look at the parameters:

[font="Courier New"][color="#000080"]HWND hwnd[/color][/font]: This is a handle to the window which needs repainting. You should be used to seeing this parameter by now, right?

[font="Courier New"][color="#000080"]LPPAINTSTRUCT lpPaint[/color][/font]: Here's the important one. This is a pointer to a [font="Courier New"][color="#000080"]PAINTSTRUCT[/color][/font] structure, which contains all sorts of information about the area to be painted.

And before we go on, I should show you exactly what a [font="Courier New"][color="#000080"]PAINTSTRUCT[/color][/font] looks like...

typedef struct tagPAINTSTRUCT { // ps
HDC hdc;
BOOL fErase;
RECT rcPaint;
BOOL fRestore;
BOOL fIncUpdate;
BYTE rgbReserved[32];
} PAINTSTRUCT;
And the members of the structure are as follows:

[font="Courier New"][color="#000080"]HDC hdc[/color][/font]: Aha! I knew there was some reason we went over device contexts, even if it took awile to get here. This is a DC that represents the invalidated area -- the area that needs to be painted.

[font="Courier New"][color="#000080"]BOOL fErase[/color][/font]: This specifies whether or not the application should erase the background. If set to [font="Courier New"][color="#000080"]FALSE[/color][/font], the system has already deleted the background. Remember in our window class when we defined a black brush as the background? This will cause the system to automatically erase the invalidated area with that black brush.

[font="Courier New"][color="#000080"]RECT rcPaint[/color][/font]: This is the most important member, as it tells you the rectangle that needs to be repainted in order to cover the whole invalidated area. I'll show you the [font="Courier New"][color="#000080"]RECT[/color][/font] structure in just a bit.

[font="Courier New"][color="#000080"]BOOL fRestore, BOOL fIncUpdate, BYTE rgbReserved[32][/color][/font]: Good news! These are reserved and are used by Windows, so you and I don't have to worry about them. :)

Now that I've showed this to you, I can tell you just what [font="Courier New"][color="#000080"]BeginPaint()[/color][/font] is accomplishing. It actually does three things. First, it validates the window again, so that another [font="Courier New"][color="#000080"]WM_PAINT[/color][/font] message will not be generated unless the window becomes invalidated again. Second, if your window class has a background brush defined, like ours does, it paints the affected area with that brush. Third, it returns a handle to a device context which represents the area needing to be painted. That area, as we saw, is defined by the important [font="Courier New"][color="#000080"]RECT[/color][/font] structure:

typedef struct _RECT {
LONG left;
LONG top;
LONG right;
LONG bottom;
} RECT;
You've already figured out that this structure represents a rectangle, but there is one thing that needs to be said about it. [font="Courier New"][color="#000080"]RECT[/color][/font]s are upper-left inclusive, but lower-right exclusive. What does that mean? Well, let's say you define a [font="Courier New"][color="#000080"]RECT[/color][/font] like this:

RECT myRect = {0, 0, 5, 5};
This [font="Courier New"][color="#000080"]RECT[/color][/font] includes the pixel at (0, 0), but it stops short of (5, 5), so that the lower-right corner of the area described by this rectangle is actually at (4, 4). It doesn't seem to make much sense at first, but you'll get used to the idea.

Now, remember what I said about using device contexts? Once you're done using one, you have to release it. In this case, you use the [font="Courier New"][color="#000080"]EndPaint()[/color][/font] function. Each call to [font="Courier New"][color="#000080"]BeginPaint()[/color][/font], which should only be made in response to a [font="Courier New"][color="#000080"]WM_PAINT[/color][/font] message, must have a matching [font="Courier New"][color="#000080"]EndPaint()[/color][/font] function to release the DC. Here's the function:

BOOL EndPaint(
HWND hWnd, // handle to window
CONST PAINTSTRUCT *lpPaint // pointer to structure for paint data );
The function returns [font="Courier New"][color="#000080"]TRUE[/color][/font] or [font="Courier New"][color="#000080"]FALSE[/color][/font] indicating its success or failure, respectively, and takes two simple parameters:

[font="Courier New"][color="#000080"]HWND hWnd[/color][/font]: Just the handle to the window. Again.

[font="Courier New"][color="#000080"]CONST PAINTSTRUCT *lpPaint[/color][/font]: A pointer to the [font="Courier New"][color="#000080"]PAINTSTRUCT[/color][/font] containing the information about the area in question. Don't let the [font="Courier New"][color="#000080"]CONST[/color][/font] confuse you. It's just there to denote and ensure that the function does not alter the contents of the structure.

For the record, the other way you can validate a window is with a call to [font="Courier New"][color="#000080"]ValidateRect()[/color][/font]. If you want to do everything manually instead of letting [font="Courier New"][color="#000080"]BeginPaint()[/color][/font] handle it, that's fine. There may be some cases where this is necessary. So here's the prototype:

BOOL ValidateRect(
HWND hWnd, // handle of window
CONST RECT *lpRect // address of validation rectangle coordinates );
The return value is [font="Courier New"][color="#000080"]TRUE[/color][/font] or [font="Courier New"][color="#000080"]FALSE[/color][/font] for success or failure, and the parameters are easy to figure out:

[font="Courier New"][color="#000080"]HWND hWnd[/color][/font]: Are you getting tired of seeing this yet? :)

[font="Courier New"][color="#000080"]CONST RECT *lpRect[/color][/font]: This is a pointer to the [font="Courier New"][color="#000080"]RECT[/color][/font] to validate. Again, you don't need to declare it as a constant; the [font="Courier New"][color="#000080"]CONST[/color][/font] is just to make sure the function doesn't go changing things on you. If you pass [font="Courier New"][color="#000080"]NULL[/color][/font], the entire client area is validated.

Now, to wrap up our discussion of this message, I'll show you the framework for handling a [font="Courier New"][color="#000080"]WM_PAINT[/color][/font] message. This would be somewhere in your message handler, as usual. I'm assuming here that we have a global variable called [font="Courier New"][color="#000080"]hMainWindow[/color][/font] that is the handle to our window.

if (msg == WM_PAINT) {
PAINTSTRUCT ps; // declare a PAINTSTRUCT for use with this message
HDC hdc; // display device context for graphics calls
hdc = BeginPaint(hMainWindow, &ps); // validate the window

// your painting goes here!

EndPaint(hMainWindow, &ps); // release the DC

// tell Windows we took care of it
return(0);
}
The only part of that code that probably doesn't make sense is the place where I have commented, "Your painting goes here!" Well, if you want your window to be refreshed with something other than your window class's default brush, you have to do it yourself, and that involves some graphics work that you haven't seen yet. Never fear, we'll get there in just a minute! While we're on the topic of messages, though, there's something I need to explain.


[size="5"]Closing Your Application

There are three messages that seem to be practically identical, and all deal with closing things out. They are [font="Courier New"][color="#000080"]WM_DESTROY[/color][/font], [font="Courier New"][color="#000080"]WM_CLOSE[/color][/font], and [font="Courier New"][color="#000080"]WM_QUIT[/color][/font]. They're similar, but you need to know the difference! [font="Courier New"][color="#000080"]WM_CLOSE[/color][/font] is sent when a window or application should be closing. When you receive a [font="Courier New"][color="#000080"]WM_CLOSE[/color][/font] message, it's a good place to ask the user if they're sure they want to quit, if you want to do it. You know those little message boxes that are always popping up on your screen when errors or notifications occur? Well, they're easy to create. In addition to serving many functions in the final program, they're also handy for reporting debug information. The call to create your very own message box is pretty simple:

int MessageBox(
HWND hWnd, // handle of owner window
LPCTSTR lpText, // address of text in message box
LPCTSTR lpCaption, // address of title of message box
UINT uType // style of message box );
The parameters, especially the last one, require some explanation:

[font="Courier New"][color="#000080"]HWND hWnd[/color][/font]: Sooner or later we'll get to a function that doesn't have this, I promise!

[font="Courier New"][color="#000080"]LPCTSTR lpText[/color][/font]: This is the text that will appear in the message box. As always, you can use escape sequences like [font="Courier New"][color="#000080"]\n[/color][/font] to format the output a little if you want.

[font="Courier New"][color="#000080"]LPCTSTR lpCaption[/color][/font]: This is the text appearing in the message box's caption bar.

[font="Courier New"][color="#000080"]UINT uType[/color][/font]: You can combine several different flags in order to create this parameter, which defines what kind of message box it will be. There are a lot of [font="Courier New"][color="#000080"]MB_[/color][/font] constants you can use, and you can combine any number of them with the | operator. Here's a list of the useful ones:

Button Definition FlagsMB_ABORTRETRYIGNORECreates a box with "Abort," "Retry," and "Ignore" buttons.MB_OKCreates a box with an "OK" button.MB_OKCANCELCreates a box with "OK" and "Cancel" buttons.MB_RETRYCANCELCreates a box with "Retry" and "Cancel" buttons.MB_YESNOCreates a box with "Yes" and "No" buttons.MB_YESNOCANCELCreates a box with "Yes," "No," and "Cancel" buttons.Icon Definition FlagsMB_ICONEXCLAMATIONAdds an exclamation point icon to the box.MB_ICONINFORMATIONAdds an information icon to the box.MB_ICONQUESTIIONAdds a question mark icon to the box.MB_ICONSTOPAdds a stop sign icon to the box.Default Button FlagsMB_DEFBUTTON1Defines the first button as the default.MB_DEFBUTTON2Defines the second button as the default.MB_DEFBUTTON3Defines the third button as the default.MB_DEFBUTTON4Defines the fourth button as the default.Other FlagsMB_HELPAdds a help button to the box. A WM_HELP message is generated if the user chooses it or presses F1.MB_RIGHTMessage box text is right-justified.MB_TOPMOSTSets the message box to always be the topmost window.I don't know about you, but I'm starting to think that Microsoft has a programmer who does nothing but write [font="Courier New"][color="#000080"]#define[/color][/font] statements all day! Now, the return value is 0 if the box could not be created. Otherwise, the result is one of the following:

IDABORT"Abort" button was selected.IDCANCEL"Cancel" button was selected.IDIGNORE"Ignore" button was selected.IDNO"No" button was selected.IDOK"OK" button was selected.IDRETRY"Retry" button was selected.IDYES"Yes" button was selected.Those lists were so long I almost forgot what we were originally talking about. Anyway, when you receive a [font="Courier New"][color="#000080"]WM_CLOSE[/color][/font] message, you can do two things. First, you can allow the default handler to return a value. If you do this, the application or window will close as planned. However, if you return 0, the message will have no effect. This is the basis of the following bit of code:

if (msg == WM_CLOSE) {
if (MessageBox(hMainWindow,
"Are you sure want to quit?",
"Notice",
MB_YESNO | MB_ICONEXCLAMATION) == IDNO)
return(0);

// otherwise, let the default handler take care of it
}
Now, [font="Courier New"][color="#000080"]WM_DESTROY[/color][/font] is a bit different. It is sent when a window is being closed. By the time you get a [font="Courier New"][color="#000080"]WM_DESTROY[/color][/font] message, the window it applies to has already been deleted from view. If the main window closes, that does not necessarilly end the application. It will keep running, but without a window. However, when a user closes the main window, they almost always mean to close the application, so you have to post a [font="Courier New"][color="#000080"]WM_QUIT[/color][/font] message when you receive [font="Courier New"][color="#000080"]WM_DESTROY[/color][/font] if you want the application to end. You could use [font="Courier New"][color="#000080"]PostMessage()[/color][/font], but since this is a special case, there's a special function for it:

VOID PostQuitMessage(int nExitCode);
The parameter is an exit code that your application returns to Windows. Remember, [font="Courier New"][color="#000080"]WinMain()[/color][/font] returns an [font="Courier New"][color="#000080"]int[/color][/font], not a [font="Courier New"][color="#000080"]void[/color][/font]. The [font="Courier New"][color="#000080"]nExitCode[/color][/font] parameter also becomes the [font="Courier New"][color="#000080"]wparam[/color][/font] member of the [font="Courier New"][color="#000080"]WM_QUIT[/color][/font] message that results. [font="Courier New"][color="#000080"]WM_QUIT[/color][/font] represents a request to close the application, so when you get one, you should end your main loop and return [font="Courier New"][color="#000080"]wparam[/color][/font] to Windows. Here's an example of what a simplified [font="Courier New"][color="#000080"]WinMain()[/color][/font] function might look like with this in place:

int WinMain(HINSTANCE hinstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
// initialization stuff goes here

// main loop - infinite!
while (TRUE)
{
// check the message queue
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT) // exit main loop on WM_QUIT
break;

TranslateMessage(&msg);
DispatchMessage(&msg);
}

// main program logic goes here
}

// perform any shutdown functions here - releasing objects and such

return(msg.wparam); // return exit code to Windows
}
Sorry about all that stuff, but it's necessary to make sure your program behaves correctly instead of causing errors for Windows -- like that ever happens! Now instead of making you any more impatient with me than you probably already are, let's look at some basic GDI graphics.


[size="5"]Plotting Pixels

At last! Plotting pixels with GDI is a cinch as long as you've got a display device context to work with. Remember, calling [font="Courier New"][color="#000080"]GetDC()[/color][/font] does this for you. To plot a pixel, not surprisingly, you call [font="Courier New"][color="#000080"]SetPixel()[/color][/font]:

COLORREF SetPixel(
HDC hdc, // handle to device context
int X, // x-coordinate of pixel
int Y, // y-coordinate of pixel
COLORREF crColor // pixel color );
The return type is something we haven't encountered yet, a [font="Courier New"][color="#000080"]COLORREF[/color][/font]. This is not a structure, but a 32-bit value in the form [font="Courier New"][color="#000080"]0x00bbggrr[/color][/font], where [font="Courier New"][color="#000080"]bb[/color][/font] is an 8-bit value for the blue component, [font="Courier New"][color="#000080"]gg[/color][/font] is green, and [font="Courier New"][color="#000080"]rr[/color][/font] is red. The high byte is unused and is always set to zero. Let's take a look at the parameters for [font="Courier New"][color="#000080"]SetPixel()[/color][/font]:

[font="Courier New"][color="#000080"]HDC hdc[/color][/font]: This is a device context for your window that you should obtain with a call to [font="Courier New"][color="#000080"]GetDC()[/color][/font]. You only need to call [font="Courier New"][color="#000080"]GetDC()[/color][/font] once, and then you can use it for any number of these functions. Don't get a new DC every time you want to plot a pixel!

[font="Courier New"][color="#000080"]int X, Y[/color][/font]: The x- and y-coordinates of the pixel. These are in client coordinates, meaning that (0, 0) represents the upper-left corner of your window's client area, not the upper-left corner of the screen.

[font="Courier New"][color="#000080"]COLORREF crColor[/color][/font]: This is the color you want to set the pixel to. To do this, it's easiest to use the [font="Courier New"][color="#000080"]RGB()[/color][/font] macro, which takes values for red, green, and blue -- in that order -- between 0 and 255. [font="Courier New"][color="#000080"]SetPixel()[/color][/font] will choose the closest available color to the one you have specified.

If the function succeeds, the return value is the color that the pixel was set to. This may not always be exactly the [font="Courier New"][color="#000080"]COLORREF[/color][/font] you pass if you're working in less than 24-bit color; Windows will choose the closest match. If the function fails, it returns -1. As an example, if you want to set the upper-left corner of your client area to white, you'd use the following call:

SetPixel(hdc, 0, 0, RGB(255, 255, 255));
This call assumes you've gotten a display device context named [font="Courier New"][color="#000080"]hdc[/color][/font]. Pretty easy, hey? There's one other way to do it that's just a tad faster. Here's the function:

BOOL SetPixelV(
HDC hdc, // handle to device context
int X, // x-coordinate of pixel
int Y, // y-coordinate of pixel
COLORREF crColor // new pixel color );
The parameters are all the same. The return value is simply [font="Courier New"][color="#000080"]TRUE[/color][/font] or [font="Courier New"][color="#000080"]FALSE[/color][/font] for success or failure. [font="Courier New"][color="#000080"]SetPixelV()[/color][/font] is slightly faster since it doesn't need to return the actual color that was used to plot. You'll probably never even notice the difference, unless you're using it thousands of times per frame, but if you don't need the extra information [font="Courier New"][color="#000080"]SetPixel()[/color][/font] provides, there's no reason not to take the slightly increased performance, right?

The only other thing you need to know about plotting pixels is how to read the value of a pixel that's already been plotted. It's no problem; a quick call to [font="Courier New"][color="#000080"]GetPixel()[/color][/font] does the job for you:

COLORREF GetPixel(
HDC hdc, // handle to device context
int XPos, // x-coordinate of pixel
int nYPos // y-coordinate of pixel );
The return value is obviously the color of the pixel at the given coordinates. If the coordinates specified are outside the clipping region (the area represented by the device context), the return value is [font="Courier New"][color="#000080"]CLR_INVALID[/color][/font]. The parameters are the same as for [font="Courier New"][color="#000080"]SetPixel()[/color][/font]: a device context to use, and the coordinates to operate on. That's it for plotting pixels. Now let's have a look at GDI's text-rendering functions.


[size="5"]GDI Text Functions

There are two functions for actually plotting text that you need to be concerned with. The simpler of the two is [font="Courier New"][color="#000080"]TextOut()[/color][/font], as shown here:

BOOL TextOut(
HDC hdc, // handle to device context
int nXStart, // x-coordinate of starting position
int nYStart, // y-coordinate of starting position
LPCTSTR lpString, // pointer to string
int cbString // number of characters in string );
By now we've seen enough [font="Courier New"][color="#000080"]BOOL[/color][/font]-returning functions to know what that means: [font="Courier New"][color="#000080"]TRUE[/color][/font] for success, [font="Courier New"][color="#000080"]FALSE[/color][/font] for failure. The parameters are:

[font="Courier New"][color="#000080"]HDC hdc[/color][/font]: The device context to use.

[font="Courier New"][color="#000080"]int nXStart, nYStart[/color][/font]: These are the coordinates of the starting point for the text, called the reference point. By default, this is the upper-left corner of the rectangular area occupied by the string. You can change this setting, as we'll see in just a bit.

[font="Courier New"][color="#000080"]LPCTSTR lpString[/color][/font]: The text to print out. Since the number of characters is given in the final parameter, this string does not need to be null-terminated.

[font="Courier New"][color="#000080"]int cbString[/color][/font]: This is the length of the string, in characters.

[font="Courier New"][color="#000080"]TextOut()[/color][/font] uses the current settings for text color, background color, and background type. Before looking at the other, more complicated text-rendering function, let's take a look at the functions you can use to control the colors being used.

COLORREF SetTextColor(
HDC hdc, // handle to device context
COLORREF crColor // text color );

COLORREF SetBkColor(
HDC hdc, // handle of device context
COLORREF crColor // background color value );
[font="Courier New"][color="#000080"]SetTextColor()[/color][/font] sets the active text color, and [font="Courier New"][color="#000080"]SetBkColor()[/color][/font] sets the active background color. The parameters are obviously the device context to apply the settings to, and the colors to use. Since these are [font="Courier New"][color="#000080"]COLORREF[/color][/font]s, remember that you can use the [font="Courier New"][color="#000080"]RGB()[/color][/font] macro for specifying your colors. Each function returns the previous value of the attribute it deals with. For instance, if you call [font="Courier New"][color="#000080"]SetTextColor(hdc, RGB(255, 0, 0))[/color][/font], the return value will be the active color that was being used before you turned it red. Finally, to set the background type, use [font="Courier New"][color="#000080"]SetBkType()[/color][/font] as shown:

int SetBkMode(
HDC hdc, // handle of device context
int iBkMode // flag specifying background mode );
The device context parameter we've seen before, but the other, [font="Courier New"][color="#000080"]iBkMode[/color][/font], can take one of two values: [font="Courier New"][color="#000080"]TRANSPARENT[/color][/font] or [font="Courier New"][color="#000080"]OPAQUE[/color][/font]. If set to [font="Courier New"][color="#000080"]TRANSPARENT[/color][/font], any text you plot will not disturb the background around the text itself. If set to [font="Courier New"][color="#000080"]OPAQUE[/color][/font], plotting text will cause the rectangular region surrounding that text to be filled with the active background color. The return value of [font="Courier New"][color="#000080"]SetBkMode()[/color][/font] is simply the previous background mode.

One more thing about [font="Courier New"][color="#000080"]TextOut()[/color][/font]. I said you could change the way the reference point is interpreted, and the way to do it is by using [font="Courier New"][color="#000080"]SetTextAlign()[/color][/font], whose prototype is shown below.

UINT SetTextAlign(
HDC hdc, // handle to device context
UINT fMode // text-alignment flag );
The parameters are:

[font="Courier New"][color="#000080"]HDC hdc[/color][/font]: The device context again. No surprises here.

[font="Courier New"][color="#000080"]UINT fMode[/color][/font]: A flag or set of flags (logically combined with |) that determine the meaning of the reference point in a call to [font="Courier New"][color="#000080"]TextOut()[/color][/font]. Only one flag can be selected from those affecting horizontal and vertical alignment, and only one of the two flags affecting use of the current position can be used. The flags are:

TA_BASELINEThe reference point will be on the baseline of the text.TA_BOTTOMThe reference point will be on the bottom edge of the bounding rectangle.TA_TOPThe reference point will be on the top edge of the bounding rectangle.TA_CENTERThe reference point will be aligned horizontally with the center of the bounding rectangle.TA_LEFTThe reference point will be on the left edge of the bounding rectangle.TA_RIGHTThe reference point will be on the right edge of the bounding rectangle.TA_NOUPDATECPThe current position is not updated by a call to a text output function. The reference point is passed with each call.TA_UPDATECPThe current position is updated by each call to a text output function, and is used as the reference point.The default setting is [font="Courier New"][color="#000080"]TA_LEFT | TA_TOP | TA_NOUPDATECP[/color][/font]. If you set [font="Courier New"][color="#000080"]TA_UPDATECP[/color][/font], subsequent calls to [font="Courier New"][color="#000080"]TextOut()[/color][/font] will ignore the [font="Courier New"][color="#000080"]nXStart[/color][/font] and [font="Courier New"][color="#000080"]nYStart[/color][/font] parameters, and render the text where the last call left off. Now that that's out of the way, let's look at the bells-and-whistles version of [font="Courier New"][color="#000080"]TextOut()[/color][/font], called [font="Courier New"][color="#000080"]DrawText()[/color][/font]:

int DrawText(
HDC hDC, // handle to device context
LPCTSTR lpString, // pointer to string to draw
int nCount, // string length, in characters
LPRECT lpRect, // pointer to struct with formatting dimensions
UINT uFormat // text-drawing flags );
This one gets a bit complicated. Since [font="Courier New"][color="#000080"]DrawText()[/color][/font] formats text, possibly to multiple lines, the return value is the height of the text in pixels, or 0 if the function fails. Let's take a look at the parameters, shall we?

[font="Courier New"][color="#000080"]HDC hDC[/color][/font]: Nothing new here; it's just our good buddy the DC.

[font="Courier New"][color="#000080"]LPCTSTR lpString[/color][/font]: This is the string to print.

[font="Courier New"][color="#000080"]int nCount[/color][/font]: This is the length of the string in characters.

[font="Courier New"][color="#000080"]LPRECT lpRect[/color][/font]: Here's where things start to get a bit different. [font="Courier New"][color="#000080"]DrawText()[/color][/font] does several different methods of formatting, including word wrapping, so you must specify a [font="Courier New"][color="#000080"]RECT[/color][/font] within which to format the text, rather than simply passing coordinates.

[font="Courier New"][color="#000080"]UINT uFormat[/color][/font]: For this, you can use one or more (logically combined with |) of a long list of flags that represent different methods of formatting. I'll show you a few of them.

DT_BOTTOMJustifies text to the bottom of the RECT. This must be combined with DT_SINGLELINE.DT_CALCRECTCalculates the RECT needed to hold the text. If the text is on multiple lines, DrawText() uses your RECT's width and alters the height. If the text is on a single line, DrawText() alters your RECT's width. In both cases, DrawText() adjusts the RECT but does not actually draw the text.DT_CENTERCenters text within the RECT you specify.DT_EXPANDTABSIf the string contains any tabs (\t), this attribute causes DrawText() to expand them. The default is eight spaces per tab.DT_LEFTLeft-justifies the text.DT_NOCLIPDraws without clipping. This speeds up DrawText() a bit.DT_RIGHTRight-justifies the text.DT_SINGLELINEDisplays text on a single line only. Carriage returns and line feeds do not overrule this attribute.DT_TABSTOPAlters the number of spaces per tab. The number of spaces per tab must be specified in bits 15-8 (the high byte of the low word) of uFormat. Again, the default setting is eight.DT_TOPJustifies text to the top of the RECT. This must be combined with DT_SINGLELINE.DT_VCENTERCenters the text vertically within the RECT. This must be combined with DT_SINGLELINE.There are more of these flags, but you get the idea. All in all, this constitutes a pretty powerful text rendering system, but remember, all those cool features are going to slow the function down. You can usually get by just fine by using [font="Courier New"][color="#000080"]TextOut()[/color][/font]. That takes care of the text rendering system, so let's do something a little more exciting.


[size="5"]Displaying Bitmaps With GDI

Remember when I told you that bitmaps are easy to work with, because they're native to Windows? Well now we're going to find out just how easy it is. :) There are four basic steps to displaying a bitmap with GDI:
  1. Get a device context to your window.
  2. Obtain a handle to the bitmap.
  3. Create a device context for the bitmap.
  4. Copy the image from one device context to the other.
You already know how to do the first step. I alluded to the second one last time, but didn't go over it. I said that there was a function called [font="Courier New"][color="#000080"]LoadBitmap()[/color][/font] that retrieves a handle to a bitmap resource. However, this function is obsolete now; it has been superseded by [font="Courier New"][color="#000080"]LoadImage()[/color][/font], which is much more flexible. So that's what we'll be using. Here she is:

HANDLE LoadImage(
HINSTANCE hinst, // handle of the instance containing the image
LPCTSTR lpszName, // name or identifier of image
UINT uType, // type of image
int cxDesired, // desired width
int cyDesired, // desired height
UINT fuLoad // load flags );
The function returns [font="Courier New"][color="#000080"]NULL[/color][/font] if it fails. Otherwise, you get a handle to the bitmap, which can either be loaded from a resource or from an external file. Notice that since this function can be used for bitmaps, cursors, or icons, the return type is simply [font="Courier New"][color="#000080"]HANDLE[/color][/font]. In Visual C++ 6.0, you'll need to include a typecast to [font="Courier New"][color="#000080"]HBITMAP[/color][/font] or the compiler will become angry with you. Here are the parameters for the function:

[font="Courier New"][color="#000080"]HINSTANCE hinst[/color][/font]: This should be the instance of your application if you're loading a resource, or [font="Courier New"][color="#000080"]NULL[/color][/font] if you want to load from an external file.

[font="Courier New"][color="#000080"]LPCTSTR lpszName[/color][/font]: This is either the resource identifier -- remember to use [font="Courier New"][color="#000080"]MAKEINTRESOURCE()[/color][/font] if you're using numerical constants -- or the full filename of the image you want to load.

[font="Courier New"][color="#000080"]UINT uType[/color][/font]: Depending on what you want to load, this should be set to either [font="Courier New"][color="#000080"]IMAGE_BITMAP[/color][/font], [font="Courier New"][color="#000080"]IMAGE_CURSOR[/color][/font], or [font="Courier New"][color="#000080"]IMAGE_ICON[/color][/font].

[font="Courier New"][color="#000080"]int cxDesired, cyDesired[/color][/font]: These are the desired dimensions for the image to be loaded. If you set them to zero, the image's actual dimensions will be used.

[font="Courier New"][color="#000080"]UINT fuLoad[/color][/font]: Like everything else we've done today, this is one or more of a series of flags which can be logically combined with the | operator. Here are the useful flags:

LR_CREATEDIBSECTIONIf uType is IMAGE_BITMAP, this causes the function to return a DIB section rather than a compatible bitmap. (DIB stands for device-independent bitmap.) This basically means to use all of the bitmap's own properties rather than making it conform to the properties of the display device.LR_DEFAULTSIZEFor icons and cursors, if cxDesired and cyDesired are set to 0, this flag causes the system metric values for icons and cursors to be used, rather than the actual dimensions of the image.LR_LOADFROMFILEYou must specify this flag if you want to load from a file rather than a resource. For loading bitmaps you should use [font="Courier New"][color="#000080"]LR_CREATEDIBSECTION[/color][/font], and [font="Courier New"][color="#000080"]LR_LOADFROMFILE[/color][/font] if it is appropriate. Now that you have obtained a handle to your image, you must create a device context and load the bitmap into it. The first step is taken by calling [font="Courier New"][color="#000080"]CreateCompatibleDC()[/color][/font], as follows:

HDC CreateCompatibleDC(HDC hdc);
The parameter is a DC with which to make the new DC compatible. If you pass [font="Courier New"][color="#000080"]NULL[/color][/font], the DC will be made compatiable with the display screen, which is what we want. The return value is a handle to a memory device context -- not a display device context! This means that the contents of this DC won't be visible. If the function fails, the return value is [font="Courier New"][color="#000080"]NULL[/color][/font]. Now, to get the bitmap into the memory device context, we use this:

HGDIOBJ SelectObject(
HDC hdc, // handle to device context
HGDIOBJ hgdiobj // handle to object );
The type [font="Courier New"][color="#000080"]HGDIOBJ[/color][/font] is more general than our [font="Courier New"][color="#000080"]HBITMAP[/color][/font], so never fear, they're compatible without any tricks on our part. Here are the parameters:

[font="Courier New"][color="#000080"]HDC hdc[/color][/font]: This is a handle to the device context which we want to fill with an object. For loading bitmaps, this must be a memory device context.

[font="Courier New"][color="#000080"]HGDIOBJ hgdiobj[/color][/font]: And this is a handle to that object. This function is used with bitmaps, brushes, fonts, pens, and regions; but the only one that concerns us is bitmaps.

The return value is a handle to the object that is being replaced in the DC, or [font="Courier New"][color="#000080"]NULL[/color][/font] if an error occurs. The return values are different for regions, but like I said, we don't care about regions. :)

Now you've got a bitmap loaded into a DC, and you need only take the last step: copying the contents of the memory device context to our display device context. However, it's necessary to obtain some information about the bitmap, such as its dimensions, which must be used in the function call that will display the image. For that, we need the [font="Courier New"][color="#000080"]GetObject()[/color][/font] function, which is used for obtaining information about graphical objects such as bitmaps.

int GetObject(
HGDIOBJ hgdiobj, // handle to graphics object of interest
int cbBuffer, // size of buffer for object information
LPVOID lpvObject // pointer to buffer for object information );
The return value is the number of bytes successfully obtained, or 0 for function failure. The parameters for the function are the following:

[font="Courier New"][color="#000080"]HGDIOBJ hgdiobj[/color][/font]: The handle to the graphics object we want information on. In this case, pass the handle to the bitmap we loaded.

[font="Courier New"][color="#000080"]int cbBuffer[/color][/font]: This is the size of the structure receiving the information. In the case of loading bitmaps, the receiving structure is of type [font="Courier New"][color="#000080"]BITMAP[/color][/font], so set this to [font="Courier New"][color="#000080"]sizeof(BITMAP)[/color][/font].

[font="Courier New"][color="#000080"]LPVOID lpvObject[/color][/font]: Pass the address of the structure receiving the information.

You need to define a variable of type [font="Courier New"][color="#000080"]BITMAP[/color][/font], and with a quick call to the [font="Courier New"][color="#000080"]GetObject()[/color][/font] function, you'll have the information you need. Since the [font="Courier New"][color="#000080"]BITMAP[/color][/font] structure is new to us, I'll show you what it looks like:

typedef struct tagBITMAP { // bm
LONG bmType;
LONG bmWidth;
LONG bmHeight;
LONG bmWidthBytes;
WORD bmPlanes;
WORD bmBitsPixel;
LPVOID bmBits;
} BITMAP;

There aren't too many members to this thing, and we're really only interested in two of them, but I'll list them all here anyway.

[font="Courier New"][color="#000080"]LONG bmType[/color][/font]: This is the bitmap type and must be set to zero. Useful, isn't it?

[font="Courier New"][color="#000080"]LONG bmWidth, bmHeight[/color][/font]: These are the two we're after -- the width and height of the bitmap, in pixels.

[font="Courier New"][color="#000080"]LONG bmWidthBytes[/color][/font]: Specifies the number of bytes in each line of the bitmap. Note that the number of bytes per pixel can be obtained by dividing this value by [font="Courier New"][color="#000080"]bmWidth[/color][/font].

[font="Courier New"][color="#000080"]LONG bmPlanes[/color][/font]: This is the number of color planes.

[font="Courier New"][color="#000080"]LONG bmBitsPixel[/color][/font]: This is the number of bits required to represent one pixel. It would appear that the note I left about figuring this out is useless. :)

[font="Courier New"][color="#000080"]LPVOID bmBits[/color][/font]: If you want to access the actual image data, this is a pointer to the bit values for the bitmap.

All right, almost done! Now we have the bitmap in a memory device context, and we know its dimensions. All we have to do is cop
Cancel Save
0 Likes 0 Comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement