Handling Keyboard and Mouse Application Buttons in WTL
Handling Keyboard and Mouse Application Buttons in WTL
Contents
- Introduction
- Hardware and Drivers
- Button Identification
- Handling Application Buttons
- Handling Mouse X Buttons
- Handling Mouse Wheel Events
- Copyright and License
- Revision History
Introduction
Most keyboards and mice sold these days go beyond the standard two buttons and 104 keys. Starting with Windows Me and 2000, the OS has built-in support for some of these extra hardware buttons (with newer OSes supporting more buttons, naturally) and will give applications some limited functionality for free. For example, if you press the Stop button, the current application will receive an Escape key press; rolling the mouse wheel will scroll the window with the focus if it is scrollable; and clicking the side mouse buttons navigate IE just like the Back and Forward toolbar buttons.
Applications can handle these buttons if they want to provide their own features. For instance, media player applications can handle the Play, Pause, and Stop buttons, and perform the corresponding actions on the current song. Graphical applications can handle mouse wheel messages to provide zooming functionality.
The sample project is a dialog-based app that shows how to handle the messages generated by the application buttons; it simply prints the details of all the messages it receives.
This article assumes you are familiar with GUI programming with WTL. Check out my WTL series if you haven't used WTL or just need a refresher. Also, if you have troubles compiling, see the README section in Part I for instructions on setting up VC to use WTL.
Hardware and Drivers
Since I have only used Microsoft keyboards and mice, this article discusses only Microsoft hardware. Hardware from other companies should (hopefully) behave the same from the application's perspective, since notifications are done with window messages.
The Microsoft Natural Pro has a row of blue buttons above the function keys, as shown here:
Newer keyboards use the function keys as application buttons as well. For example, the Natural Keyboard 4000 has these additional labels on the F1-F5 keys:
The keys can be toggled between function keys and application buttons with the F Lock key.
Mice have a scroll wheel and two extra side buttons (sometimes one on each side):
The set of buttons that function and generate commands is highly dependent on your OS and driver version. Windows 2000 or later with no drivers will only recognize a subset of buttons (mostly the browser navigation buttons and volume controls). Some buttons will only function if you install the IntelliType or IntelliPoint drivers. For example, the 4000 keyboard's combo buttons and Favorites buttons do not function at all without IntelliType.
Keep this in mind if some of your hardware buttons don't seem to work - update your drivers!
Button Identification
There are #define
s in winuser.h for each of the application buttons. The mouse buttons are called "X buttons", identified in messages as XBUTTON1
and XBUTTON2
. The buttons also have virtual key codes: VK_XBUTTON1
and VK_XBUTTON2
.
The scroll wheel acts as the middle mouse button:
Since the wheel button is not considered an X button, it will not be covered here. The mouse wheel itself does not have an ID or virtual key code; instead, a special message is sent when the wheel is rolled.
The keyboard buttons are identified by constants that start with APPCOMMAND_
, for example APPCOMMAND_BROWSER_BACKWARD
and APPCOMMAND_VOLUME_MUTE
. Note that some keyboard buttons have no IDs at all, such as the 4000 keyboard's Favorites buttons:
The features invoked by these buttons are implemented entirely by the IntelliType software, and applications are not notified when these buttons are pressed.
Some keyboards have a Zoom slider:
This control is also handled entirely by IntelliType. When the user moves the slider, IntelliType sends the active application a mouse wheel message, making it appear that the user rolled the wheel with the Control key pressed.
Handling Application Buttons
When the user presses a mouse or keyboard button that triggers a command, and it's not a button that is implemented entirely by the drivers (such as the Favorites buttons described above), the current application receives a WM_APPCOMMAND
message. WM_APPCOMMAND
sends several pieces of info packed into the message parameters: the window handle where the event happened, the ID of the command (an APPCOMMAND_*
constant), a flag indicating whether the command was triggered by the keyboard or the mouse, and flags indicating which shift keys and mouse buttons were also pressed.
The list of command IDs is quite long, so I won't list them here. See the MSDN documentation on WM_APPCOMMAND
for the full list. The second flag can be FAPPCOMMAND_KEY
if the command was triggered with the keyboard, FAPPCOMMAND_MOUSE
if it was triggered with the mouse, or FAPPCOMMAND_OEM
if some other method was used. You can examine this flag to tell, for example, if an APPCOMMAND_BROWSER_BACKWARD
command was invoked using the Back button on the keyboard, or X button 1 on the mouse.
You can examine the final set of flags to tell what keys or mouse buttons were pressed when the command was generated. The flags are: MK_CONTROL
, MK_SHIFT
, MK_LBUTTON
, MK_MBUTTON
, MK_RBUTTON
, MK_XBUTTON1
, MK_XBUTTON2
. Note that drivers may not always pass these flags to the current application, so if the user presses the Back button while holding the Shift key, the flags sent with the WM_APPCOMMAND
message may not include MK_SHIFT
.
One thing to watch out for is that WM_APPCOMMAND
is different from most other messages, in that the app should return TRUE
(instead of zero) if it handles the message. If you return zero, the drivers may perform the default command, in addition to whatever your app does.
In WTL, the message map macro MSG_WM_APPCOMMAND
can be used to handle WM_APPCOMMAND
. Your handler should have this prototype:
LRESULT OnAppCommand(HWND hwndCtrl, int nCommand, UINT uDevice, UINT uKeys);
If the message is handled, the function should return TRUE
. In WTL versions before 7.5, the MSG_WM_APPCOMMAND
macro always returns 0, so you will need to redefine it to get the correct behavior:
#if _WTL_VER < 0x0750
#undef MSG_WM_APPCOMMAND
#define MSG_WM_APPCOMMAND(func) \
if (uMsg == WM_APPCOMMAND) \
{ \
SetMsgHandled(TRUE); \
lResult = func((HWND)wParam, GET_APPCOMMAND_LPARAM(lParam), \
GET_DEVICE_LPARAM(lParam), GET_KEYSTATE_LPARAM(lParam)); \
if(IsMsgHandled()) \
return TRUE; \
}
#endif
This is how the sample app lists the WM_APPCOMMAND
messages received after pressing a few application buttons:
Handling Mouse X Buttons
Client area messages
When the user clicks an X button, the system sends a mouse message to the window that is under the mouse cursor at the time. When the cursor is in the application's client area, the X buttons generate the messages WM_XBUTTONDOWN
, WM_XBUTTONUP
, and WM_XBUTTONDBLCLK
. (When the cursor is over a control, WM_APPCOMMAND
messages are generated instead.)
The X button mouse messages send two pieces of info in the WPARAM
: a set of flags that indicates which shift keys and other mouse buttons were pressed at the time, and another number that indicates which mouse button generated the message (XBUTTON1
or XBUTTON2
). The LPARAM
contains the mouse cursor coordinates, as with other mouse messages.
In WTL, there is a message map macro for each message. For example, MSG_WM_XBUTTONDOWN
handles WM_XBUTTONDOWN
, with the handler having this prototype:
LRESULT OnXButtonDown(UINT uButton, UINT uKeys, CPoint pt);
As with WM_APPCOMMAND
, handlers for an X button message should return TRUE
if the message is handled. The WTL message map macros all return zero, so you will need to redefine them. For example, a fixed MSG_WM_XBUTTONDOWN
is:
#undef MSG_WM_XBUTTONDOWN
#define MSG_WM_XBUTTONDOWN(func) \
if (uMsg == WM_XBUTTONDOWN) \
{ \
SetMsgHandled(TRUE); \
lResult = func(GET_XBUTTON_WPARAM(wParam), GET_KEYSTATE_WPARAM(wParam), \
CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); \
if(IsMsgHandled()) \
return TRUE; \
}
MSG_WM_XBUTTONUP
and MSG_WM_XBUTTONDBLCLK
need similar changes. This needs to be done for all WTL versions, since the macros are still incorrect in version 7.5.
Non-client area messages
When the user clicks an X button in the non-client area of a window, the system sends a mouse message to that window. The messages are WM_NCXBUTTONDOWN
, WM_NCXBUTTONUP
, and WM_NCXBUTTONDBLCLK
. These messages send two pieces of info in the WPARAM
: the hit-test value indicating which part of the non-client area the cursor is over, and an XBUTTON*
constant indicating which button generated the message. The LPARAM
contains the cursor coordinates.
WTL has a message map macro for each of these three messages, for example MSG_WM_NCXBUTTONDOWN
handles WM_NCXBUTTONDOWN
. Handlers should have this prototype:
LRESULT OnNCXButtonDown(UINT uButton, int nHitTest, CPoint pt);
As with the WM_XBUTTON*
messages, handlers for WM_NCXBUTTON*
should return TRUE
if the message is handled. The WTL macros for WM_NCXBUTTON*
all return zero, so you'll need to redefine them. For example, a fixed MSG_WM_NCXBUTTONDOWN
is:
#undef MSG_WM_NCXBUTTONDOWN
#define MSG_WM_NCXBUTTONDOWN(func) \
if (uMsg == WM_NCXBUTTONDOWN) \
{ \
SetMsgHandled(TRUE); \
lResult = func(GET_XBUTTON_WPARAM(wParam), GET_NCHITTEST_WPARAM(wParam), \
CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); \
if(IsMsgHandled()) \
return TRUE; \
}
MSG_WM_NCXBUTTONUP
and MSG_WM_NCXBUTTONDBLCLK
need similar changes. This needs to be done for all WTL versions, since the macros are still incorrect in version 7.5.
Here is an example of the mouse messages received after clicking the X buttons:
Handling Mouse Wheel Events
There are two types of scroll wheels. The original type clicks into discrete positions as it is rotated, whereas the new type rotates smoothly. The IntelliMouse Explorer 4.0 has a smooth-scrolling wheel that is also a tilt wheel:
The tilt feature is discussed later in this section. The smooth-scrolling and tilt features require IntelliPoint; without it, the wheel will send messages as if it were an older-style clicking wheel.
When the user rolls the wheel, the application receives a WM_MOUSEWHEEL
message. The WPARAM
contains two bits of info: flags indicating which shift keys or mouse buttons were pressed, and a distance value. The LPARAM
holds the coordinates of the cursor.
The distance value for clicking wheels is always a multiple of the number WHEEL_DELTA
(defined as 120). The sign of this value tells you which direction the wheel was rotated: positive means forward (away from the user), negative means backward (toward the user). For example, if the distance is -WHEEL_DELTA
, then the user turned the wheel one click towards himself. If the distance is 3*WHEEL_DELTA
, then he quickly turned the wheel three clicks away. Larger distance values mean the application should do larger scrolling or zooming operations.
Here is an example of the messages received when scrolling a clicking wheel:
With a smooth-scrolling wheel, the distance values will not always be multiples of WHEEL_DELTA
. It is the application's responsibility to keep track of how much distance has been covered. If the wheel doesn't move a full WHEEL_DELTA
distance, the application could perform a smaller scroll or zoom, or it could wait until a full multiple of WHEEL_DELTA
is traversed.
Here is an example of the messages received when using a smooth-scrolling wheel:
If the keyboard has a zoom slider, IntelliType sends a WM_MOUSEWHEEL
message with the MK_CONTROL
flag set. If your app has a zoom feature, you should perform a zoom in response to this message. The last two messages in the screen shot above were generated with a zoom slider - notice that the messages indicate that the Control
key is pressed. The zoom slider on the 4000 keyboard is a simple digital switch, so the distance parameter is always WHEEL_DELTA
. If the distance is positive, the app should zoom in, or if it's negative, the app should zoom out.
Note that some apps, notably IE and Firefox, have the zoom directions reversed. The 4000's zoom slider is marked with + and - signs, indicating that pushing the slider up (which sends a positive distance) will zoom in, and pushing down will zoom out. Personally, I would follow the slider markings. Also, the zoom features in Office and Paint Shop Pro match the slider markings. Hopefully, one day, all programs will come together in harmony and decide on which way to zoom.
The tilt wheel is not supported natively by any shipping version of Windows, so IntelliPoint converts tilt wheel events into horizontal scroll messages, and applications are not directly notified of tilt wheel events. In Vista, there is a new message, WM_MOUSEHWHEEL
, that is sent when the wheel is tilted. WM_MOUSEHWHEEL
has the same parameters as WM_MOUSEWHEEL
, with the exception that the distance parameter indicates left/right movement instead of forward/back. Also, a handler for WM_MOUSEHWHEEL
should return TRUE
if the message is handled, whereas a WM_MOUSEWHEEL
handler should return zero.
In WTL, you can use the MSG_WM_MOUSEWHEEL
message map macro to handle WM_MOUSEWHEEL
. The handler should have this prototype:
LRESULT OnMouseWheel(UINT uKeys, short nDistance, CPoint pt);
WTL does not yet have support for WM_MOUSEHWHEEL
, but creating a message map macro for it is simple:
#define WM_MOUSEHWHEEL 0x020E
#define MSG_WM_MOUSEHWHEEL(func) \
if (uMsg == WM_MOUSEHWHEEL) \
{ \
SetMsgHandled(TRUE); \
lResult = func((UINT)LOWORD(wParam), (short)HIWORD(wParam), \
CPoint(GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam))); \
if(IsMsgHandled()) \
return TRUE; \
}
The WM_MOUSEHWHEEL
handler should have the same prototype as OnMouseWheel()
above. Note that I haven't tried the sample code on Vista, but hopefully it will all work!
Copyright and License
This article is copyrighted material, ©2006 by Michael Dunn. I realize this isn't going to stop people from copying it all around the 'net, but I have to say it anyway. If you are interested in doing a translation of this article, please email me to let me know. I don't foresee denying anyone permission to do a translation, I would just like to be aware of the translation so I can post a link to it here.
The demo code that accompanies this article is released to the public domain. I release it this way so that the code can benefit everyone. (I don't make the article itself public domain because having the article available only on CodeProject helps both my own visibility and the CodeProject site.) If you use the demo code in your own application, an email letting me know would be appreciated (just to satisfy my curiosity about whether folks are benefitting from my code) but is not required. Attribution in your own source code is also appreciated but not required.
Revision History
May 4, 2006: Article first published.