This article first appeared in Visual Developer Magazine.
(Note: This is my unedited original and may differ slightly from the published version.)
When Jeff asked me if Visual Basic could create a form that could snap to the edge of its parent, also known as docking, I suspected it would be more than a simple HAX. Although easy to force a form to dock, it’s difficult to monitor a form while it’s moving and cause the drag rectangle to snap to the edges of its parent. Jeff’s article idea sounded right up my alley – subclassing. Subclassing, in Windows terms, is the process of creating a procedure to intercept any messages sent to a window and either modifying those messages or passing them on to the original procedure. VB 5 added the AddressOf keyword, enabling programmers to subclass any VB object that has an hWnd (handle) property. This article will show you how to subclass the parent form of a control and snap that form to its parent’s edges.
I decided to make this project a control rather than show just the docking code. This makes it simple to drop the control onto a form to give it docking capabilities. However, it also adds a level of complexity to the subclassing algorithms. The main problem is that all copies of the control in a single project use all the public code in the control. If you drop the docking control onto two forms, then the controls need some way to access their parent forms knowing nothing but the window handle. The basic process of subclassing in VB is to change the original window handler (WndProc) address to: 1) Point to your own procedure:
origWndProc = SetWindowLong(hwnd, GWL_WNDPROC, AddressOf AppWndProc)
2) Handle incoming messages and call the original WndProc:
Public Function AppWndProc(ByVal hwnd As Long, ByVal Msg As Long, _
ByVal wParam As Long, ByVal lParam As Long) As Long
AppWndProc = CallWindowProc(origWndProc, hwnd, Msg, wParam, lParam)
End Function
and 3) Restore the original WndProc address before destroying the object:
SetWindowLong hwnd, GWL_WNDPROC, origWndProc
The original WndProc will be different for each copy of a control, so you can’t just use a public variable. The Microsoft Knowledge Base (KB) Article number Q179398 details one method for keeping the controls separate. However, this method is a bit dangerous to use when subclassing the parent. It stores the control’s object pointer in the window’s UserData area, a 4 byte user-defined storage area contained in every windows object. Any control could commandeer the UserData for itself. If anything else places information in the UserData area, your application would crash.
I prefer using the registry to hold any needed data. In this case, I’m storing a global locked memory pointer containing a copy of the specific data from the control and parent that I need to accomplish the docking (see Table 1):
Table 1 - Control and parent specific variables in seVarsType OrigWndProc Parent form's original WndProc address lParenthWnd Parent form's hWnd lTophWnd MDIForm parent form hWnd lTrayhWnd System tray hWnd lseHwnd SnapEdge control hWnd lxSnap xSnap property – horizontal distance to snap from lySnap ySnap property – vertical distance to snap from bSnapEnabled Enabled property
hMem = GlobalAlloc(GPTR, LenB(seVars)) SaveSetting "SnapEdge", "hMem", CStr(seVars.lParenthWnd), CStr(hMem) CopyMemory ByVal hMem, seVars, LenB(seVars)
I use the window handle as the key to the data, and thus all I need to retrieve that data is the handle provided by my AppWndProc message handler:
If hwnd <> lLasthWnd Then
hMem = Val(GetSetting("SnapEdge", "hMem", CStr(hwnd)))
CopyMemory seVars, ByVal hMem, LenB(seVars)
lLasthWnd = hwnd
End If
At first I thought that using the registry would be too slow. Fortunately, Microsoft caches the registry and accesses it quickly. Also, I only need to look at the registry when the control using the subclassing procedure changes.
GETTIN' ON THE EDGE
To dock a form, you need to locate the edges of the parent. When you have a form on the desktop, you need to account for the location of the taskbar. Since it can reside on any edge of the screen, and can be moved after your program starts, you must determine the edges at the beginning of a move. When your form is an MDI child, then its parent is the MDI form, plus you don’t need to account for the task bar. The MDI form will return a client area that excludes any aligned controls (such as Toolbar or PictureBox controls), but you must account for the border size:
SystemParametersInfo SPI_GETBORDER, 0, lBorder, 0 lBorder = lBorder * 2
The position and edges of the window are stored in a RECT structure:
Public Type RECT
Left As Long
Top As Long
Right As Long
Bottom As Long
End Type
The GetWindowRect API function retrieves the client area of the parent:
Dim rParent As RECT GetWindowRect seVars.lTophWnd, rParent
When a window is moving, it will receive the WM_MOVING message, with the lParam parameter of AppWndProc pointing to a RECT structure containing the size and position of the drag rectangle. Modifying that RECT structure allows you to dynamically change the size and position of the drag rectangle. First, you must copy it from the lParam pointer to a local copy:
Dim rLatest As RECT CopyMemory rLatest, ByVal lParam, Len(rLatest)
The lxSnap and lySnap elements of the seVars structure hold the minimum snap distance. The control’s xSnap and ySnap properties are in twips, while the seVars elements are converted to pixels. When the drag rectangle is moved to within lxSnap pixels of the left or right edge, the drag rectangle’s Left and Right elements are changed to reflect a docked position. When the drag rectangle is moved to within lySnap pixels of the top or bottom edge, the drag rectangle’s Top and Bottom elements are modified to accomplish the vertical dock.
If rLatest.Left <= lMinX + lxSnap Then
' Snap to the left
bSnapNow = True
rLatest.Left = lMinX
rLatest.Right = rLatest.Left + (rOrig.Right - rOrig.Left)
bLeft = True
ElseIf rLatest.Right >= lMaxX - lxSnap Then
' Snap to the right
End If
If rLatest.Top <= lMinY + lySnap Then
' Snap to the top
ElseIf rLatest.Bottom >= lMaxY - lySnap Then
' Snap to the bottom
End If
GETTIN’ OFF THE EDGELSet rOrig = rLatest
But how do you know when the cursor has moved to a position that should undock? You must first store a copy of the cursor position with every WM_MOVING message. Then you must compare the current cursor position with the saved position for each docked edge. If the cursor is moving toward the edge, then save that position for your next comparison. If it is moving away from the edge, then see if the new position is lxSnap or lySnap pixels away from the saved position. If it is, then undock (see Listing).

