Getting Input from the Keyboard

类别:VC语言 点击:0 评论:0 推荐:
Getting Input from the Keyboard

A Windows application learns of keyboard events the same way it learns about mouse events: through messages. A program receives a message whenever a key is pressed or released. If you want to know when the Page Up or Page Down key is pressed so that your application can react accordingly, you process WM_KEYDOWN messages and check for key codes identifying the Page Up or Page Down key. If you'd rather know when a key is released, you process WM_KEYUP messages instead. For keys that produce printable characters, you can ignore key-down and key-up messages and process WM_CHAR messages that denote characters typed at the keyboard. Relying on WM_CHAR messages instead of WM_KEYUP/DOWN messages simplifies character processing by enabling Windows to factor in events and circumstances surrounding the keystroke, such as whether the Shift key is pressed, whether Caps Lock is on or off, and differences in keyboard layouts.

The Input Focus

Like the mouse, the keyboard is a global hardware resource shared by all applications. Windows decides which window to send mouse messages to by identifying the window under the cursor. Keyboard messages are targeted differently. Windows directs keyboard messages to the window with the "input focus." At any given time, no more than one window has the input focus. Often the window with the input focus is the main window of the active application. However, the input focus might belong to a child of the main window or to a control in a dialog box. Regardless, Windows always sends keyboard messages to the window that owns the focus. If your application's window has no child windows, keyboard processing is relatively straightforward: When your application is active, its main window receives keyboard messages. If the focus shifts to a child window, keyboard messages go to the child window instead and the flow of messages to the main window ceases.

Windows notifies a window that it is about to receive or lose the input focus with WM_SETFOCUS and WM_KILLFOCUS messages, which MFC programs process as shown here:

// In CMainWindow's message map ON_WM_SETFOCUS () ON_WM_KILLFOCUS () void CMainWindow::OnSetFocus (CWnd* pOldWnd) { // CMainWindow now has the input focus. pOldWnd // identifies the window that lost the input focus. // pOldWnd will be NULL if the window that lost the // focus was created by another thread. } void CMainWindow::OnKillFocus (CWnd* pNewWnd) { // CMainWindow is about to lose the input focus. // pNewWnd identifies the window that will receive // the input focus. pNewWnd will be NULL if the // window that's receiving the focus is owned by // another thread. }

An application can shift the input focus to another window with CWnd::SetFocus:

pWnd->SetFocus ();

Or it can use the static CWnd::GetFocus function to find out who currently has the input focus:

CWnd* pFocusWnd = CWnd::GetFocus ();

In the Win32 environment, GetFocus returns NULL if the window that owns the focus was not created by the calling thread. You can't use GetFocus to get a pointer to a window created by another application, but you can use it to identify windows that belong to your application.

Keystroke Messages

Windows reports key presses and releases by sending WM_KEYDOWN and WM_KEYUP messages to the window with the input focus. These messages are commonly referred to as keystroke messages. When a key is pressed, the window with the input focus receives a WM_KEYDOWN message with a virtual key code identifying the key. When the key is released, the window receives a WM_KEYUP message. If other keys are pressed and released while the key is held down, the resultant WM_KEYDOWN and WM_KEYUP messages separate the WM_KEYDOWN and WM_KEYUP messages generated by the key that's held down. Windows reports keyboard events as they happen in the order in which they happen, so by examining the stream of keystroke messages coming into your application, you can tell exactly what was typed and when.

All keys but two generate WM_KEYDOWN and WM_KEYUP messages. The two exceptions are Alt and F10, which are "system" keys that have a special meaning to Windows. When either of these keys is pressed and released, a window receives a WM_SYSKEYDOWN message followed by a WM_SYSKEYUP message. If other keys are pressed while the Alt key is held down, they, too, generate WM_SYSKEYDOWN and WM_SYSKEYUP messages instead of WM_KEYDOWN and WM_KEYUP messages. Pressing F10 puts Windows in a special modal state that treats the next keypress as a menu shortcut. Pressing F10 followed by the F key, for example, pulls down the File menu in most applications.

