Although Multiple Document Interface (MDI) is less common these days, it's still a powerful choice for advanced applications. To make arranging the MDI child windows easier, MDISnap adds "magnetic edges" to the MDI client area borders and other child windows, so arranging windows gets easier.
The best idea is to play with the sample application. Open some windows and tile them ([H] for horizontal, and [V] for vertical tile), then play around!
Update: I've added the following changes:
- disable magnetic edges when using shift
- magnetic edges disabled when move/size is initiated through the system menu (that's much better than the "jumping windows" I had before)
- Holding down Ctrl, you can swap two windows
- Brushed up the sample application a bit
The first two changes were submitted by
a_zur. Thank you!
To use MDI Snapper, include this in your project:
- Declare a variable
CMDISnapper m_mdiSnap accessible from all (e.g., in
- Adjust the snap wdith by calling
CChildFrame (and other
CMDIChildWnd-derived classes you use), override
WindowProc, and call
m_mdiSnap.OnMessage before passing the message to the base class implementation.
struct SNAPINFO implements the logic for a single window, and holds required state variables. A typical call (e.g., in
WM_MOVING) looks like this:
sninf.Init(rectOld, rectNew, snapWidth);
rectNew = sninf.rout;
rectNew could then be used as corrected position of the window.
CMDISnapper implements exactly this for MDI child windows. It assumes and uses MDI client area coordinates in all places and does the appropriate conversions.
Integration in other projects / environments
SNAPINFO can be used in any Windows project and does the basic calculations.
CMDISnapper uses MFC classes, but can be easily ported to e.g., ATL or Win32 API, as it does not rely on MFC specifics.
Init(RECT const & oldRect, RECT const & newRect, DWORD snapWidth)
Prepare calculation for adjusting a window while it is resized or moved.
Init(RECT const & r, DWORD snapWidth, bool moveOnly)
Prepare calculation for adjusting a single window, independent of changes.
void SnapHLine(long y)
Specify the y coordinate of a horizontal line where one of the window edges could snap to. The line will be taken into consideration only if it's close enough to the top or bottom edge of the window.
void SnapVLine(long y)
RECT & EndSnap()
returns a reference to
rect which contains the final coordinates after
EndSnap was called.
CMDISnapper(DWORD snapWidth = 8)
MDISnapper constructor, at your service. If you already know what you want, you can specify a snap width.
void SetSnapWidth(DWORD snapWidth)
DWORD GetSnapWidth() const
Set / retrieve snap width. It will be used with the next relevant message (currently,
LRESULT OnMessage(CWnd * wnd, UINT msg, WPARAM wp, LPARAM lp)
Call this function in the window proc of the window(s) that should snap. Pass the window, the message sent, and its parameters. Return value can be ignored. Forwards the following messages to specific handlers:
void Sizing(CWnd * wnd, RECT & rnew)
WM_MOVING handler; retrieves the current
rect, iterates through all visible sibling windows, and adjusts the new size/position rectangle specified in
Bugs, Limitations, Notes
- Move via keyboard does not use the "magnetic" feature (but at least it's usable now).
- In the current implementation, edges will snap to other edges even when they are "far away" (open seven windows in the sample app, tile them vertically, close the two in the middle, and play, you will see what I mean). This behavior is by design (sic!). If you want to snap only to "close" edges, you can do the following in
CMDISnapper::Sizing, when you iterate the child windows:
- Store a copy of the original
rect of the sizing window, inflated by a few pixels (
snapWidth comes into mind).
SnapHLine only when the y coordinate falls into this inflated
rect, similar for
- Using the "old" and "new"
SNAPINFO is a bit useless, as the old coordinates are used only to check whether the window is sized or moved. I originally intended to snap only into the direction of the mouse move, but this resulted in much more complex calculations and stupid behavior.
- Pressing/releasing Shift or Control requires a minimal mouse move for the display to be updated. Automatically doing so is quite expensive (it requires a keyboard hook or a timer, and Sizing would have to remember the original, unadjusted position).