[ Nomad Programming, Graphics, and Algorithms Tutorial ] [ Compiler : MS Visual C++ 6.0 ] [ Part 8 - Keyboard Handling ] [ http://nomad.openglforums.com ] <<------------------------------------------------------>> [ DISCLAIMER ] I will hold no responsibility to whatever happens to you, your computer, your sanity, your pet, or whatever that may happen to your existence for your reading these texts and for the outcome of the various source code(s) given in each tutorial. So in short, read at your own risk! [ INTRODUCTION ] In this tutorial, I'll discuss how to grab keyboard inputs as well as show you guys how to disable the Alt-F4 key-combination in your program. One of its use you probably know already (ahem, games...:) ). This is one of the "more important" tutorials that I shall be discussing. The reason? Obviously, unless you're creating a small demo program that doesn't require user input, then you'd best learn handling inputs well...;). SideNote: To those of you who think that learning this would not benefit you since you'll be learning or have learned (somehow) DirectX's DirectInput, well, it might surprise you guys that the performance of what will be discussed here is equivalent of that of DirectInput. What's good about DirectInput, however, is its support for joystick, force-feedback devices, and its action-mapping concepts, but for pure mouse and/or keyboard handling, DirectInput performs equally well with what will be discussed here and in Part 9 of my tutorials...:). [ TUTORIAL ] Let me start by telling you that there are two ways that you could detect (or read) keyboard inputs (I'm not sure if there ARE only two ways, I just know only two...hehe...:) ). The first one will be through the WM_KEYDOWN or WM_KEYUP messages for non-system keystrokes and the WM_SYSKEYDOWN and WM_SYSKEYUP for system keystrokes. (Another message, WM_CHAR, will be discussed further later...;) ) Simply put, system keystrokes are the key-combinations where MS Windows has more priority (such as Alt-Tab and Alt-Esc). The WM_SYSKEYDOWN and WM_SYSKEYUP are usually sent to our window's WndProc for keys typed in combination with the Alt key. The WM_KEYDOWN and WM_KEYUP messages, on the other hand, are usually generated for key combinations WITHOUT the Alt key. You'd usually get inputs through WM_KEYDOWN and WM_KEYUP. You should know by now that all window messages are handled inside a window's WndProc (i.e. you handle messages inside your window's event handler). Now, for the four messages mentioned above, the wParam parameter of WndProc will hold the virtual key code (whooooo...:) ) of the keyboard input. I can just hear some of you saying: "What the heck is this guy talking about? Virtual what??!??!...Curses! This is going to be difficult isn't it?!?"... Err...no, it's not going to be difficult, in fact, it's too darn simple...don't believe me? Read on then...:). As stated above, for the four messages introduced above, the wParam in our window's WndPoc will hold the virtual key code...simply put, virtual key codes are "codenames" for the keys that you see in your, well, keyboard! =)! Let's have some examples: VK_BACK - Backspace Key VK_RETURN - Enter Key (either one, if you have two) VK_SHIFT - Shift Keys (either one also) VK_CAPITAL - Caps Lock Key VK_PRIOR - Page Up Key Obviously, this means that if the user presses the Enter key for example, the wParam in our window's WndProc will have the value VK_RETURN. "Ahh...I get it now!" - Previously Confused Reader Yup, I knew you would...;). OK, so what we need now is know the virtual key codes that we could grab from our wParam. Here's a list that I prepared just for you (yeah, I know, I'm very nice...:) ): //----8<----[ LIST BEGIN ]--------/ Decimal Hex Virtual Key Code Key Counterpart ------- --- ---------------- --------------- 8 08 VK_BACK Backspace 9 09 VK_TAB Tab 13 0D VK_RETURN Enter (either one) 16 10 VK_SHIFT Shift (either one) 17 11 VK_CONTROL Ctrl (either one) 18 12 VK_MENU Alt (either one) 19 13 VK_PAUSE Pause 20 14 VK_ESCAPE Esc 32 20 VK_SPACE Spacebar 33 21 VK_PRIOR Page Up 34 22 VK_NEXT Page Down 35 23 VK_END End 36 24 VK_HOME Home 37 25 VK_LEFT Left Arrow 38 26 VK_UP Up Arrow 39 27 VK_RIGHT Right Arrow 40 28 VK_DOWN Down Arrow 44 2C VK_SNAPSHOT Print Screen 45 2D VK_INSERT Insert 46 2E VK_DELETE Delete 48-57 30-39 None 0 to 9 on main keyboard 65-90 41-5A None A to Z 96-105 60-69 VK_NUMPAD0 to VK_NUMPAD9 Numeric Keypad 0 to 9 w/ Num Lock ON 106 6A VK_MULTIPLY Numeric Keypad * 107 6B VK_ADD Numeric Keypad + 109 6D VK_SUBTRACT Numeric Keypad - 110 6E VK_DECIMAL Numeric Keypad . 111 6F VK_DIVIDE Numeric Keypad / 112-121 70-79 VK_F1 to VK_F10 Function Keys F1 to F10 122-135 7A-87 VK_F11 to VK_F24 Function Keys F11 to F24 144 90 VK_NUMLOCK Num Lock 145 91 VK_SCROLL Scroll Lock //----8<----[ LIST END ]--------// Wow! What a long list! And to think I removed some keycodes already...:). (Don't worry, those I've removed aren't really used or found in our normal QWERTY keyboard anyway, so it's OK...:) ) Notice that we do not have virtual key codes for 0 to 9 and A to Z...notice also that 'A' and 'a' have the same decimal / hexadecimal value...well, remember about my very brief mention of WM_CHAR above? Well, we use this message to grab characters 0 to 9 and A to Z (regardless if they are in uppercase or lowercase...;) ). Now look at the code below: //----8<----[ CODE BEGIN ]--------// while(GetMessage(&msg, hWindow, 0,0)) // keep on getting msgs { TranslateMessage(&msg); // convert to "known" msg DispatchMessage(&msg); // send to msg queue } //----8<----[ CODE END ]--------// Familiar? It should be, it's where you keep on grabbing messages...it's usually located in your window's WinMain(). Notice the TranslateMessage() function, this function helps us in grabbing character input by "converting" keyboard character input into ASCII characters...this means that when the message is dispatched (through the DispatchMessage()), the wParam of our window's WndProc will contain the ASCII equivalent of our character input. Your WndProc will look something along the lines of: //----8<----[ CODE BEGIN ]--------// LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case WM_CHAR: // received a character message switch(wParam) // check what character was pressed through wParam { case 'a': case 'A': // ... // Place your code here in response to keyboard input A or a // ... break; case 'b': case 'B': // ... // Place your code here in response to keyboard input B or b // ... break; // ...and so on... } break; // break from WM_CHAR message case WM_KEYUP: switch(wParam) { case VK_F1: // user has pressed and released the F1 key // ... // Place your code here in response to keyboard input F1 // ... break; // ...and so on... } break; // break from WM_KEYUP message // ...and so on... //----8<----[ CODE END ]--------// Now you might be wond'rin' why there's a decimal and hex value counterpart for each virtual keycodes...well, again, messages are passed onto our window's WndProc (specifically, the wParam parameter), and well, wParam means that it's a word (the "w" in "wParam" gave it away...:) ), and a word accepts an integer best...which means all of the virtual keycodes are actually #define'd elsewhere with an equivalent hexadecimal value (I'm not sure with other compilers, but with Visual C++ 6.0, it's in the WINUSER.H file, under the INCLUDE directory...;) ). This means you could use these decimal and hex values instead of the pre-defined virtual keycodes made by Microsoft...(this also means you could #define your own virtual key code and give each key your own name...;) ). To summarize so far: o WM_CHAR messages are for character inputs (by 'character', I mean, all the characters that you can type on your favorite text editor...;) ) o WM_KEYDOWN and WM_KEYUP are for non-system keystrokes. It is up to you if you want your program to respond while the key is pressed down (WM_KEYDOWN) or when the key is released (WM_KEYUP) o WM_SYSKEYDOWN and WM_SYSKEYUP are for system keystrokes. Again, it is up to you if you want your program to respond while the key is pressed down (WM_SYSKEYDOWN) or when the key is released (WM_SYSKEYUP) "OK...I'm confused...I have, say VK_MULTIPLY which I can grab with the WM_KEYDOWN or WM_KEYUP message, but I can also just grab the equivalent '*' character through the WM_CHAR message...which should I use?" -- Newly Perplexed Reader Actually, it is up to you...just let your program react correctly when the user presses the asterisk key...;). OK, we're half way through this tutorial...tired? I hope not...I'm the one doing the typing here...=). Look at the NOMAD.SOURCE Section below to find out how to handle keyboard inputs through window messages. Now, the second and simpler method (which I prefer) is: //----8<----[ CODE BEGIN ]--------// if (GetKeyState(VIRTUAL_KEY_CODE_HERE) & 0x80) { // Do stuffs here corresponding to what // key the user has pressed } //----8<----[ CODE END ]--------// The above code allows you to grab keyboard inputs from anywhere in your code and not just your window's WndProc (nice huh? -- just replace "VIRTUAL_KEY_CODE_HERE" in the code above with any virtual key code)...;). The only thing that should baffle you with the above code is the introduction of GetKeyState()...simply put, GetKeyState() accepts a virtual key code (or any of its decimal and hexadecimal value equivalent) and returns the current state of the chosen virtual key code...we then simply use a BITWISE-AND (you do that with a single ampersand) with 0x80... So why 0x80? Well, 0x80 is 10000000 in binary (0x80 is in hexadecimal -- just in case you don't know yet...:) ). This means we check the first bit of the current key state returned by GetKeyState()...if the first bit is 1, then the user is/has pressing/pressed the key that we want to grab...simple huh? (Microsoft Windows provides a handy Calculator together with its operating system...it is there where I converted the 0x80 to 10000000 -- a handy tool...just thought you should know...;) ) IMPORTANT: Note that VK_SHIFT (Shift Key), VK_CONTROL (Ctrl Key), and VK_MENU (Alt Key) can only be used with the GetKeyState() function...they CANNOT be detected with any window message. Now the following paragraphs will have nothing to do with keyboard handling...but I must introduce it to you nonetheless since it is used in the provided source code below (under the NOMAD.SOURCE Section). When you read the source code below, you will see a function named MessageBox()...what this does is pop up a, well, message box (you know, those box that alert you with some info like "Do you want to overwrite the file?", etc.)...the prototype of this function is: MessageBox(hWnd,text,title bar,MB_OK); The first parameter accepts a handle to a window (but you can just put 'NULL'), the next is the message in your message box, then the words to show in the message box's title bar, and lastly, MB_OK will put an "OK" button for the user to close the message box...simple! =). Also, previously, we used GetMessage() to grab our window's messages...we change that now with PeekMessage(). Don't worry, both of 'em work in almost the same way...just read the code...;). Check out the NOMAD.SOURCE Section below to see how this all fits together (don't worry, it's heavily commented...;) ). At last, we're done with handling keyboard through the Win32 API...mouse handling, here we come! (Oh yeah, note that in the NOMAD.SOURCE Section below, I also show how to disable the Alt-F4 keystroke...VERY useful in games...;)) [ ENDING ] And that's that! Not hard eh? If there is anything that's not clear to you, your mind, or to that part of you that lets you comprehend the things here, you may email me at (according to priority): willietang@hehe.com or willietang@yahoo.com Part 9 will be the mouse's turn...:). Briefly, we'll deal with new messages (again). These messages are (duh Willie!) for handling the mouse... so watch out for Part 9. [ REFERENCE ] I want to recommend the following site to you: URL : http://www.Peroxide.dk This is a site where you'll see guys creating a 3D game named Era (the Era engine was originally intended for their remake of the classic game Ultima 1, but due to some issues, they had to let that idea go). This site has some tutorials (from DOS graphics, to Win32, to collision detection!) worth checking out. [ NOMAD.SOURCE ] /* Author : Willie Tang * Compiler : MS Visual C++ 6.0 * Created : September 05, 2002 * Modified : September 06, 2002 * * Notes : The code below works as a Win32 Application. * To run the code, create a new project (File->New). * In the Projects Tab, select "Win32 Application", * give the project a name them click OK. Then select * the "An Empty Project" radio button, click Finish. * * To insert the code to the project. Click on the * Project menu->Add to Project->New. On the Files * Tab, select "C++ Source File", give any name to * the file, then click OK. After that, copy and paste * the code below to the editor. ************************************************************/ #include #include #include //--[ I know, I know, global variables are bad...but hey, //--[ my goal is to let you know how to use these stuffs. //--[ Besides, it's actually OK to use global variables //--[ for small programs...;). HWND hWindow; // handle to window HDC hDC; // window's device context LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) { switch(message) { case WM_CREATE: // window is created MessageBox(NULL, "Hit Esc To Close This Window.", "So You Know", MB_OK); break; case WM_DESTROY: // window is destroyed PostQuitMessage(0); // add "quit" to msg queue break; // We grab system keystrokes here...this will // disable the Alt-F4 key!!! =). case WM_SYSKEYDOWN: case WM_SYSKEYUP: case WM_SYSCHAR: //...do nothing... break; // Note that I shall only be using the WM_KEYDOWN // message. If you want your program to react when // the key is released, use WM_KEYUP instead... // Also, I shall only be detecting function keys F1-F4 case WM_KEYDOWN: switch(wParam) { case VK_F1: // user has pressed F1 key MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed F1!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case VK_F2: MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed F2!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case VK_F3: // user has pressed F3 key MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed F3!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case VK_F4: MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed F4!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; } break; // This is the WM_CHAR message, you grab your // character inputs here. Note that I shall only // be detecting letters 'a' to 'c' (small and big)... case WM_CHAR: // received character message switch(wParam) { case 'a': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed a!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case 'A': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed A!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case 'b': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed b!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case 'B': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed B!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case 'c': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed c!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; case 'C': MessageBox(NULL, // accepts hWnd, can be NULL here "You Pressed C!!!", // text shown in message box "Notice", // text shown in title bar MB_OK); // put an OK button break; } break; default: return(DefWindowProc(hWnd, message, wParam, lParam)); } return(0L); // return a zero } int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd) { WNDCLASS wndclass; MSG msg; BOOL done = FALSE; // Initialize values of wndclass' fields wndclass.style = CS_HREDRAW | CS_VREDRAW; // window's style wndclass.lpfnWndProc = (WNDPROC)WndProc; // pointer to WndProc wndclass.cbClsExtra = 0; // disregard... wndclass.cbWndExtra = 0; // disregard... wndclass.hInstance = hInstance; // copy from WinMain() wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION); // window icon // IDI_ASTERISK // IDI_EXCLAMATION // IDI_HAND // IDI_QUESTION // IDI_WINLOGO wndclass.hCursor = LoadCursor(NULL, IDC_ARROW); // window's mouse cursor // IDC_CROSS // a...'cross'...:) // IDC_NO // slashed circle // IDC_WAIT // hourglass // IDC_HELP // arrow with question mark // IDC_UPARROW // arrow pointing up wndclass.hbrBackground = (HBRUSH)CreateSolidBrush(RGB(255,255,255)); wndclass.lpszMenuName = NULL; // window's menu name wndclass.lpszClassName = "MyWindowName"; // window's name // Register wndclass if(RegisterClass(&wndclass) == 0) return FALSE; // Create our window and get its handle hWindow = CreateWindow ("MyWindowName", // from wndclass.lpszClassName "My Title Bar", // this will show on title bar // WS_OVERLAPPEDWINDOW, // border, title, min-max-close button WS_OVERLAPPED, // border, title // WS_POPUP // no title, no button // WS_VISIBLE // window is visible from start // WS_VSCROLL // window has vertical scroll bar // WS_HSCROLL // window has horizontal scroll bar // WS_MAXIMIZE // window is created in maximized state // WS_MINIMIZE // window is created in minimized state 0, // starting x-coord (upper left) 0, // starting y-coord (upper left) 200, // window's x-size 100, // window's y-size NULL, // parent window's handle NULL, // handle to a menu hInstance, // copy from WinMain()'s parameter NULL); // disregard... // Check if window was created if(hWindow == NULL) return FALSE; // Show our window ShowWindow(hWindow, SW_SHOW); // show created window // SW_HIDE // hide created window // SW_RESTORE // show window in restored state // SW_MAXIMIZE // show window in maximized state // SW_MINIMIZE // show window in minimized state // Keep on grabbing messages while(done == FALSE) { if(PeekMessage(&msg,NULL,0,0,PM_REMOVE)) // Did A Message Arrive? { if(msg.message == WM_QUIT) // if message says WM_QUIT done = TRUE; // quit from while-loop else // else... { TranslateMessage(&msg); // convert to "known" msg DispatchMessage(&msg); // send to msg queue } } else { if(GetKeyState(VK_ESCAPE) & 0x80) // if user pressed Esc key { MessageBox(NULL, // accepts hWnd, can be NULL! "Closing Window", // text to show in message box "Note", // text to show in title bar MB_OK); // put an OK button in message box PostQuitMessage(0); // put a WM_QUIT message to msg queue done = TRUE; // quit from loop } if((GetKeyState(VK_MENU) & 0x80) && // If user pressed Alt key and (GetKeyState(VK_F1) & 0x80)) // the F1 key at the same time... { MessageBox(NULL, // accepts hWnd, can be NULL! "You Pressed Alt-F1!!!", // text to show in message box "Note", // text to show in title bar MB_OK); // put an OK button in message box } } } return 0; } <<------------------------------------------------------>> [ Written By: Willie Tang ] [ Written On: 08.20.2002 ] [ Updated On: 09.06.2002 ]