Working with the Keyboard


There are a many ways to grab keyboard input from Win32. They each have their good and bad points, and to make the right choice you need to know how deep you need to pry into keyboard input data. Before we discuss these various approaches, let's get a few vocabulary words out of the way so that we're talking the same language:

  • Character code: Describes the ASCII or UNICODE character that is the return value of the C function, getchar().

  • Virtual scan code: Macros defined in Winuser.h that describe the components of data sent in the wParam value of WM_CHAR, WM_KEYDOWN, and WM_KEYUP messages.

  • OEM scan code: The scan codes provided by OEMs. They are useless unless you care about coding something specific for a particular keyboard manufacturer.

Those definitions will even resonate more once you've seen some data, so let's pry open the keyboard and do a little snooping.

Mike's Keyboard Snooper

I wrote a small program to break out all of the different values for Windows keyboard messages, and as you'll see shortly, this tool really uncovers some weird things that take place with Windows. Taken with the definitions we just discussed, however, you'll soon see that the different values will make a little more sense. Each line in the tables below contains the values of wParam and lParam for Windows keyboard messages. I typed the following sequence of keys, 1 2 3 a b c, to produce the first table. Look closely at the different values that are produced for the different Windows messages such as WM_KEYDOWN, WM_CHAR, WM_KEYUP, and so on:

 WM_KEYDOWN  Code: 49 '1' Repeat:1 Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 49 '1' Repeat:1 Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 49 '1' Repeat:1 Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 50 '2' Repeat:1 Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 50 '2' Repeat:1 Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 50 '2' Repeat:1 Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 51 '3' Repeat:1 Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 51 '3' Repeat:1 Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 51 '3' Repeat:1 Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A' Repeat:1 Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Re1'd:0 WM_CHAR     Code: 97 'a' Repeat:1 Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 65 'A' Repeat:1 Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 66 'B' Repeat:1 Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 98 'b' Repeat:1 Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 66 'B' Repeat:1 Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 67 'C' Repeat:1 Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 99 'c' Repeat:1 Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 67 'C' Repeat:1 Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

You'll first notice that the message pipe gets the sequence of WM_KEYDOWN, WM_CHAR, and WM_KEYUP for each key pressed and released. The next thing you'll notice is that the code returned by WM_CHAR is different from the other messages when characters are lower case.

This should give you a clue that you can use WM_CHAR for simple character input when all you care about is getting the right character code. What happens if a key is held down? Let's find out. The next table shows the output I received by first pressing and holding an 'a' and then the left Shift key:

 WM_KEYDOWN  Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_CHAR     Code: 97 'a' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_CHAR     Code: 97 'a' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_CHAR     Code: 97 'a' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_CHAR     Code: 97 'a' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_CHAR     Code: 97 'a' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYUP    Code: 65 'A' Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYUP    Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

It seems that I can't count on the repeat value as shown here. It is completely dependent on your equipment manufacturer and keyboard driver software. You may get repeat values and you may not. You need to make sure your code will work either way.

For the next sequence, I held the left Shift key and typed the same original sequence: 1 2 3 a b c:

 WM_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 49 '1'  Repeat:1  Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 33 '!'  Repeat:1  Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 49 '1'  Repeat:1  Oem:  2  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 50 '2'  Repeat:1  Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 64 '@'  Repeat:1  Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 50 '2'  Repeat:1  Oem:  3  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 51 '3'  Repeat:1  Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 35 '#'  Repeat:1  Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 51 '3'  Repeat:1  Oem:  4  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 65 'A'  Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 65 'A'  Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 65 'A'  Repeat:1  Oem: 30  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 66 'B'  Repeat:1  Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 66 'B'  Repeat:1  Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 66 'B'  Repeat:1  Oem: 48  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 67 'C'  Repeat:1  Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code: 67 'C'  Repeat:1  Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 67 'C'  Repeat:1  Oem: 46  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYUP    Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

There's nothing too surprising here; the Shift key will repeat until the next key is pressed. Note that the repeats on the Shift key don't continue. Just as in the first sequence, only the WM_CHAR message gives you your expected character.

You should realize by now that if you want to use keys on the keyboard for hotkeys, you can use the WM_KEYDOWN message, and you won't have to care if the Shift key (or even the Caps Lock key) is pressed. Pressing the Caps Lock key gives you this output:

 WM_KEYDOWN  Code: 20 '_'  Repeat:1  Oem: 58  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 20 '_'  Repeat:1  Oem: 58  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

The messages that come though for WM_CHAR will operate as if the Shift key is pressed down.

Let's try some function keys including, F1, F2, F3, and the shifted versions also:

 WM_KEYDOWN  Code:112 'p'  Repeat:1  Oem: 59  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:112 'p'  Repeat:1  Oem: 59  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code:113 'q'  Repeat:1  Oem: 60  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:113 'q'  Repeat:1  Oem: 60  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code:114 'r'  Repeat:1  Oem: 61  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:114 'r'  Repeat:1  Oem: 61  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WW_KEYDOWN  Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYDOWN  Code:112 'p'  Repeat:1  Oem: 59  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:112 'p'  Repeat:1  Oem: 59  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code:113 'q'  Repeat:1  Oem: 60  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:113 'q'  Repeat:1  Oem: 60  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code:114 'r'  Repeat:1  Oem: 61  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code:114 'r'  Repeat:1  Oem: 61  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYUP    Code: 16 '_'  Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

