Handling Important EventsAs you've been painfully learning, Windows is an event-based operating system. Responding to events is one of the most important aspects of a standard Windows program. This next section covers some of the more important events that have to do with window manipulation, input devices, and timing. If you can handle these basic events, you'll have more than you need in your Windows arsenal to handle anything that might come up as part of a DirectX game, which itself relies very little on events and the Windows operating system. Window ManipulationThere are a number of messages that Windows sends to notify you that the user has manipulated your window. Table 3.4 contains a small list of some of the more interesting manipulation messages that Windows generates. Let's take a look at WM_ACTIVATE, WM_CLOSE, WM_SIZE, and WM_MOVE and what they do. For each one of these messages, I'm going to list the message, wparam, lparam, and some comments, along with a short example WinProc() handler for the event. Parameterization: fActive = LOWORD(wParam); // activation flag fMinimized = (BOOL)HIWORD(wParam); // minimized flag hwndPrevious = (HWND)lParam; // window handle The fActive parameter basically defines what is happening to the window—that is, is the window being activated or deactivated? This information is stored in the low-order word of wparam and can take on the values shown in Table 3.5.
The fMinimized variable simply indicates if the window was minimized. This is true if the variable is nonzero. Lastly, the hwndPrevious value identifies the window being activated or deactivated, depending on the value of the fActive parameter. If the value of fActive is WA_INACTIVE, hwndPrevious is the handle of the window being activated. If the value of fActive is WA_ACTIVE or WA_CLICKACTIVE, hwndPrevious is the handle of the window being deactivated. This handle can be NULL. That makes sense, huh? In essence, you use the WM_ACTIVATE message if you want to know when your application is being activated or deactivated. This might be useful if your application keeps track of every time the user Alt+Tabs away or selects another application with the mouse. On the other hand, when your application is reactivated, maybe you want to play a sound or do something. Whatever, it's up to you. Here's how you code when your application is being activated in the main WinProc(): case WM_ACTIVATE: { // test if window is being activated if (LOWORD(wparam)!=WA_INACTIVE) { // application is being activated } // end if else { // application is being deactivated } // end else } break; Parameterization: None The WM_CLOSE message is very cool. It is sent right before a WM_DESTROY and the following WM_QUIT are sent. The WM_CLOSE indicates that the user is trying to close your window. If you simply return(0) in your WinProc(), nothing will happen and the user won't be able to close your window! Take a look at DEMO3_7.CPP and the executable DEMO3_7.EXE to see this in action. Try killing the application—you won't be able to! WARNING Don't panic when you can't kill DEMO3_7.EXE. Simply press Ctrl+Alt+Del, and the Task Manager will come up. Then select and terminate the DEMO3_7.EXE application. It will cease to exist—just like service at electronics stores starting with "F" in Silicon Valley. Here's the coding of the empty WM_CLOSE handler in the WinProc() as coded in DEMO3_7.CPP: case WM_CLOSE: { // kill message, so no further WM_DESTROY is sent return(0); } break; If making the user mad is your goal, the preceding code will do it. However, a better use of trapping the WM_CLOSE message might be to include a message box that confirms that the application is going to close or maybe do some housework. DEMO3_8.CPP and the executable take this route. When you try to close the window, a message box asks if you're certain. The logic flow for this is shown in Figure 3.20. Figure 3.20. The logic flow for WM_CLOSE.Here's the code from DEMO3_8.CPP that processes the WM_CLOSE message: case WM_CLOSE: { // display message box int result = MessageBox(hwnd, "Are you sure you want to close this application?", "WM_CLOSE Message Processor", MB_YESNO | MB_ICONQUESTION); // does the user want to close? if (result == IDYES) { // call default handler return (DefWindowProc(hwnd, msg, wparam, lparam)); } // end if else // throw message away return(0); } break; Cool, huh? Notice the call to the default message handler, DefWindowProc(). This occurs when the user answers Yes and you want the standard shutdown process to continue. If you knew how to, you could have sent a WM_DESTROY message instead, but since you haven't learned how to send messages yet, you just called the default handler. Either way is fine, though. Next, let's take a look at the WM_SIZE message, which is an important message to process if you've written a windowed game and the user keeps resizing the view window! Message: WM_SIZE Parameterization: fwSizeType = wParam; // resizing flag nWidth = LOWORD(lParam); // width of client area nHeight = HIWORD(lParam); // height of client area The fwSizeType flag indicates what kind of resizing just occurred, as shown in Table 3.6, and the low and high word of lParam indicate the new window client dimensions.
As I said, processing the WM_SIZE message can be very important for windowed games because when the window is resized, the graphics display must be scaled to fit. This will never happen if your game is running in full-screen, but in a windowed game, you can count on the user trying to make the window larger and smaller. When this happens, you must recenter the display and scale the universe or whatever to keep the image looking correct. As an example of tracking the WM_SIZE message, DEMO3_9.CPP prints out the new size of the window as it's resized. The code that tracks the WM_SIZE message in DEMO3_9.CPP is shown here: case WM_SIZE: { // extract size info int width = LOWORD(lparam); int height = HIWORD(lparam); // get a graphics context hdc = GetDC(hwnd); // set the foreground color to green SetTextColor(hdc, RGB(0,255,0)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // set the transparency mode to OPAQUE SetBkMode(hdc, OPAQUE); // draw the size of the window sprintf(buffer, "WM_SIZE Called - New Size = (%d,%d)", width, height); TextOut(hdc, 0,0, buffer, strlen(buffer)); // release the dc back ReleaseDC(hwnd, hdc); } break; WARNING You should know that the code for the WM_SIZE message handler has a potential problem: When a window is resized, not only is a WM_SIZE message sent, but a WM_PAINT message is sent as well! Therefore, if the WM_PAINT message was sent after the WM_SIZE, the code in WM_PAINT could erase the background and thus the information just printed in WM_SIZE. Luckily, this isn't the case, but it's a good example of problems that can occur when messages are out of order or when they aren't sent in the order you think they are. Last, but not least, let's take a look at the WM_MOVE message. It's almost identical to WM_SIZE, but it is sent when a window is moved rather than resized. Here are the details: Parameterization: xPos = (int) LOWORD(lParam); // new horizontal position in screen coords yPos = (int) HIWORD(lParam); // new vertical position in screen coords WM_MOVE is sent whenever a window is moved to a new position, as shown in Figure 3.21. However, the message is sent after the window has been moved, not during the movement in real time. If you want to track the exact pixel-by-pixel movement of a window, you need to process the WM_MOVING message. However, in most cases, processing stops until the user is done moving your window. Figure 3.21. Generation of the WM_MOVE message.As an example of tracking the motion of a window, DEMO3_10.CPP and the associated executable DEMO3_10.EXE print out the new position of a window whenever it's moved. Here's the code that handles the WM_MOVE processing: case WM_MOVE: { // extract the position int xpos = LOWORD(lparam); int ypos = HIWORD(lparam); // get a graphics context hdc = GetDC(hwnd); // set the foreground color to green SetTextColor(hdc, RGB(0,255,0)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // set the transparency mode to OPAQUE SetBkMode(hdc, OPAQUE); // draw the size of the window sprintf(buffer, "WM_MOVE Called - New Position = (%d,%d)", xpos, ypos); TextOut(hdc, 0,0, buffer, strlen(buffer)); // release the dc back ReleaseDC(hwnd, hdc); } break; Well, that's it for window manipulation messages. There are a lot more, obviously, but you should have the hang of it now. The thing to remember is that there is a message for everything. If you want to track something, just look in the Win32 Help and sure enough, you'll find a message that works for you! The next sections cover input devices so you can interact with the user (or yourself) and make much more interesting demos and experiments that will help you master Windows programming. Banging on the KeyboardBack in the old days, accessing the keyboard required sorcery. You had to write an interrupt handler, create a state table, and perform a number of other interesting feats to make it work. I'm a low-level programmer, but I can say without regret that I don't miss writing keyboard handlers anymore! Ultimately you're going to use DirectInput to access the keyboard, mouse, joystick, and any other input devices. Nevertheless, you still need to learn how to use the Win32 library to access the keyboard and mouse. If for nothing else, you'll need them to respond to GUI interactions and/or to create more engaging demos throughout the book until we cover DirectInput. So without further ado, let's see how the keyboard works. The keyboard consists of a number of keys, a microcontroller, and support electronics. When you press a key or keys on the keyboard, a serial stream of packets is sent to Windows describing the key(s) that you pressed. Windows then processes this stream and sends your window keyboard event messages. The beauty is that under Windows, you can access the keyboard messages in a number of ways:
Each one of these methods works in a slightly different manner. The WM_CHAR and WM_KEYDOWN messages are generated by Windows whenever a keyboard keypress or event occurs. However, there is a difference between the types of information encapsulated in the two messages. When you press a key on the keyboard, such as A, two pieces of data are generated:
The scan code is a unique code that is assigned to each key of the keyboard and has nothing to do with ASCII. In many cases, you just want to know if the A key was pressed; you're not interested in whether or not the Shift key was held down and so on. Basically, you just want to use the keyboard like a set of momentary switches. This is accomplished by using scan codes. The WM_KEYDOWN message is responsible for generating scan codes when keys are pressed. The ASCII code, on the other hand, is cooked data. This means that if you press the A key on the keyboard but the Shift key is not pressed or the Caps Lock key is not engaged, you see an a character. Similarly, if you press Shift+A, you see an A. The WM_CHAR message sends these kinds of messages. You can use either technique—it's up to you. For example, if you were writing a word processor, you would probably want to use the WM_CHAR message because the character case matters and you want ASCII codes, not virtual scan codes. On the other hand, if you're making a game and F is fire, S is thrust, and the Shift key is the shields, who cares what the ASCII code is? You just want to know if a particular button on the keyboard is up or down. The final method of reading the keyboard is to use the Win32 function GetAsyncKeyState(), which tracks the last known keyboard state of the keys in a state table—like an array of Boolean switches. This is the method I prefer because you don't have to write a keyboard handler. Now that you know a little about each method, let's cover the details of each one in order, starting with the WM_CHAR message. The WM_CHAR message has the following parameterization:
To process the WM_CHAR message, all you have to do is write a message handle for it, like this: case WM_CHAR: { // extract ascii code and state vector int ascii_code = wparam; int key_state = lparam; // take whatever action } break; And of course, you can test for various state information that might be of interest. For example, here's how you would test for the Alt key being pressed down: // test the 29th bit of key_state to see if it's true #define ALT_STATE_BIT 0x20000000 if (key_state & ALT_STATE_BIT) { // do something } // end if And you can test for the other states with similar bitwise tests and manipulations. As an example of processing the WM_CHAR message, I have created a demo that prints out the character and the state vector in hexadecimal form as you press keys. The program is called DEMO3_11.CPP, and the executable is of course DEMO3_11.EXE. Try pressing weird key combinations and see what happens. The code that processes and displays the WM_CHAR information is shown here, excerpted from the WinProc(): case WM_CHAR: { // get the character char ascii_code = wparam; unsigned int key_state = lparam; // get a graphics context hdc = GetDC(hwnd); // set the foreground color to green SetTextColor(hdc, RGB(0,255,0)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // set the transparency mode to OPAQUE SetBkMode(hdc, OPAQUE); // print the ascii code and key state sprintf(buffer,"WM_CHAR: Character = %c ",ascii_code); TextOut(hdc, 0,0, buffer, strlen(buffer)); sprintf(buffer,"Key State = 0X%X ",key_state); TextOut(hdc, 0,16, buffer, strlen(buffer)); // release the dc back ReleaseDC(hwnd, hdc); } break; The next keyboard event message, WM_KEYDOWN, is similar to WM_CHAR, except that the information is not "cooked." The key data sent during a WM_KEYDOWN message is the virtual scan code of the key rather than the ASCII code. The virtual scan codes are similar to the standard scan codes generated by any keyboard, except that virtual scan codes are guaranteed to be the same for any keyboard. For example, it's possible that the scan code for a particular key on your 101 AT–style keyboard is 67, but on another manufacturer's keyboard, it might be 69. See the problem? The solution used in Windows was to virtualize the real scan codes to virtual scan code with a lookup table. As programmers, we use the virtual scan codes and let Windows do the translation. Thanks, Windows! With that in mind, here are the details of the WM_KEYDOWN message: Message: WM_KEYDOWN Wparam—Contains the virtual key code of the key pressed. Table 3.8 contains a list of the most common keys that you might be interested in. lpara—Contains a bit-encoded state vector that describes other special control keys that may be pressed. The bit encoding is shown in Table 3.8. In addition to the WM_KEYDOWN message, there is WM_KEYUP. It has the same parameterization—that is, wparam contains the virtual key code, and lparam contains the key state vector. The only difference is that WM_KEYUP is sent when a key is released. For example, if you're using the WM_KEYDOWN message to control something, take a look at the code here: case WM_KEYDOWN: { // get virtual key code and data bits int virtual_code = (int)wparam; int key_state = (int)lparam; // switch on the virtual_key code to be clean switch(virtual_code) { case VK_RIGHT:{} break; case VK_LEFT: {} break; case VK_UP: {} break; case VK_DOWN: {} break; // more cases... default: break; } // end switch // tell windows that you processed the message return(0); } break; As an experiment, try modifying the code in DEMO3_11.CPP to support the WM_KEYDOWN message instead of WM_CHAR. When you're done, come back and we'll talk about the last method of reading the keyboard. The final method of reading the keyboard is to make a call to one of the keyboard state functions: GetKeyboardState(), GetKeyState(), or GetAsyncKeyState(). We'll focus on GetAsyncKeyState() because it works for a single key, which is what you're usually interested in rather than the entire keyboard. If you're interested in the other functions, you can always look them up in the Win32 SDK. Anyway, GetAsyncKeyState() as the following prototype: SHORT GetAsyncKeyState(int virtual_key); You simply send the function the virtual key code that you want to test, and if the high bit of the return value is 1, the key is pressed. Otherwise, it's not. I have written some macros to make this easier: #define KEYDOWN(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 1 : 0) #define KEYUP(vk_code) ((GetAsyncKeyState(vk_code) & 0x8000) ? 0 : 1) The beauty of using GetAsyncKeyState() is that it's not coupled to the event loop. You can test for keypresses anywhere you want. For example, say that you're writing a game and you want to track the arrow keys, spacebar, and maybe the Ctrl key. You don't want to have to deal with the WM_CHAR or WM_KEYDOWN messages; you just want to code something like this: if (KEYDOWN(VK_DOWN)) { // move ship down, whatever } // end if if (KEYDOWN(VK_SPACE)) { // fire weapons maybe? } // end if // and so on Similarly, you might want to detect when a key is released to turn something off. Here's an example: if (KEYUP(VK_ENTER)) { // disengage engines } // end if As an example, I have created a demo that continually prints out the status of the arrow keys in the WinMain(). It's called DEMO3_12.CPP, and the executable is DEMO3_12.EXE. Here's the WinMain() from the program: int WINAPI WinMain(HINSTANCE hinstance, HINSTANCE hprevinstance, LPSTR lpcmdline, int ncmdshow) { WNDCLASSEX winclass; // this will hold the class we create HWND hwnd; // generic window handle MSG msg; // generic message HDC hdc; // graphics device context // first fill in the window class stucture winclass.cbSize = sizeof(WNDCLASSEX); winclass.style = CS_DBLCLKS | CS_OWNDC | CS_HREDRAW | CS_VREDRAW; winclass.lpfnWndProc = WindowProc; winclass.cbClsExtra = 0; winclass.cbWndExtra = 0; winclass.hInstance = hinstance; winclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); winclass.hCursor = LoadCursor(NULL, IDC_ARROW); winclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); winclass.lpszMenuName = NULL; winclass.lpszClassName = WINDOW_CLASS_NAME; winclass.hIconSm = LoadIcon(NULL, IDI_APPLICATION); // save hinstance in global hinstance_app = hinstance; // register the window class if (!RegisterClassEx(&winclass)) return(0); // create the window if (!(hwnd = CreateWindowEx(NULL, // extended style WINDOW_CLASS_NAME, // class "GetAsyncKeyState() Demo", // title WS_OVERLAPPEDWINDOW | WS_VISIBLE, 0,0, // initial x,y 400,300, // initial width, height NULL, // handle to parent NULL, // handle to menu hinstance,// instance of this application NULL))) // extra creation parms return(0); // save main window handle main_window_handle = 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 // get a graphics context hdc = GetDC(hwnd); // set the foreground color to green SetTextColor(hdc, RGB(0,255,0)); // set the background color to black SetBkColor(hdc, RGB(0,0,0)); // set the transparency mode to OPAQUE SetBkMode(hdc, OPAQUE); // print out the state of each arrow key sprintf(buffer,"Up Arrow: = %d ",KEYDOWN(VK_UP)); TextOut(hdc, 0,0, buffer, strlen(buffer)); sprintf(buffer,"Down Arrow: = %d ",KEYDOWN(VK_DOWN)); TextOut(hdc, 0,16, buffer, strlen(buffer)); sprintf(buffer,"Right Arrow: = %d ",KEYDOWN(VK_RIGHT)); TextOut(hdc, 0,32, buffer, strlen(buffer)); sprintf(buffer,"Left Arrow: = %d ",KEYDOWN(VK_LEFT)); TextOut(hdc, 0,48, buffer, strlen(buffer)); // release the dc back ReleaseDC(hwnd, hdc); } // end while // return to Windows like this return(msg.wParam); } // end WinMain Also, if you review the entire source on the CD-ROM, you'll notice that there aren't handlers for WM_CHAR or WM_KEYDOWN in the message handler for the window. The fewer messages that you have to handle in the WinProc(), the better! In addition, this is the first time you have seen action taking place in the WinMain(), which is the section that does all game processing. Notice that there isn't any timing delay or synchronization, so the redrawing of the information is free-running (in other words, working as fast as possible). In Chapter 4, "Windows GDI, Controls, and Last-Minute Gift Ideas," you'll learn about timing issues, how to keep processes locked to a certain frame rate, and so forth. But for now, let's move on to the mouse. Squeezing the MouseThe mouse is probably the most innovative computer input device ever created. You point and click, and the mouse pad is physically mapped to the screen surface—that's innovation! Anyway, as you guessed, Windows has a truckload of messages for the mouse, but we're going to look at only two classes of messages: WM_MOUSEMOVE and WM_*BUTTON*. Let's start with the WM_MOUSEMOVE message. The first thing to remember about the mouse is that its position is relative to the client area of the window that it's in. Referring to Figure 3.22, the mouse sends coordinates relative to the upper-left corner of your window, which is 0,0. Figure 3.22. The details of mouse movement.Other than that, the WM_MOUSEMOVE message is fairly straightforward. Message: WM_MOUSEMOVE Parameterization: int mouse_x = (int)LOWORD(lParam); int mouse_y = (int)HIWORD(lParam); int buttons = (int)wParam; Basically, the position is encoded as 16-bit entries in the lparam, and the buttons are encoded in the wparam, as shown in Table 3.9.
So all you have to do is logically AND one of the bit codes with the button state and you can detect which mouse buttons are pressed. Here's an example of tracking the x,y position of the mouse along with the left and right buttons: case WM_MOUSEMOVE: { // get the position of the mouse int mouse_x = (int)LOWORD(lParam); int mouse_y = (int)HIWORD(lParam); // get the button state int buttons = (int)wParam; // test if left button is down if (buttons & MK_LBUTTON) { // do something } // end if // test if right button is down if (buttons & MK_RBUTTON) { // do something } // end if } break; Trivial, ooh, trivial! For an example of mouse tracking, take a look at DEMO3_13.CPP on the CD-ROM and the associated executable. The program prints out the position of the mouse and the state of the buttons using the preceding code as a starting point. Take note of how the button changes only when the mouse is moving. This is as you would expect because the message is sent when the mouse moves rather than when the buttons are pressed. Now for some details. The WM_MOUSEMOVE is not guaranteed to be sent all the time. You may move the mouse too quickly for it to track. Therefore, don't assume that you'll be able to track individual mouse movements that well—for the most part, it's not a problem, but keep it in mind. Also, you should be scratching your head right now, wondering how to track if a mouse button was pressed without a mouse move. Of course, there is a whole set of messages just for that. Take a look at Table 3.10. The button messages also have the position of the mouse encoded just as they were for the WM_MOUSEMOVE message—in the wparam and lparam. For example, to test for a left button double-click, you would do this: case WM_LBUTTONDBLCLK: { // extract x,y and buttons int mouse_x = (int)LOWORD(lParam); int mouse_y = (int)HIWORD(lParam); // do something intelligent // tell windows you handled it return(0); } // break; Killer! I feel powerful, don't you? Windows is almost at our feet! |