An application processes keystroke messages by providing message-map entries and message handling functions for the messages it is interested in. WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, and WM_SYSKEYUP messages are processed by a class's OnKeyDown, OnKeyUp, OnSysKeyDown, and OnSysKeyUp member functions, respectively. The corresponding message-map macros are ON_WM_KEYDOWN, ON_WM_KEYUP, ON_WM_SYSKEYDOWN, and ON_WM_SYSKEYUP. When activated, a keystroke handler receives a wealth of information about the keystroke, including a code identifying the key that was pressed or released.

Keystroke message handlers are prototyped as follows:

afx_msg void OnMsgName (UINT nChar, UINT nRepCnt, UINT nFlags)

nChar is the virtual key code of the key that was pressed or released. nRepCnt is the repeat count—the number of keystrokes encoded in the message. nRepCnt is usually equal to 1 for WM_KEYDOWN or WM_SYSKEYDOWN messages and is always 1 for WM_KEYUP or WM_SYSKEYUP messages. If key-down messages arrive so fast that your application can't keep up, Windows combines two or more WM_KEYDOWN or WM_SYSKEYDOWN messages into one and increases the repeat count accordingly. Most programs ignore the repeat count and treat combinatorial key-down messages (messages in which nRepCnt is greater than 1) as a single keystroke to prevent overruns—situations in which a program continues to scroll or otherwise respond to keystroke messages after the user's finger has released the key. In contrast to the PC's keyboard BIOS, which buffers incoming keystrokes and reports each one individually, the Windows method of reporting consecutive presses of the same key to your application provides a built-in hedge against keyboard overruns.

The nFlags parameter contains the key's scan code and zero or more of the bit flags described here:

Bit(s) Meaning Description 0_7 OEM scan code 8-bit OEM scan code 8 Extended key flag 1 if the key is an extended key, 0 if it is not 9_12 Reserved N/A 13 Context code 1 if the Alt key is pressed, 0 if it is not 14 Previous key state 1 if the key was previously pressed, 0 if it was up 15 Transition state 0 if the key is being pressed, 1 if it is being released

The extended key flag allows an application to differentiate between the duplicate keys that appear on most keyboards. On the 101-key and 102-key keyboards used with the majority of IBM-compatible PCs, the extended key flag is set for the Ctrl and Alt keys on the right side of the keyboard; the Home, End, Insert, Delete, Page Up, Page Down, and arrow keys that are clustered between the main part of the keyboard and the numeric keypad; and the keypad's Enter and forward-slash (/) keys. For all other keys, the extended key flag is 0. The OEM scan code is an 8-bit value that identifies the key to the keyboard BIOS. Most Windows applications ignore this field because it is inherently hardware dependent. (If needed, scan codes can be translated into virtual key codes with the ::MapVirtualKey API function.) The transition state, previous key state, and context code are generally disregarded too, but they are occasionally useful. A previous key state value equal to 1 identifies typematic keystrokes—keystrokes generated when a key is pressed and held down for some length of time. Holding down the Shift key for a second or so, for instance, generates the following sequence of messages:


If you want your application to disregard keystrokes generated as a result of typematic action, simply have it ignore WM_KEYDOWN messages with previous key state values equal to 1. The transition state value is 0 for WM_KEYDOWN and WM_SYSKEYDOWN messages and 1 for WM_KEYUP and WM_SYSKEYUP messages. Finally, the context code indicates whether the Alt key was pressed when the message was generated. With certain (usually unimportant) exceptions, the code is 1 for WM_SYSKEYDOWN and WM_SYSKEYUP messages and 0 for WM_KEYDOWN and WM_KEYUP messages.

In general, applications shouldn't process WM_SYSKEYDOWN and WM_SYSKEYUP messages; they should let Windows process them instead. If these messages don't eventually find their way to ::DefWindowProc, system keyboard commands such as Alt-Tab and Alt-Esc will stop working. Windows puts a tremendous amount of power in your hands by routing all mouse and keyboard messages through your application first, even though many of these messages are meaningful first and foremost to the operating system. As with nonclient-area mouse messages, the improper handling of system keystroke messages—in particular, the failure to pass these messages on to the operating system—can result in all sorts of quirky behavior.

Virtual Key Codes