There's a distinct lack of WM_CHAR messages, isn't there? Also, notice that the code returned by the F1 key is the same as the lower case 'p' character. So, what does 'p' look like?

 WM_KEYDOWN  Code: 80 'P'  Repeat:1  Oem: 25  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_CHAR     Code:112 'p'  Repeat:1  Oem: 25  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 80 'P'  Repeat:1  Oem: 25  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 

Isn't that interesting? The virtual scan code for 'p' as encoded for WM_CHAR is exactly the same as the code for WM_KEYUP and WM_KEYDOWN. This funky design leads to some buggy misinterpretations of these two messages, if you are looking at nothing but the virtual scan code. I've seen some games where you could use the function keys to enter your character name!

Gotcha

You can't use WM_CHAR to grab function key input, or any other keyboard key not associated with a typeable character. It is confusing that the ASCII value for the lower case 'p' character is also the VK_F1. If you were beginning to suspect that you can't use the wParam value from all these messages in the same way, you're right.

If you want to figure out the difference between keys you should use the OEM scan code. There's a Win32 helper function to translate it into something useful:

 // grab bits 16-23 from LPARAM unsigned int oemScan = int(lParam & (Oxff << 16)) 16; UINT vk = MapVirtualKey(oemScan, 1); if (vk == VK_F1) {    //  we've got someone pressing the F1 key! } 

The VK_F1 is a #define in WinUser.h, where you'll find definitions for every other virtual key you'll need: VK_ESCAPE, VK_TAB, VK_SPACE, and so on.

Processing different keyboard inputs seems messy, doesn't it? Hold on, it gets better. The next sequence shows the left Shift key, right Shift key, left Ctrl key, and right Ctrl key:

 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 16 '_' Repeat:1  Oem: 42  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 16 '_' Repeat:1  Oem: 54  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 16 '_' Repeat:1  Oem: 54  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 17 '_' Repeat:1  Oem: 29  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 17 '_' Repeat:1  Oem: 29  Ext'd:0  IsAlt:0  WasDown:0  Rel'd:1 WM_KEYDOWN  Code: 17 '_' Repeat:1  Oem: 29  Ext'd:1  IsAlt:0  WasDown:0  Rel'd:0 WM_KEYUP    Code: 17 '_' Repeat:1  Oem: 29  Ext'd:1  IsAlt:0  WasDown:0  Rel'd:1 

The only way to distinguish the left Shift key from the right Shift key is to look at the OEM scan code. On the other hand, the only way to distinguish the left Ctrl key from the right Ctrl key is to look at the extended key bit to see if it is set for the right Ctrl key. This insane cobbler of aggregate design is the best example of what happens if you have a mandate to create new technology while supporting stuff as old as my high school diploma (or is that my grade school one?)

Best Practice

To get around the problems of processing keyboard inputs that look the same as I've outlined in this section, you'll want to write your own handler for accepting the WM_KEYDOWN and WM_KEYUP messages. If your game is going to have a complicated enough interface to distinguish between left and right Ctrl or Shift keys, and use these keys in combination with others, you've got an interesting road ahead. My best advice is to try to keep things as simple as possible. It's a bad idea to assign different actions to both Ctrl or Shift keys anyway. If your game only needs some hotkeys, and no fancy combinations, WM_KEYDOWN will work fine all by itself.

Here's a summary of how to get the right data out of these keyboard messages:

  • WM_CHAR: Use this message only if your game cares about printable characters: no function keys, Ctrl keys, or Shift keys as a single input.

  • WM_KEYDOWN/WM_KEYUP: Grabs each key as you press it, but makes no distinction between upper and lower case characters. Use this to grab function key input - and compare the OEM scan codes with MapVirtualKey(). You won't get upper and lower case characters without tracking the status of the shift keys yourself.

It's almost like this system was engineered by a Congressional conference committee.

GetAsyncKeyState() and Other Evils

There's a Win32 function that will return the status of any key. It's tempting to use, especially given the morass of weirdness you have to deal with going a more traditional route with Windows keyboard messages. Unfortunately, there's a dark side to these functions, and other functions that poll the state of device hardware outside of the message loop.

Most testing scripts or replay features pump recorded messages into the normal message pump, making sure that actual hardware messages are shunted away. Polling functions like GetAsyncKeyState() can't be fooled or trapped in the same way. They also make debugging and testing more difficult, since timing of keyboard input could be crucial to recreating a weird bug.

There are other polled functions are equally evil, and one of them is the polled device status functions in DirectInput, such as IDirectInputDevice8::GetDeviceState().The only way I'd consider using these functions is if I wrote my own mini-message pump, where polled device status was converted into messages sent into my game logic. That, of course, is a lot more work.

Handling the Alt Key Under Windows

If I use the same program to monitor keyboard messages related to pressing the right and left Alt keys, I get nothing. No output at all. Windows keeps the Alt key for itself, and uses it to send special commands to your application. You should listen to WM_SYSCOMMAND to find out what's going on. Some of these messages are important to handle and I'll be covering them in Chapter 11.

You could use the polling functions to find out if the Alt keys have been pressed, but not only does that go against some recent advice it's not considered "polite" Windows behavior. Microsoft has guidelines that well behaved applications should follow, including games. The Alt key is reserved for commands sent to Windows. Users will not expect your game to launch missiles when all they want to do is switch over to Excel, and try to look busy for the boss.




Game Coding Complete
Game Coding Complete
ISBN: 1932111751
EAN: 2147483647
Year: 2003
Pages: 139

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net