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.
Design Considerations
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
|