When the form is finally dropped, you will receive the WM_MOVE message. You need to reset all the flags and provide a control event to indicate the move and the docked status. I communicate from the form’s subclassing procedure to my control using the control’s MouseMove event, which is VB’s handler for the WM_MOUSEMOVE message (see Listing). Since I never need MouseMove for regular control events, it can become a very simple way to send data back to the original control. The lower two bytes of the lParam parameter of the WM_MOUSEMOVE message are the X As Single parameter of the MouseMove event, and the upper two bytes of lParam are the Y As Single parameter. VB translates from pixels to twips, so you must convert back again to get the original data. An event is fired in the parent form using the RaiseEvent statement that allows you to handle any special docking procedures (see Figure 1):
Private Sub UserControl_MouseMove(Button As Integer, _
Shift As Integer, X As Single, Y As Single)
If hMem Then
X = X \ Screen.TwipsPerPixelX
Y = Y \ Screen.TwipsPerPixelY
RaiseEvent Moved(X, Y)
End If
End Sub
Note that the AppWndProc procedure also watches for the WM_DESTROY message. This allows you to End the application from within the VB development environment without causing a crash. The key to preventing the crash is storing needed data in the registry. When VB ends without resetting the original WndProc, all public variables are cleared, but the message handler is still pointing to your VB code. The AppWndProc will see the WM_DESTROY message (it’s a Constant, so it isn’t cleared with End), but it cannot call the procedure pointed to by the origWndProc variable – origWndProc has been zeroed out! Since the global memory pointed to by hMem was not released with a GlobalRelease API call, it’s still valid, and thus the copy of the registry pointer can still be used to find the correct value of origWndProc.
Although one of the major difficulties you’ll encounter when writing subclassing code is resolved with the above technique, there are still several things you must watch out for. You should be careful if you subclass the same object twice within the same project. If you are using a control, you’ll be okay – it’s a separate project. However, when you have two subclassing procedures, one of them will actually be calling the other one whenever it uses the CallWindowProc API function! You will receive a "Bad DLL Calling Convention" error in Windows 95/98, although NT seems to breeze past it. Adding an On Local Error Resume Next before CallWindowProc takes care of it in Win 95/98. If you do attempt this, be sure to use a separate origWndProc variable for each procedure.
You should also keep your replacement WndProc procedure as simple as possible – try not to change any properties or directly reference objects, as it’s possible to recursively call your AppWndProc and quickly overflow the stack, resulting in a crash. Finally, run your program using Ctrl-F5 to insure a complete compile. If you have Compile On Demand turned on, then your procedures won’t compile until they are needed. A simple error such as an undimensioned variable in your AppWndProc will crash VB.
There are many enhancements you can make to the basic docking control I wrote for this article. For example, you might want to maximize the height when the form is docked on the left or right, dock onto another form rather than the parent edges, or stick a docked form to a non-parent form (by watching the WM_MOVE message). Understanding the basics of subclassing is a key to creating unique interfaces and will allow your program to stand out in a crowded marketplace.