Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Make It Snappy


February 2002/User Interface Programming

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 screen’s working area, the window snaps to that edge.

This is more than a cool gimmick; it’s 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 I’ll 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 can’t 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. I’ve included my usual subclassing library in this month’s 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 cursor’s “anchor point” (see Figure 3) moves relative to the window. This is too disconcerting and annoying to live with and must therefore be fixed.

Snappish Behavior

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 you’re 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().

Miscellany

Although unusual, it is nevertheless conceivable that the user wants the window two pixels from the screen’s 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 you’re Shift-dragging close to the edge and then release the Shift key, the window should snap even if you’re holding the mouse perfectly still. In practice, this is hard to do, since there’s a modal loop involved here, and it’s deep in the innards of Windows. If you want to look into this (which I haven’t; nor, apparently, has Nullsoft) I suggest a keyboard hook as probably the easiest approach.

If you’re feeling inspired and ambitious, there are lots of other things you can do with snapping windows. WinAmp itself is a lot snappier than I’ve 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.

References

Nullsoft: WinAmp, http://www.winamp.com.

Petter Hesselberg. “Window Subclassing,” Windows Developer’s Journal, March 2000.


Petter Hesselberg is a Partner with Accenture’s Oslo office. He’s 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.


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.