Introduction
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!
Using it
To use MDI Snapper, include this in your project:
- Declare a variable
CMDISnapper m_mdiSnap
accessible from all (e.g., in CFooApp
or CMainFrame
object)
- Adjust the snap wdith by calling
m_mdiSnap.SetSnapWidth(int)
.
- For
CChildFrame
(and other CMDIChildWnd
-derived classes you use), override WindowProc
, and call m_mdiSnap.OnMessage
before passing the message to the base class implementation.
Implementation Overview
struct SNAPINFO
implements the logic for a single window, and holds required state variables. A typical call (e.g., in WM_SIZING
/ WM_MOVING
) looks like this:
SNAPINFO sninf;
sninf.Init(rectOld, rectNew, snapWidth);
sninf.SnapVLine(0);
sninf.SnapHLine(0);
sninf.SnapVLine(clientArea.right);
sninf.SnapHLine(clientArea.bottom);
sninf.EndSnap();
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.
Documentation
struct SNAPINFO
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)
Guess what.
RECT & EndSnap()
returns a reference to rect
which contains the final coordinates after EndSnap
was called.
class CMDISnapper
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, WM_SIZING
and WM_MOVING
) processed.
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: WM_SIZING
, WM_MOVING
, WM_ENTERSIZEMOVE
, WM_EXITSIZEMOVE
.
void Sizing(CWnd * wnd, RECT & rnew)
Called from WM_SIZING
and WM_MOVING
handler; retrieves the current rect
, iterates through all visible sibling windows, and adjusts the new size/position rectangle specified in rnew
.
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).
- Call
SnapHLine
only when the y coordinate falls into this inflated rect
, similar for SnapVLine
.
- Using the "old" and "new"
rect
in 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).