The most important value by far that gets passed to a keystroke message handler is the nChar value identifying the key that was pressed or released. Windows identifies keys with the virtual key codes shown in the table below so that applications won't have to rely on hardcoded values or OEM scan codes that might differ from keyboard to keyboard.

Conspicuously missing from this table are virtual key codes for the letters A through Z and a through z and for the numerals 0 through 9. The virtual key codes for these keys are the same as the corresponding characters' ANSI codes: 0x41 through 0x5A for A through Z, 0x61 through 0x7A for a through z, and 0x30 through 0x39 for 0 through 9.

If you look inside Winuser.h, where the virtual key codes are defined, you'll find a few key codes that aren't listed in the following table, including VK_SELECT, VK_EXECUTE, and VK_F13 through VK_F24. These codes are provided for use on other platforms and can't be generated on conventional IBM keyboards. Nonletter and nonnumeric keys for which Windows does not provide virtual key codes—for example, the semicolon (;) and square bracket ([]) keys—are best avoided when processing key-down and key-up messages because their IDs can vary on international keyboards. This doesn't mean that your program can't process punctuation symbols and other characters for which no VK_ identifiers exist; it simply means that there's a better way to do it than relying on key-up and key-down messages. That "better way" is WM_CHAR messages, which we'll discuss in a moment.

Virtual Key Codes

Virtual Key Code(s) Corresponding Key(s) VK_F1_VK_F12 Function keys F1_F12 VK_NUMPAD0_VK_NUMPAD9 Numeric keypad 0_9 with Num Lock on VK_CANCEL Ctrl-Break VK_RETURN Enter VK_BACK Backspace VK_TAB Tab VK_CLEAR Numeric keypad 5 with Num Lock off VK_SHIFT Shift VK_CONTROL Ctrl VK_MENU Alt VK_PAUSE Pause VK_ESCAPE Esc VK_SPACE Spacebar VK_PRIOR Page Up and PgUp VK_NEXT Page Down and PgDn VK_END End VK_HOME Home VK_LEFT Left arrow VK_UP Up arrow VK_RIGHT Right arrow VK_DOWN Down arrow VK_SNAPSHOT Print Screen VK_INSERT Insert and Ins VK_DELETE Delete and Del VK_MULTIPLY Numeric keypad * VK_ADD Numeric keypad + VK_SUBTRACT Numeric keypad - VK_DECIMAL Numeric keypad . VK_DIVIDE Numeric keypad / VK_CAPITAL Caps Lock VK_NUMLOCK Num Lock VK_SCROLL Scroll Lock VK_LWIN Left Windows key VK_RWIN Right Windows key VK_APPS Menu key ()

Shift States and Toggles

When you write handlers for WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN, or WM_SYSKEYUP messages, you might need to know whether the Shift, Ctrl, or Alt key is held down before deciding what to do. Information about the shift states of the Shift and Ctrl keys is not encoded in keyboard messages as it is in mouse messages, so Windows provides the ::GetKeyState function. Given a virtual key code, ::GetKeyState reports whether the key in question is held down. The expression

::GetKeyState (VK_SHIFT)

returns a negative value if the Shift key is held down or a nonnegative value if it is not. Similarly, the expression

::GetKeyState (VK_CONTROL)

returns a negative value if the Ctrl key is held down. Thus, the bracketed statements in the following code fragment taken from an OnKeyDown handler are executed only when Ctrl-Left (the left arrow key in combination with the Ctrl key) is pressed:

if ((nChar == VK_LEFT) && (::GetKeyState (VK_CONTROL) < 0)) { }

To inquire about the Alt key, you can call ::GetKeyState with a VK_MENU parameter or simply check the context code bit in the nFlags parameter. Usually even that amount of effort isn't necessary because if the Alt key is pressed, your window will receive a WM_SYSKEYDOWN or WM_SYSKEYUP message instead of a WM_KEYDOWN or WM_KEYUP message. In other words, the message ID generally tells you all you need to know about the Alt key. As a bonus, you can use the identifiers VK_LBUTTON, VK_MBUTTON, and VK_RBUTTON in conjunction with ::GetKeyState to determine if any of the mouse buttons is held down.

An application can also use ::GetKeyState to determine whether Num Lock, Caps Lock, and Scroll Lock are on or off. While the high bit of the return code indicates whether a key is currently pressed (yielding a negative number when the high bit is 1), the low bit—bit 0—indicates the state of the toggle. The expression

