|Copyright © Microsoft Corporation. This document is an archived reproduction of a version originally published by Microsoft. It may have slight formatting modifications for consistency and to improve readability.|
This column came about
in an unusual way. Months
ago, while adding support for Windows NT® 4.0 to
one of NuMega's products, I stumbled across a new window
message—WM_MOUSEWHEEL. At first, I didn't understand what the message
represented. The Win32® SDK documentation said: "The WM_MOUSEWHEEL
message is sent to the focus window when the mouse wheel is rotated." My
question was, what mouse wheel? The December 1996 MSJ had not yet
arrived from the printer, so I was unable to learn about the message
from the Editor's Note.
Eventually, I put two and two together and figured out that this new window message was put into Windows NT 4.0 to support the Microsoft IntelliMouse™. IntelliMouse looks like a standard Microsoft mouse, but with the rim of a small rubber wheel sticking up between the two buttons. After more investigation I learned that support for the IntelliMouse is built into Microsoft Office 97, Internet Explorer 3.0, and a handful of other apps. With this limited support, I didn't figure it was worth tracking one of these mice down, installing it, and getting used to a new way of doing things. Boy, was I wrong!
Shortly thereafter, an IntelliMouse arrived at my doorstep and I decided to give it a try. I was pleasantly surprised to find that it really made browsing in Internet Explorer much less tedious. As an added bonus, most of the common controls and dialogs from the Windows NT 4.0 COMDLG32.DLL and COMCTL32.DLL work with the IntelliMouse too, regardless of the application you're using. Even better, the mouse wheel can be used as a third mouse button. The software that comes with the IntelliMouse lets you assign default actions to this button. On my system, I set up a click on the mouse wheel to act like a left-button double-click. I'm normally not one to gush, but put all this together and you've got one really slick package! Once you get into the swing of it, the IntelliMouse is one of the few hardware accessories that every serious Windows-based programmer should have.
In the midst of my excitement over this new mouse (do I need a life, or what?), two dark clouds appeared. First, simply installing the IntelliMouse driver and popping into SoftIce for Windows NT would render my keyboard useless. Faced with the prospect of giving up either my IntelliMouse or my beloved SoftIce for Windows NT, there was only one thing I could do: I made a nuisance of myself with the SoftIce team. Not only did the SoftIce people make the two parties peacefully cohabitate, they added explicit support for the mouse wheel. In the latest versions of SoftIce, you can scroll the code, data, and variable windows with the mouse wheel.
My remaining frustration was that I couldn't use my mouse wheel in more applications. I wanted to use it in programs such as CodeWright (my editor), the Visual C++® 4.2 IDE, and the INFOVIEW online help viewer. Rather than pestering software vendors for upgrades such as Visual Studio™ 97, I worked in reverse and retrofitted the mouse wheel to existing programs. Now, I just load my mouse wheel support program from the startup group and forget about whether an application explicitly supports the IntelliMouse.
The trick to making existing programs respond to the mouse wheel message is to convert the messages into something to which the program can respond. The WM_ MOUSEWHEEL message is for scrolling. You may recall that there are already two predefined window messages that relate to scrolling: WM_VSCROLL (vertical scroll) and WM_HSCROLL (horizontal scroll). The WM_MOUSEWHEEL message is usually used to scroll a window's contents up and down, so it's roughly equivalent to the WM_VSCROLL message. By converting WM_MOUSEWHEEL messages to the appropriate WM_VSCROLL message, it should be possible to retrofit any application that responds to WM_VSCROLL. Alternatively, instead of converting messages, you should be able to post an equivalent WM_VSCROLL message when a WM_MOUSEWHEEL message goes by. The WM_MOUSEWHEEL messages will typically be silently dropped on the floor in these programs.
The tough question is, how can I see WM_MOUSEWHEEL messages in other programs in order to translate them to WM_VSCROLL messages? The easiest solution is to set a systemwide WH_GETMESSAGE hook. You can then see mouse wheel messages in other processes and post the equivalent WM_VSCROLL message to the appropriate window. That's the simple version of the story, though. As you'll see from this month's code, there are several twists and turns that weren't obvious when I set out to write it.
There were a few considerations to contend with before I started slashing away at the code. To begin with, the code for a systemwide hook must be in a DLL because Windows will load the DLL into the appropriate address space before calling the hook callback function for the first time. Put another way, whenever Windows is about to call a hook callback, it checks to see if the callback is in a DLL that's not currently loaded. If so, Windows loads the DLL. In fact, using hooks is one way of getting an arbitrary chunk of code to execute in the context of another process.
Since this DLL will be loaded in the process address space of every process, I needed it to have as little effect as possible on the system. Since the WH_GETMESSAGE hook procedure is called every time a process calls GetMessage or PeekMessage, I made the hook procedure bail out very quickly if it wasn't going to be translating WM_MOUSEWHEEL messages. Second, since nearly every process will load the hook DLL, I made it consume as little memory as possible. More on this later.
Beyond just making my hooking DLL and procedure small and fast, I also didn't want my code to adversely affect existing programs. For that reason, I chose to make the hook procedure perform mouse wheel message translation only in programs that the user explicitly asks it to support. If I didn't do this, the hook DLL could end up posting WM_VSCROLL messages to applications that already respond to the WM_MOUSEWHEEL message. Likewise, I didn't want to blindly post WM_VSCROLL messages to applications that don't respond to that message or that don't respond properly. The tradeoff of the approach I took is that users have to specify each executable file for which they want mouse wheel support.
At this point, since I was already committed to keeping track of which processes would be affected, it wasn't much more work to customize the behavior for each process. For example, in some applications you might want the mouse wheel to scroll in page increments rather than line increments. Or instead of scrolling a single line, you might want the application to scroll several lines at once. I chose to keep these per-program preferences in a registry value that has the same name as the affected application. If the application isn't affected, its name won't appear as a registry value. Otherwise, the corresponding registry value stores a DWORD that encodes whether line or page scrolling should be used for that program. In addition, the value also encodes how many lines or pages should be scrolled. When the hook DLL loads in each process, it retrieves the name of the process, checks the registry for that process name, and stores away whether the application should be mouse wheel-enabled and, if so, how it should work.
That's pretty much it for the design of the hook DLL. However, the DLL by itself is useless without a sponsor program that installs the systemwide hook in the first place. In addition, the sponsor program will be the first to bring the hook DLL into memory, thereby allowing the DLL to do some one-time initialization. Since I wanted my program to be easy to use yet still keep the DLL small, it also made sense to make the sponsor program contain the UI for adding new programs to the list of supported programs. (The UI in the sponsor program collects the information about the program to be supported and adds it to the appropriate spot in the registry.)
The Mouse Wheel Code
With these design issues resolved, let's look at the code that implements them. It's called MouseWheel.DLL, and the code is shown in Figure 1. Starting at the top, note that I've split the global data into two sections in the executable file. The first section (.shared) is a shared section, meaning that the physical RAM for this data is shared among all processes that use the DLL. I put information into this section that shouldn't change between processes: the HHOOK I get back from calling SetWindowsHookEx and the WM_MOUSEWHEEL message number. I put variables that each process needs its own copy of in the traditional data section: whether WM_VSCROLL message should be emitted, page scrolling versus line scrolling, and so on.
The first function in MouseWheel.CPP is GetMsgProc, which is the WH_GETMESSAGE hook callback function. This function first attempts to be a good citizen by calling CallNextHookEx, which chains on to any other installed hooks. Next, GetMsgProc does its best to bail out quickly if possible. If the variable g_okToAct is false in the current process, the function exits. Then, GetMsgProc checks to see if the retrieved window message is a mouse wheel message. If not, the function quickly exits.
If the function hasn't exited after these two tests, it knows that the message is a mouse wheel scrolling message and that WM_VSCROLL messages will need to be posted. Since the mouse wheel can be scrolled forward or back, GetMsgProc needs to decide what to use as the WPARAM for the WM_VSCROLL message. If line scrolling is in ef-fect, WPARAM becomes SB_LINEUP or SB_LINEDOWN. For page scrolling, the WPARAM becomes SB_PAGEUP or SB_PAGEDOWN. Finally, GetMsgProc enters a for loop, posting the specified number of WM_VSCROLL messages to the window that will be receiving the mouse wheel messages.
Skipping the GetMouseWheelMsg function for the moment, look at the DllMain function in MouseWheel.CPP. The actions of the DLL_PROCESS_ATTACH code in DllMain depend on which process context it's running in. If run from the sponsor program, it needs to install the systemwide WH_GETMESSAGE hook and save the associated HHOOK for use by the GetMsgProc callback. In addition, the DLL_PROCESS_ATTACH code calls the GetMouseWheelMsg function to figure out and save away the mouse wheel message number.
If you're wondering how DllMain knows which process it's running in, it uses the semi-sleazy hack of looking at the lpReserved parameter. If it's nonzero, the DLL was brought into memory via an implicit link to the DLL from another executable module. If lpReserved is zero, the DLL was loaded after the process started, most likely via LoadLibrary. In the case of MouseWheel.DLL, I assume that the DLL was loaded by the operating system as part of calling the WH_GETMESSAGE hook if lpReserved is zero. If lpReserved is nonzero, I assume that the DLL was loaded by the sponsor program.
If DllMain isn't running in the sponsoring process's context, DllMain's job is to determine if and how mouse wheel messages should be translated for the process into which the DLL was loaded. The first subtask here is to find the name of the current process. A call to GetModuleFileName with an HMODULE parameter of zero quickly gives the required path to the EXE. Next, the names of all processes that MouseWheel.DLL will affect are stored as values under the registry key
DllMain queries the appropriate value and, if found, sets
the global variables that describe how mouse wheel messages should be
The DLL_PROCESS_DETACH code in DllMain is much simpler. Its only job is to remove the systemwide hook by calling UnhookWindowsEx. It only does this if the cur- rent process set the hook in the first place (that is, the sponsor process).
Now let's check out the GetMouseWheelMsg function. Isn't the mouse wheel message defined to be WM_MOUSEWHEEL in WINNT.H with a value of 0 X 20A? Yes and no. It turns out that Windows NT 4.0 was the first version of the Win32 system to directly support the WM_MOUSEWHEEL message; Windows 95 has no explicit support for this message. In its place, the IntelliMouse software registers a window message by calling RegisterWindowMessage. Programs that want to support the IntelliMouse under Windows 95 need to figure out what the registered message number is and use that rather than the WM_MOUSEWHEEL #define. The GetMouseWheelMsg function is my attempt to isolate the rest of the MouseWheel.CPP code from this oddity.
How does GetMouseWheelMsg know the name of the registered message that it should look for? Believe it or not, there's an IntelliMouse SDK on the Microsoft Web site (http://www.microsoft.com/hardware/mouse/intellimouse/sdk/). You'll want to download the file ZMOUSE.H, which contains a whole bunch of #defines and comments about programming for the IntelliMouse. I didn't use it in my MouseWheel.CPP code because I didn't want to distribute Microsoft header files. Instead, I plucked out the relevant text string (MSWHEEL_ROLLMSG) and included it in the GetMouseWheelMsg function.
As a final note on the MouseWheel.CPP code, I was able to cut its memory footprint down quite a bit by not using any C++ runtime library functions. This in turn let me write my own _DllMainCRTStartup function, which is just a wrapper around a call to DllMain. Put another way, I was able to strip out all of the startup code and runtime library overhead from a traditional DLL. I also merged several of the sections using the linker /MERGE switch. The resulting MouseWheel.DLL is only 4KB in size and uses just three pages of RAM.
The sponsor program is written in MFC (see Figure 2). It's mostly boilerplate MFC code, so I won't dwell on it too much. The implicit link between the sponsor program and MouseWheel.DLL occurs in CMWApp:: InitInstance. The majority of the nonboilerplate code is found in CMWDlg:: OnInitDialog,CMWDlg::OnAddProgram, CMWDlg::Refresh Program Listbox, and CMWDlg::OnDelete Program. These methods implement the UI for adding, deleting, and displaying the list of programs that MouseWheel.DLL supports.
initially implemented the sponsor program (MW.EXE) as a simple dialog.
However, the need for GUI code for letting the user add additional
programs to the supported list led me to write my first nontrivial MFC
program. While I didn't have much trouble getting the UI functionality
into place, I spent way too much time battling MFC to keep the program
off the taskbar, where it sucks up space and is obtrusive. All I wanted
was a simple, small icon in the status area near the clock. After many
hours, I got fairly close, but it's still not perfect.
|Figure 3 Mouse Wheel main dialog|
the sponsor program resides quietly as an icon in the taskbar status
area. Right-clicking the icon brings up a system menu that lets you
close down the program and remove the systemwide hook. More important,
double-clicking on the status icon brings up the main dialog (see Figure 3).
The Add button brings up another dialog where you can enter or browse for the name of an executable (see Figure 4). In this same dialog, you specify whether the new program should have line scrolling or page scrolling, and how many lines or pages should be scrolled. You won't see an immediate effect if you add the name of an executable that's already running. Why? It's likely that the specified executable has already loaded MouseWheel.DLL (via the systemwide hook), so its version of the global variables in MouseWheel.DLL are in the do-nothing state. You'll need to force MouseWheel.DLL to reload by shutting down MW.EXE and then restarting it.
|Figure 4 Select dialog|
in the main dialog, the Delete button removes the currently selected
program from MouseWheel's list of registered programs. As with the add
button, simply changing the contents of the registry
won't have any effect on currently running programs. You'll want to shut
down and restart MW.EXE. |
The OK button has a slightly weird behavior at first glance. Normally, an OK button in a dialog-based program would dismiss the dialog, thereby causing the program to end. That's not the desired behavior here. It's important to keep the support program around. Therefore, I made the OK button merely hide the dialog window. If you want to actually terminate the program, you can click on the close button at the upper-right corner when the main dialog is visible. Alternatively, you can right-click on the taskbar status area icon and select Close.
Some Caveats and Final Words
you find that MouseWheel isn't working for you, there are a couple of
things to check. First, make sure that the application path for the
program that you want to support is correctly specified. Remember,
MouseWheel only affects programs that you explicitly tell it to support.