The popular music-playing program WinAmp has a nifty feature unrelated to music: Whenever you move the WinAmp window within a few pixels of one edge of the screens working area, the window snaps to that edge.
This is more than a cool gimmick; its a genuinely useful feature on a par with the snap to grid option found in PowerPoint, the Visual Studio Dialog Editor, and practically every other drawing or layout program on the planet. This month Ill show you how to teach this trick to your own programs.
Work Area Lock-in
Let me start with a simple warm-up exercise: A window that cant be moved off the working area of the screen. Figure 1 shows the demo dialog I use for this; demo.rc (Listing 1) contains the resource script and the resource identifiers are in resource.h (Listing 2). The main program and the dialog function boxDlgProc() are in demo.c (Listing 3).
boxDlgProc() is pretty simple as dialog functions go; it handles WM_COMMAND so that you can close the dialog, and if the check box is checked, it handles WM_MOVING. Whereas WM_MOVE is sent after a window has moved, WM_MOVING is sent when a window is about to be moved, which gives you a chance to influence the process. The lParam of WM_MOVING is a pointer to a RECT with the proposed new window position, in screen coordinates. To adjust the window position, all you have to do is change this rectangle and return TRUE in response to the message.
WM_MOVING requires the window function to return TRUE. The window function for dialogs is DefDlgProc(); DefDlgProc() in turn calls boxDlgProc(), which is a dialog function, not a window function. To make DefDlgProc() return TRUE, boxDlgProc() first sets the window word identified by DWLP_MSGRESULT to TRUE, then returns TRUE. When a dialog function returns TRUE, this indicates to DefDlgProc() that the message was handled and that nothing more need be done. This is a little more convoluted than a straight window function; the SetDlgMsgResult() macro handles the sordid details.
boxDlgProc() uses SystemParametersInfo() with the SPI_GETWORKAREA parameter to get hold of the working area; i.e., the screen exclusive of task bar and app bars; then compares this to the window rectangle passed with WM_MOVING. If the window would extend outside the work area, boxDlgProc() adjusts the rectangle appropriately.
Note, by the way, that DefDlgProc() handles WM_CLOSE by sending a WM_COMMAND/IDCANCEL message to the dialog, so the WM_COMMAND handler is invoked when you close the window as well as when you hit the Escape key.
Snap to Edge of Work Area
My warm-up exercise has some problems that are addressed by the main event in Figure 2. (This dialog is identical to Figure 1 except for captions and labels; both dialogs share the same resource file and resource identifier header file.)
Problem #1 Since the WM_MOVING code is embedded in the dialog function, it is difficult to reuse. If you copy the code to every dialog function you write, it will be difficult to maintain. As usual, my solution is subclassing, because this provides the loosest possible coupling between the snappy behavior and the functional purpose of the dialog. Ive included my usual subclassing library in this months code archive; the files wdjsub.h and wdjsub.c provide all the necessary functionality.
Problem #2 Living in a box is unnecessarily limiting; sometimes you really do want to move a window partially off-screen.
Problem #3 When you tangle with an edge, the cursors anchor point (see Figure 3) moves relative to the window. This is too disconcerting and annoying to live with and must therefore be fixed.
As mentioned, demo.rc (Listing 1) contains the resource script for both dialogs, and resource.h (Listing 2) contains the resource identifiers for both dialogs. demo.c (Listing 3) also contains snapDlgProc(), the dialog function for the second dialog. snapDlgProc() turns the snappy subclassing on and off whenever the user checks or unchecks the check box. The interface to the snappy subclassing is declared in wnd_snap.h (Listing 4); the subclassing itself is defined in wnd_snap.c (Listing 5).
The snapWndProc subclassing handles two messages. On WM_ENTERSIZEMOVE, it figures out the cx and cy offsets as depicted in Figure 3. These are simply stored in static variables quite safe because users never drag multiple windows. On WM_MOVING, the subclassing first figures out the new position based on cx, cy, and the current mouse position. Then it does something similar to what boxDlgProc() did, except that it moves the window to the edge if it is close to it rather than if it is outside the work area.
But what do I mean by close? Does close mean the same to all users? Should it?
I could weasel out of this question by making the closeness configurable, which is what Nullsoft did with WinAmp. But Windows is already loaded to the gills with configurable system parameters and window metrics; what does the user need with more? I decided to use the caption height as a reasonable yardstick for closeness and be done with it this simplifies my code, my API, and the user interface of programmers using my library. It also simplifies life for the 99.67 percent of users for whom 18 pixels is just fine. (18 pixels is the caption height on my system; if youre sight-impaired and have 30-pixel-high captions you probably prefer the resultant wider snap zone.)
It turns out, by the way, that the cx and cy offsets depicted in Figure 3 are essential when making sudden jumps, as in snapping to an edge. Without it, you must make a giant 18-pixel leap in one single WM_MOUSEMOVE to get unstuck from the edge. This is pretty hopeless even if you know what it takes; to try it for yourself, remove the first OffsetRect() call in onMoving().
Although unusual, it is nevertheless conceivable that the user wants the window two pixels from the screens edge. The usual way of overriding grid magnetism is to use a modifier key, either Shift or Control. WinAmp uses the Shift key, and so do I.
Ideally, the snap feature should react dynamically to the Shift key during dragging if youre Shift-dragging close to the edge and then release the Shift key, the window should snap even if youre holding the mouse perfectly still. In practice, this is hard to do, since theres a modal loop involved here, and its deep in the innards of Windows. If you want to look into this (which I havent; nor, apparently, has Nullsoft) I suggest a keyboard hook as probably the easiest approach.
If youre feeling inspired and ambitious, there are lots of other things you can do with snapping windows. WinAmp itself is a lot snappier than Ive let on so far; you can configure its constituent windows any which way you want, and the snap feature makes easy what would otherwise require extreme dexterity with the mouse.
Nullsoft: WinAmp, http://www.winamp.com.
Petter Hesselberg. Window Subclassing, Windows Developers Journal, March 2000.
Petter Hesselberg is a Partner with Accentures Oslo office. Hes been programming Windows for the past thirteen years and is the author of the book Programming Industrial Strength Windows. He can be reached through http://pethesse.home.online.no.