::GetKeyState (VK_NUMLOCK) & 0x01

evaluates to nonzero if Num Lock is on and evaluates to 0 if it is not. The same technique works for the VK_CAPITAL (Caps Lock) and VK_SCROLL (Scroll Lock) keys. It's important to mask off all but the lowest bit of the return code before testing because the high bit still indicates whether the key itself is up or down.

In all cases, ::GetKeyState reports the state of the key or the mouse button at the time the keyboard message was generated, not at the precise moment that the function is called. This is a feature, not a bug, because it means you don't have to worry about a key being released before your message handler gets around to inquiring about the key state. The ::GetKeyState function should never be called outside a keyboard message handler because the information it returns is valid only after a keyboard message has been retrieved from the message queue. If you really need to know the current state of a key or a mouse button, or if you want to check a key or a mouse button outside a keyboard message handler, use ::GetAsyncKeyState instead.

Character Messages

One problem you'll encounter if you rely exclusively on key-up and key-down messages for keyboard input is shown in the following scenario. Suppose you're writing a text editor that turns messages reporting presses of the character keys into characters on the screen. The A key is pressed, and a WM_KEYDOWN message arrives with a virtual key code equal to 0x41. Before you put an A on the screen, you call ::GetKeyState to determine whether the Shift key is held down. If it is, you output an uppercase "A"; otherwise, you output a lowercase "a." So far, so good. But what if Caps Lock is enabled too? Caps Lock undoes the effect of the Shift key, converting "A" to "a" and "a" to "A." Now you have four different permutations of the letter A to consider:

Virtual Key Code VK_SHIFT Caps Lock Result 0x41 No Off a 0x41 Yes Off A 0x41 No On A 0x41 Yes On a

While you might reasonably expect to overcome this problem by writing code to sense all the possible shift and toggle states, your work is complicated by the fact that the user might also have the Ctrl key held down. And the problem is only compounded when your application is run outside the United States, where keyboard layouts typically differ from the U.S. keyboard layout. A U.S. user presses Shift-0 to enter a right parenthesis symbol. But Shift-0 produces an equal sign on most international keyboards and an apostrophe on Dutch keyboards. Users won't appreciate it much if the characters your program displays don't match the characters they type.

That's why Windows provides the ::TranslateMessage API function. ::TranslateMessage converts keystroke messages involving character keys into WM_CHAR messages.The message loop provided by MFC calls ::TranslateMessage for you, so in an MFC application you don't have to do anything special to translate keystroke messages into WM_CHAR messages. When you use WM_CHAR messages for keyboard input, you needn't worry about virtual key codes and shift states because each WM_CHAR message includes a character code that maps directly to a symbol in the ANSI character set (Windows 98) or Unicode character set (Windows 2000). Assuming that Caps Lock is not turned on, pressing Shift-A produces the following sequence of messages:

Message Virtual Key Code Character Code WM_KEYDOWN VK_SHIFT WM_KEYDOWN 0x41 WM_CHAR 0x41 ("A") WM_KEYUP 0x41 WM_KEYUP VK_SHIFT

Now you can safely ignore key-up and key-down messages because everything you need to know about the keystroke is encoded in the WM_CHAR message. If the Alt key had been held down while Shift-A was pressed, your application would have received a WM_SYSCHAR message instead:


Since Alt-key combinations are generally used for special purposes, most applications ignore WM_SYSCHAR messages and process WM_CHAR messages instead.

Figure 3-6 shows the characters in the ANSI character set. Since ANSI codes are only 8 bits wide, there are only 256 possible characters. Unicode uses 16-bit character codes, expanding the possible character count to 65,536. Fortunately, the first 256 characters in the Unicode character set and the 256 characters in the ANSI character set are identical. Thus, code like this:

case _T (`a'): case _T (`A'):

works fine with either character set.

Figure 3-6. The ANSI character set.

An ON_WM_CHAR entry in a class's message map routes WM_CHAR messages to the member function OnChar, which is prototyped as follows:

afx_msg void OnChar (UINT nChar, UINT nRepCnt, UINT nFlags)

nRepCnt and nFlags have the same meanings that they have in keystroke messages. nChar holds an ANSI or Unicode character code. The following code fragment traps presses of the letter keys, the Enter key, and the Backspace key, all of which produce WM_CHAR messages:

// In CMainWindow's message map ON_WM_CHAR () void CMainWindow::OnChar (UINT nChar, UINT nRepCnt, UINT nFlags) { if (((nChar >= _T (`A')) && (nChar <= _T (`Z'))) || ((nChar >= _T (`a')) && (nChar <= _T (`z')))) { // Display the character } else if (nChar == VK_RETURN) { // Process the Enter key } else if (nChar == VK_BACK) { // Process the Backspace key } }

If it's unclear to you whether a particular key produces a WM_CHAR message, there's an easy way to find out. Simply run the VisualKB application that comes with this book and press the key. If the key produces a WM_CHAR message, the message will appear in VisualKB's window.

Dead-Key Messages

There are two keyboard messages I didn't mention because they are rarely used by application programs. Many international keyboard drivers allow users to enter a character accented with a diacritic by typing a "dead key" representing the diacritic and then typing the character itself. ::TranslateMessage translates WM_KEYUP messages corresponding to dead keys into WM_DEADCHAR messages, and it translates WM_SYSKEYUP messages generated by dead keys into WM_SYSDEADCHAR messages. Windows provides the logic that combines these messages with character messages to produce accented characters, so dead-key messages are usually passed on for default processing. Some applications go the extra mile by intercepting dead-key messages and displaying the corresponding diacritics. The keystroke following the dead key then replaces the diacritic with an accented character. This provides visual feedback to the user and prevents dead keys from having to be typed "blind."

You can process dead-key messages in an MFC application by including an ON_WM_DEADCHAR or ON_WM_SYSDEADCHAR entry in a message map and supplying handling functions named OnDeadChar and OnSysDeadChar. You'll find descriptions of these functions in the MFC documentation.

The Caret

The flashing vertical bar that word processors and other Windows applications use to mark the point where the next character will be inserted is called the caret. The caret serves the same purpose in a Windows application that the blinking underscore cursor does in a character-mode application. MFC's CWnd class provides the seven caret-handling functions shown below. The one essential function missing from this table, ::DestroyCaret, must be called directly from the Windows API because there is no MFC equivalent.

CWnd Caret Handling Functions

Function Description CreateCaret Creates a caret from a bitmap CreateSolidCaret Creates a solid line caret or a block caret CreateGrayCaret Creates a gray line caret or a block caret GetCaretPos Retrieves the current caret position SetCaretPos Sets the caret position ShowCaret Displays the caret HideCaret Hides the caret

The caret, like the mouse cursor, is a shared resource. However, unlike the cursor, which is a global resource shared by everyone, the caret is a per-thread resource that's shared by all windows running on the same thread. To ensure proper handling, applications that use the caret should follow these simple rules:

A window that uses the caret should "create" a caret when it receives the input focus and should "destroy" the caret when it loses the input focus. A caret is created with CreateCaret, CreateSolidCaret, or CreateGrayCaret and is destroyed with ::DestroyCaret.

Once a caret is created, it isn't visible until ShowCaret is called to make it visible. The caret can be hidden again with a call to HideCaret. If calls to HideCaret are nested—that is, if HideCaret is called twice or more in succession—ShowCaret must be called an equal number of times to make the caret visible again.

When you draw in the area of a window that contains the caret outside an OnPaint handler, you should hide the caret to avoid corrupting the display. You can redisplay the caret after drawing is complete. You don't need to hide and redisplay the caret in an OnPaint handlerbecause ::BeginPaint and ::EndPaint do that for you.

A program moves the caret by calling SetCaretPos. Windows doesn't move the caret for you; it's your program's job to process incoming keyboard messages (and perhaps mouse messages) and manipulate the caret accordingly. GetCaretPos can be called to retrieve the caret's current position.

As you know, a window receives a WM_SETFOCUS message when it receives the input focus and a WM_KILLFOCUS message when it loses the input focus. The following WM_SETFOCUS handler creates a caret, positions it, and displays it when a window gains the input focus:

void CMainWindow::OnSetFocus (CWnd* pWnd) { CreateSolidCaret (2, m_cyChar); SetCaretPos (m_ptCaretPos); ShowCaret (); }

And this WM_KILLFOCUS handler saves the caret position and hides and destroys the caret when the input focus is lost:

void CMainWindow::OnKillFocus (CWnd* pWnd) { HideCaret (); m_ptCaretPos = GetCaretPos (); ::DestroyCaret (); }

In these examples, m_cyChar holds the caret height and m_ptCaretPos holds the caret position. The caret position is saved when the focus is lost, and it is restored when the focus is regained. Since only one window can have the input focus at a time and keyboard messages are directed to the window with the input focus, this approach to caret handling ensures that the window that "owns" the keyboard also owns the caret.

The caret-create functions serve two purposes: defining the look of the caret and claiming ownership of the caret. The caret is actually a bitmap, so you can customize its appearance by supplying a bitmap to CWnd::CreateCaret. But more often than not you'll find that the easier-to-use CreateSolidCaret function (it's easier to use because it doesn't require a bitmap) does the job nicely. CreateSolidCaret creates a solid block caret that, depending on how you shape it, can look like a rectangle, a horizontal or vertical line, or something in between. In the OnSetFocus example above, the statement

CreateSolidCaret (2, m_cyChar);

creates a vertical-line caret 2 pixels wide whose height equals the character height of the current font (m_cyChar). This is the traditional way of creating a caret for use with a proportional font, although some programs key the width of the caret to the width of a window border. You can obtain the border width by calling ::GetSystemMetrics with the value SM_CXBORDER. For fixed-pitch fonts, you might prefer to use a block caret whose width and height equal the width and height of one character, as in

CreateSolidCaret (m_cxChar, m_cyChar);

A block caret doesn't make sense for a proportionally spaced font because of the varying character widths. CWnd's CreateGrayCaret function works just as CreateSolidCaret does except that it creates a gray caret rather than a solid black caret. Caret dimensions are expressed in logical units, so if you change the mapping mode before creating a caret, the dimensions you specify will be transformed accordingly.

As mentioned above, it's your job to move the caret. CWnd::SetCaretPos repositions the caret, accepting a CPoint object that contains the x and y client-area coordinates of the new cursor position. Positioning the caret in a string of text is fairly straightforward if you're using a fixed-pitch font because you can calculate a new x offset into the string by multiplying the character position by the character width. If the font is proportionally spaced, you'll have to do a little more work. MFC's CDC::GetTextExtent and CDC::GetTabbedTextExtent functions enable an application to determine the width, in logical units, of a string of characters rendered in a proportional font. (Use GetTabbedTextExtent if the string contains tab characters.) Given a character position n, you can compute the corresponding caret position by calling GetTextExtent or GetTabbedTextExtent to find the cumulative width of the first n characters. If the string "Hello, world" is displayed at the position specified by a CPoint object named point and dc is a device context object, the following statements position the caret between the "w" and "o" in "world":

CSize size = dc.GetTextExtent (_T ("Hello, w"), 8); SetCaretPos (CPoint (point.x +, point.y));

GetTextExtent returns a CSize object whose cx and cy members reflect the string's width and height.

Caret positioning gets slightly more complicated if you're using a proportional font and don't have a character offset to work with, which is exactly the situation you'll find yourself in when you write an OnLButtonDown handler that repositions the caret when the left mouse button is clicked. Suppose your application maintains a variable named m_nCurrentPos that denotes the current character position—the position within a string at which the next typed character will be inserted. It's easy to calculate the new caret position when the left or right arrow key is pressed: You just decrement or increment m_nCurrentPos and call GetTextExtent or GetTabbedTextExtent with the new character position to compute a new offset. But what if the left mouse button is clicked at some arbitrary location in the string? There is no relationship between where the mouse click occurred and m_nCurrentPos, so you must use the horizontal difference between the cursor position and the beginning of the string to work backward to a character position, and then calculate the final caret position. This inevitably involves some iteration since there is neither a Windows API function nor an MFC class member function that accepts a string and a pixel offset and returns the character at that offset. Fortunately, it's not terribly difficult to write that function yourself. You'll see how it's done in the next section.