The source code presented will culminate in a Win32 DLL called DockWnd.dll whose functions can be used by any Win32 program to easily support docking tool windows. There are many different ways to create a docking window. I imagine most of the code on the Internet uses well-designed C++ classes to hide the implementation. However, I still prefer to code in C, so the design of this library will be a non-object oriented approach and therefore easily useable by an application written in any language.
This code/article is based upon some original free code provided by James Brown. His website, contains an earlier, different version of this code (as well as numerous other free Win32 tutorials/examples).
Contents
DOCKINFO structure
How DockingCreateFrame creates a floating tool window
Prevent tool window deactivation
Show all tool windows as active
Sync the activation of all tool windows
Sync the enabled state of all tool windows
Docking a tool window
Floating versus docked size
Moving a window with a drag-rectangle
Drawing a drag-rectangle
Redrawing the docked windows
Enumerating tool windows
Various other features of the library
An application
Creating a tool window
Handling WM_SIZE message in the owner
Handling WM_NCACTIVATE message in the owner
Handling WM_ENABLE message in the owner
Handling messages sent to standard controls
Multiple child windows inside a tool window
Closing a tool window
Saving/Restoring a tool window size/position
Miscellaneous remarks
DOCKINFO structure
Because our docking library needs to maintain some information about each tool window it manages, we need some structure to store the information. We'll define a DOCKINFO structure (in dockwnd.h), and allocate a DOCKINFO for each tool window created. The application will call the DockingAlloc function in our docking library to allocate a DOCKINFO (initialized to default values), and then pass it to the DockingCreateFrame function which creates the actual tool window associated with that DOCKINFO. This struct will hold information such as the handle (HWND) to the tool window, the handle to the tool window's owner window, whether the tool window is currently docked to its owner or floating, and other information for our private use in managing docking tool windows. We store the handle to the tool window in the DOCKINFO's hwnd field. We store the handle to the owner window in the DOCKINFO's container field. And the value of the DOCKINFO's uDockedState field tells whether the tool window is floating or docked. This field is OR'ed with DWS_FLOATING (and therefore a negative value) when a tool window is floating. We'll discuss the other DOCKINFO fields later.
Note: It is the application's responsibility to create and manage the owner window. Our library deals only with creating/managing the tool windows.
We register our own window class (with the class name of "DockWnd32") for our tool windows. The window procedure (dockWndProc) for this class is inside of our library. We use the GWL_USERDATA field of the tool window to store a pointer to that tool window's DOCKINFO struct. In this way, we can easily and quickly fetch the appropriate DOCKINFO given only a handle (HWND) to a particular tool window.
We don't want to limit the application to only one owner window, and its set of docked windows. For example, perhaps an application will have two owner windows, each with its own set of docked windows. Nor, do we want to limit the application to a particular number of tool windows. So, we may be asked to create DOCKINFOs for numerous sets of tool windows and owners. By storing a pointer to a tool window's DOCKINFO in the tool window itself, and storing handles to the tool window and its owner window inside the DOCKINFO, we can easily get all of the information we need for our library to do what it needs to do, with minimal work on the part of the application.
There are times when our library needs to be able to enumerate all tool windows for a particular owner window. We'll get to the particulars of that later. In some of our example code below, we'll just refer to a placeholder function called DockingNextToolWindow which you should assume will fetch the next tool window (actually, that tool window's DOCKINFO) for a particular owner window. In the actual source code for the library, this is replaced by more complex code that we'll examine later.
How DockingCreateFrame creates a floating tool window
A floating tool window is just a standard window with the WS_POPUP style. When a popup window is created with an owner window, the popup is positioned so that it always stays on top of that owner window. This is how we can create and display a floating tool window:
// Create a floating (popup) tool window
HWND hwnd = CreateWindowEx(
WS_EX_TOOLWINDOW,
"ToolWindowClass", "ToolWindow",
WS_POPUP | WS_SYSMENU | WS_THICKFRAME | WS_CAPTION | WS_VISIBLE,
200, 200, 400, 64,
hwndOwner, NULL, GetModuleHandle(0), NULL
);
Note: In the above example, it is assumed that hwndOwner is a handle to some other window the application created to be the owner window for our tool window. The application must create this window, and then pass its handle to DockingCreateFrame. In other words, the application must create the owner window before any tool window can be created for it.
The WS_EX_TOOLWINDOW extra style doesn't do anything special, other than to make a window with a smaller titlebar. It doesn't make the window magically float - this is achieved automatically by specifying WS_POPUP style and an owner window. Here's what the above CreateWindowEx may display:
A tool window floating above its owner window ("Main window").
Prevent window deactivation
The image above shows the owner window ("Main window") with an inactive titlebar. This is entirely normal, because only one window at a time can have the input focus, and the operating system normally shows only that one window with an active titlebar. So, when we create our tool window, the operating system shows the tool window as active, and shows the owner window as deactivated.
But, it is normal practice for tool windows and their owner window to appear active at the same time. It looks more natural this way. So we need to devise a strategy to keep our tool window and owner window both appear active, even if only one technically has the input focus.
The solution involves the WM_NCACTIVATE message. The operating system sends this message when a window's non-client area (the titlebar and border) needs to be activated or deactivated. As with all window messages, WM_NCACTIVATE is sent with two parameters - wParam and lParam. When a window receives WM_NCACTIVATE with wParam=TRUE, this indicates that the titlebar and border should be shown as active. When wParam=FALSE, this indicates that the titlebar and border should be shown as inactive.
Note: MSDN states that WM_NCACTIVATE's lParam will always be 0. However, I have observed that lParam indicates the window handle of the window being deactivated. This appears to be true under Win95, 98 and NT, 2000, XP. Our solution relies upon this undocumented feature.
So, when we create our tool window, our owner window receives a WM_NCACTIVATE with wParam=FALSE and our tool window receives a WM_NCACTIVATE with wParam=TRUE.
When this message is passed to DefWindowProc(), the operating system does two things. First, the titlebar is drawn as either active or inactive, depending upon whether wParam is TRUE or FALSE respectively. Secondly, the operating system sets an internal flag for the window which remembers if the window was painted as active or inactive. This enables DefWindowProc() to process subsequent WM_NCPAINT messages to paint the titlebar with the proper activation. It is advisable to always pass WM_NCACTIVATE to DefWindowProc() so that this internal flag is set, even if you also do your own processing of WM_NCACTIVATE.
This WM_NCACTIVATE message provides us with a way to make all our tool windows, and owner window, look active, even if only one window technically has the focus. To do this, whenever our tool windows or owner window receive a WM_NCACTIVATE, we will always substitute TRUE for wParam when we pass the WM_NCACTIVATE to DefWindowProc(). The result is that the operating system always renders the titlebars of our tool windows and owner window as active.
Here is some code we could add to the window procedure of all our tool windows, and owner window, to show them all as simultaneously active:
case WM_NCACTIVATE:
return DefWindowProc(hwnd, msg, TRUE, lParam);
Both the tool window and its owner window are shown active.
Incidentally, MDI child windows also use this same technique to keep their titlebars active. The only difference is that MDI windows have the WS_CHILD style, instead of WS_POPUP.
Show all tool windows as active
The above method seems to do what we want, but there is a problem. Our owner window and tool windows will always appear active, even if our application is not in the foreground. For example, if the end user switches to some other application's window, our tool window and owner window still will look active, which can be a bit disconcerting to the end user.
Also, whenever we display a message box or a normal dialog box, the owner window and tool windows will still appear active, when in this scenario we ideally want to make them look inactive.
This calls for a more careful study of window activation messages. Here is a description of the series of window activation messages sent when one window becomes active, and another inactive:
WM_MOUSEACTIVATE is sent to the window about to become active, to ask it whether or not the activation request should be allowed. What your window procedure returns (i.e., MA_ACTIVATE or MA_NOACTIVATE) affects the subsequent activation messages.
WM_ACTIVATEAPP is sent if a window belonging to a different application is about to become active (or inactive). This message is sent both to the window that is currently active (to tell it that it is about to become inactive) as well as the window that is about to become active. The return value should always be zero, and never affects subsequent messages' behaviour.
As described above, WM_NCACTIVATE is sent when a window's non-client area needs to be shown activated or deactivated.
WM_ACTIVATE is sent last of all to the window becoming active. When this message is passed to DefWindowProc(), the operating system sets the input focus to that window.
With all of these activation messages, only two windows are actually involved - the window being deactivated, and the window being activated. So, even if we have many floating tool windows, not all of them will receive these messages. Only the one window being activated, and the one window being deactivated, receive the messages. But to make things look and feel right, we want the displayed state (i.e., whether a tool window's titlebar is shown active or inactive) of each tool window to be the same as all other tool windows. So, even though not all of our windows will receive the above messages, we still need to have all windows synced to the same state.
This same discussion applies whenever we need to disable or enable our owner window. If the owner window is to be disabled or enabled, we want to sync all the tool windows to that same state. (But, a different set of messages are sent for a window being disabled or enabled.)
So, our docking library has a bit of work to do in order to make things look and feel right:
When our application is activated / deactivated, we need to sync the active / inactive display of all tool windows with each other.
This also applies to activation within our own application. For example, if the user activates a window we create that isn't one of our tool windows, then we want to show all tool windows deactivated. And if the user switches back to a tool window from that window, we want all tool windows shown active.
When the owner window is disabled due to a modal dialog or message box being displayed, then we must disable all tool windows (and any modeless dialogs) to prevent the user from interacting with them while the modal dialog / message box is on screen.
A first try at a solution
Our first stab at a solution will be to concentrate on the WM_ACTIVATE message. This message is received whenever a window is activated or deactivated. The direction we will take will be to decide if the window receiving this message is active or inactive, and synchronise all other windows to the same state by manually sending them a "spoof" WM_NCACTIVATE message. This spoof message will force the other windows to update their titlebars to the same state as the window receiving the WM_ACTIVATE.
Here's a function that we could add to our docking library. Whenever one of our tool windows, or owner window, receives a WM_ACTIVATE message, it will call this function to sync the state of all tool windows:
Collapse/*********************** DockingActivate() **********************
* Sends WM_NCACTIVATE to all the owner's tool windows. A
* tool or owner window calls this in response to receiving
* a WM_ACTIVATE message.
*
* container = Handle to owner window.
* hwnd = Handle to window which received WM_ACTIVATE (can
* be the owner, or one of its tool windows).
* wParam = WPARAM of the WM_ACTIVATE message.
* lParam = LPARAM of the WM_ACTIVATE message.
*/
LRESULT WINAPI DockingActivate(HWND container,
HWND hwnd, WPARAM wParam, LPARAM lParam)
{
DOCKINFO * dwp;
BOOL fKeepActive;
fKeepActive = (wParam != WA_INACTIVE);
// Get the DOCKINFO of the next tool window for this owner window. when 0
// is returned, there are no more tool windows for this owner.
while ((dwp = DockingNextToolWindow(container)))
{
// Sync this tool window to the same state as the window that called
// DockingActivate.
SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, 0);
}
// Allow the window that called DockingActivate to handle its WM_NCACTIVATE
// as normally it would.
return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}
It works, after a fashion. All tool windows activate and deactivate correctly, and all at the same time. This solution is not the best though.
The problem is that every tool window's titlebar flashes whenever the active window changes. This is because of the way the operating system sends the WM_ACTIVATE message. This message is first sent to the window that is being deactivated. If that happens to be a tool window or the owner window, it will call DockingActivate to deactivate all the tool windows. WM_ACTIVATE is then sent to the active window. If that window also happens to be a tool window or owner window, it will call DockingActivate to (correctly) activate all the tool windows. It is the fact that DockingActivate is quickly called twice (once to deactivate the tool windows, and then to activate them) that causes all the windows to flash.
A partial solution is to perform a check before deactivating the tool windows. We know that if a window is being deactivated, lParam identifies the (other) window about to be activated. And if this other window is one of our tool windows, we can skip deactivating the tool windows, because we know the other (tool) window is going to subsequently activate them anyway.
if (fKeepActive == FALSE)
{
while ((dwp = DockingNextToolWindow(container)))
{
if (dwp->hwnd == (HWND)lParam)
return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}
}
This prevents every tool window from briefly deactivating, then activating again. There is still a problem, albeit a minor one. The problem is, the single tool window that is being deactivated will still flicker briefly before being activated again. This is because it will already have received its WM_NCACTIVATE message, which caused the window to be redrawn deactivated. The window gets its activated look eventually, but this brief flicker is still visible.
Sync the activation of all tool windows
We need to take a step back and approach the problem from a slightly different direction. Instead of handling WM_ACTIVATE, which is called after a window's titlebar is redrawn, we'll go straight to the heart of the problem, and rewrite DockingActivate to be called whenever a window receives a WM_NCACTIVATE message. This will ensure that no unnecessary activation or deactivation will take place.
The function presented below performs several tasks on behalf of the tool (or owner) window that calls DockingActivate:
Search the list for the other window being activated/deactivated (the window specified by lParam, rather than the window receiving WM_NCACTIVATE). If this other window is a tool window, then we force all tool windows as activated.
Synchronize all current tool windows to our (possibly new) state.
Activate/deactivate the window that calls DockingActivate, depending on our new state.
The code looks like this:
CollapseLRESULT WINAPI DockingActivate(HWND container,
HWND hwnd, WPARAM wParam, LPARAM lParam)
{
DOCKINFO * dwp;
BOOL fKeepActive;
BOOL fSyncOthers;
// If this is a spoof'ed message we sent, then handle it
// normally (but reset LPARAM to 0).
if (lParam == -1)
return DefWindowProc(hwnd, WM_NCACTIVATE, wParam, 0);
fKeepActive = wParam;
fSyncOthers = TRUE;
while ((dwp = DockingNextToolWindow(container)))
{
// UNDOCUMENTED FEATURE:
// If the other window being activated/deactivated (i.e. not the one that
// called here) is one of our tool windows, then go (or stay) active.
if ((HWND)lParam == dwp->hwnd)
{
fKeepActive = TRUE;
fSyncOthers = FALSE;
break;
}
}
if (fSyncOthers == TRUE)
{
// Sync all other tool windows to the same state.
while ((dwp = DockingNextToolWindow(container)))
{
// Send a spoof'ed WM_NCACTIVATE message to this tool window,
// but not if it is the same window that called here. Note that
// we substitute a -1 for LPARAM to indicate that this is a
// spoof'ed message we sent. The operating system would never
// send a WM_NCACTIVATE with LPARAM = -1.
if (dwp->hwnd != hwnd && hwnd != (HWND)lParam)
SendMessage(dwp->hwnd, WM_NCACTIVATE, fKeepActive, -1);
}
}
return DefWindowProc(hwnd, WM_NCACTIVATE, fKeepActive, lParam);
}
The code above uses an undocumented feature of the WM_NCACTIVATE message which I observed while experimenting with these activation messages. The MSDN documentation states that lParam is unused (presumably zero), but this is not the case under Windows 95, 98, ME, and NT, 2000, XP.
Instead, lParam is a handle to the other window being activated/deactivated in our place (i.e., if we are being deactivated, lParam will be the handle to the window being activated). This is not always the case, specifically when the other window being activated/deactivated belongs to another process. In this case, lParam will be zero.
Sync the enabled state of all tool windows
Now, we need to tackle the other problem. When our owner window is disabled (perhaps because a modal dialog or message box has popped up), we need to disable all tool windows too. This feature prevents the user from clicking on and activating not only the main window, but also any tool window, while the modal dialog or message box is displayed.
The solution is similar to how we solved the activation problem, except that this time we write a function that a tool window or the owner window calls whenever it receives a WM_ENABLE message. DockingEnable simply enables/disables all the tool windows to the same state as the owner window.
Collapse/*********************** DockingEnable() **********************
* Sends WM_ENABLE to all the owner's tool windows.
* A window calls this in response to receiving a
* WM_ENABLE message.
*
* container = Handle to owner window.
* hwnd = Handle to window which received WM_ENABLE (can
* be the owner, or one of its tool windows).
* wParam = WPARAM of the WM_ENABLE message.
* lParam = LPARAM of the WM_ENABLE message.
*/
LRESULT WINAPI DockingEnable(HWND container,
HWND hwnd, WPARAM wParam, >LPARAM lParam)
{
DOCKINFO * dwp;
while ((dwp = DockingNextToolWindow(container)))
{
// Sync this tool window to the same state as the window that called
// DockingEnable (but not if it IS the window that called here).
if (dwp->hwnd != hwnd) EnableWindow(dwp->hwnd, wParam);
}
// Allow the window that called DockingEnable to handle its WM_ENABLE
// as normally it would.
return DefWindowProc(hwnd, WM_ENABLE, wParam, lParam);
}
Docking a tool window
The previous discussion took you through the steps necessary to create floating tool windows. Now, we'll discuss the techniques necessary to get these floating windows to "dock" with their owner window. I'm not going to reproduce all the library's source code in this tutorial, because quite a lot is involved. I'm instead going to give an overview of the approach taken, and you can study the profusely commented source code for details.
First of all, we need to define the terms "Docked" and "Undocked". A tool window is undocked when it is floating. And as we already know, in order to make that happen, the tool window must have the WS_POPUP style.
On the other hand, a tool window is docked when it is visually contained completely within its owner window, alongside one of the owner's borders. In order to make this happen, we must create the tool window with the WS_CHILD (not WS_POPUP) style (or change the style from WS_POPUP to WS_CHILD), and make its owner window also its parent window. When a tool window has the WS_CHILD style, the operating system restricts it to the area inside of its parent window, and the tool window is graphically "anchored" to its parent window (i.e., when the end user moves the parent window, the child window automatically moves with it).
But note that when the parent window is resized, the parent window will need to also move/resize the docked tool window so that the tool window remains "attached" to the border. (Of course, our library has functions the owner window can call to make this as easy as possible.)
A good docking library must allow the end user to be able to dock and undock any tool window by grabbing the tool window with the mouse and dragging it over to a dockable or undockable area. There are many different ways to implement docking windows. This is because there is no standard, built-in docking window support in Windows. Application developers have had to implement their own docking windows, or rely upon third party libraries to do the work for them (such as MFC).
There are two common types of docking window implementations. The most common (and intuitive, in my opinion) is the type where you grab the tool window (by a "gripper bar" or its title bar) with the mouse, and drag it around the screen. When you drag the tool window, instead of the window itself moving, a drag-rectangle (feedback rectangle) is XORed on the screen, showing the outline of where the window will move to when you release the mouse - like the way windows work when full-window dragging is turned off. With this method, when a window is dragged to / from a window, the feedback rectangle visibly changes to indicate that the window can be dropped. This is the docking implementation that our docking library uses.
A tool window being dragged. You can see the drag rectangle.
The second type of docking implementation can be found in some newer style applications (such as Microsoft Outlook). Instead of a feedback rectangle, windows can be directly "teared" or "snapped" on or off the owner window - i.e., they snap into place as soon as you manipulate them. Personally, I don't like this type of user-interface, and our docking library does not use it.
Our tool windows will have the following characteristics:
A docked tool window will have a "gripper bar" along its left side to allow the user to grab it and undock it.
A tool window will use a feedback (drag) rectangle as it's moved around the screen - even if the "full window drag" system setting is in effect. This is shown in the picture above.
While a drag-rectangle is dragged around the screen, at some point it will intersect one of the borders of its owner window. When this happens, the drag-rectangle will need to visibly change in order to reflect the fact that the tool window is now within a docking "region". Normal convention is for a wide (say three pixel) shaded rectangle to represent a floating position, and for a single-pixel rectangle to represent a docked position.
When the mouse is released after dragging a tool window, a test must be made to see if the window should be made to dock or float. (i.e., was the drag-rectangle ultimately moved to one of these docking "regions", or is it outside of any such region and therefore the tool window is floating?)
At the end user's discretion, a tool window can be forced to float, even when the drag-rectangle is released over a dockable area. This is usually achieved by the end user holding the
When floating, a tool window can be resized just like any normal window. No special processing is required to do this - the standard Windows sizing behaviour can be used in this case.
When docked, a tool window can be resized either vertically or horizontally (but not both) to decrease or increase its size. A tool window docked to the top or bottom border of its owner can be resized horizontally. A tool window docked to the left or right border can be resized vertically.
When the user double-clicks a floating tool window's titlebar, or a docked window's gripper bar, the tool window is toggled from floating to docked, or vice versa.
Our docking library keeps track of whether a tool window is docked or floating. And if it is docked, we need to know to which of the owner's borders the tool window is docked. The uDockedState field of the DOCKINFO is used to store this state. As mentioned, if this field is OR'ed with DWS_FLOATING, then the tool window is floating. If not OR'ed with DWS_FLOATING, then the tool window is docked, and the remaining bits of the field are either DWS_DOCKED_LEFT, DWS_DOCKED_RIGHT, DWS_DOCKED_TOP, or DWS_DOCKED_BOTTOM depending upon to which border the tool window is docked.
We need to be able to toggle a tool window between being a child window (docked) and being a popup window (floating). This is basically accomplished with the code shown below.
// Assume "dwp" is a pointer to the tool window's DOCKINFO.
DWORD dwStyle = GetWindowLong(dwp->hwnd, GWL_STYLE);
// Is the window currently floating?
if (dwp->uDockedState & DWS_FLOATING)
{
// Toggle from WS_POPUP to WS_CHILD. We do this by altering
// the window's style flags to remove WS_POPUP, and add
// WS_CHILD. Then, we set the owner window as the parent.
SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_POPUP) | WS_CHILD);
SetParent(dwp->hwnd, dwp->container);
}
else
{
// Toggle from WS_CHILD to WS_POPUP. We do this by altering
// the window's style flags to remove WS_CHILD, and add
// WS_POPUP. Then, we make sure it has no parent.
SetWindowLong(dwp->hwnd, GWL_STYLE, (dwStyle & ~WS_CHILD) | WS_POPUP);
SetParent(dwp->hwnd, NULL);
}
Look at the second SetParent API call in the code above. The only way to make a child (docked) window into a popup (floating) window is to set its parent window to zero (NULL). Because the tool window no longer has a parent, it is not visually confined to some other window. It can float freely around the desktop. But because it still has an owner window, the operating system keeps it floating above that owner window. In other words, when a window is docked, its owner window is also its parent window. When a window is floating, its owner window is no longer its parent as well.
Floating versus docked size
As mentioned, a tool window can be in one of two states: docked, or floating (undocked). We will remember the size of a tool window both in its floating state, and its docked state, and store this information in the DOCKINFO. In this way, the end user can give the tool window different sizes for its two states. Because we also allow the end user to quickly toggle between the two states by double-clicking on the gripper/titlebar, we need to remember where the tool window was last positioned in both states.
When a tool window is floating, it can be resized just like a normal window. This means that we will need to store both the width and height in the DOCKINFO. And of course, in order to remember its position, we need to store its X and Y position (in screen coordinates). These values are stored in the DOCKINFO's cxFloating, cyFloating, xpos, and ypos fields respectively.
Note: cxFloating and cyFloating are actually set to the size of the floating tool window's client (inner) area instead of the physical size of the tool window itself (including its titlebar and borders). This is because we always want the client area to remain the same size, even when the system settings change (i.e. the titlebar height is modified using the Control Panel).
When a tool window is docked, it can be resized in only one direction -- vertically or horizontally. This means that we need to remember only its width or height, but not both. If the tool window is docked to the top or bottom border of the owner, then we remember its height. If the tool window is docked to the left or right border of the owner, then we remember its width. Whichever value we remember, we store it in DOCKINFO's nDockedSize field. As far as its position is concerned, that is already remembered in the DOCKINFO's uDockedState field.
Moving a window with a drag-rectangle
The first obstacle we encounter is getting Windows to show a feedback rectangle when the end user moves a floating window around. Starting with Windows 95, a new user-interface feature was introduced. This feature is normally referred to as "Show window contents while dragging". When enabled, windows are no longer moved and sized using the standard feedback rectangle.
Unfortunately, there is no way to turn this feature off for specific windows. The SystemParametersInfo API call (with the SPI_GETDRAGFULLWINDOWS setting) can turn this feature on and off, but this is a system-wide setting, and is not really suitable. Of course, we could devise a method where we temporarily turn off the drag-window system setting just during the window movement (actually, this would be very straight-forward). The point is, it's a bit of a hack, and I prefer proper solutions to problems like this.
The only solution is to override the standard Windows behaviour and manually provide a feedback rectangle. This means processing a few mouse messages. Now, I don't want to show any code - again, the source code clearly demonstrates how to get this working (in the window procedure for a tool window, dockWndProc). What I will do is give a basic outline of the processing that is required.
The most important task is to stop the user from dragging the window around with the mouse. I know this sounds counter-productive, but we need to completely take over the standard window movement logic. This is actually quite simple - our docking window procedure just needs to handle WM_NCLBUTTONDOWN, and return 0 if the mouse is clicked in the caption area. By preventing the default window procedure from handling this message, window dragging is completely disabled.
In order to simulate the window being moved, we need to handle a few mouse messages. Only three need processing:
WM_NCLBUTTONDOWN - This message is received when the end user clicks on a tool window. In addition to returning 0 to prevent the operating system from doing normal window dragging, we draw the drag-rectangle at its initial position, and set the mouse capture using the SetCapture API call. We also install a keyboard hook so we can check if the end user presses the CTRL key (to force the tool window floating) or the ESC key (to abort the operation).
WM_MOUSEMOVE - This message is received whenever the mouse is moved. Our response is to redraw the drag-rectangle in the new position (erase it in the old position and draw it in the new position). In addition, we need to decide what type of rectangle to draw, depending on if the end user has moved the rectangle into a dockable region, or not.
WM_LBUTTONUP - This message is received when the mouse is released. We remove the drag-rectangle from the screen, release the mouse capture, and then take the appropriate action to physically reposition the tool window. This may mean docking / undocking, or simply moving the window if it was already floating.
As you can see, there's a little bit of work involved, but nothing particularly complicated. The big advantage of using this method is that the same mouse code can be used when the window is docked or floating. This keeps the code short and simple.
Drawing a drag-rectangle
A drag-rectangle is basically just a simple rectangle. This rectangle ideally needs to be drawn using XOR blitting logic, so that we can easily draw / erase the rectangle as it is moving around.
A tool window being dragged. You can see the drag rectangle.
The code below draws a shaded rectangle with the specified coordinates. The equivalent function in the source code does a little more than the code below (it draws both types of drag-rectangles), but I've stripped it down to keep it simple.
Collapsevoid DrawXorFrame(int x, int y, int width, int height)
{
// Raw bits for bitmap - enough for an 8x8 monochrome image
static WORD _dotPatternBmp1[] =
{
0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055, 0x00aa, 0x0055
};
HBITMAP hbm;
HBRUSH hbr;
HANDLE hbrushOld;
WORD *bitmap;
int border = 3;
HDC hdc = GetDC(0);
// Create a patterned bitmap to draw the borders
hbm = CreateBitmap(8, 8, 1, 1, _dotPatternBmp1);
hbr = CreatePatternBrush(hbm);
hbrushOld = SelectObject(hdc, hbr);
// Draw the rectangle in four stages - top, right, bottom, left
PatBlt(hdc, x+border, y, width-border, border, PATINVERT);
PatBlt(hdc, x+width-border, y+border, border, height-border, PATINVERT);
PatBlt(hdc, x, y+height-border, width-border, border, PATINVERT);
PatBlt(hdc, x, y, border, height-border, PATINVERT);
// Clean up
SelectObject(hdc, hbrushOld);
DeleteObject(hbr);
DeleteObject(hbm);
ReleaseDC(0, hdc);
}
As you can see, we have the bitmap data for our rectangle as global data in our docking library. And we simply call some graphics functions to blit in onto the screen (in a rectangular shape) at the screen position where the end user has currently moved the mouse.
Redrawing the docked windows
When a tool window's state changes from docked to floating, or vice versa, this means that the layout of the owner window needs to be redrawn. For example, if a tool window was floating, and then is docked to the owner window, then other tool windows already docked may need to be resized/repositioned to accommodate the new docked tool window.
And if a tool window was docked to the owner window, but is torn off and left floating, that means the other, remaining docked windows may likewise need to be resized/repositioned to fill the "hole" left by the formerly docked window.
Whenever a tool window's state toggles between states, our docking library has a function named updateLayout that is called to send a spoofed WM_SIZE message to the owner window to inform it that it needs to redraw itself. The owner window then is expected to redraw its contents and call a docking library function named DockingArrangeWindows. DockingArrangeWindows does all the work of repositioning and redrawing the docked tool windows.
Enumerating tool windows
In the above code excerpts, we had a placeholder function named DockingNextToolWindow that enumerated the tool windows for a given owner window. We don't actually have such a function in the docking library. Let's examine how our docking library actually enumerates tool windows.
Unfortunately, the Windows operating system does not have a function to enumerate all the windows owned by a particular window. If it did, we could just pass our owner window to that function. What the operating system does have is a function called EnumChildWindows. This enumerates all of the child windows of a given parent window. Since a docked tool window has its owner window as its parent also, EnumChildWindows will enumerate all the docked tool windows for a given owner. But EnumChildWindows will not enumerate any of the floating tool windows, because the owner window is not also the parent of the floating tool windows.
There is another operating system function called EnumWindows. This enumerates all of the top-level (i.e., popup) windows on the desktop. Since our floating tool windows are WS_POPUP style, this works to enumerate them. (But, it will enumerate all windows on the desktop in addition to our tool windows, so we have a little extra work to do to isolate only the desired tool windows). EnumWindows does not enumerate any of the children (WS_CHILD windows) of those top-level windows. So, EnumWindows will not enumerate any docked tool windows.
Therefore, enumerating all the tool windows will be a two-step process. First, we'll call EnumChildWindows to enumerate the docked windows for a given parent window (which also happens to be the owner window). Then, we will call EnumWindows to enumerate the floating tool windows for a given owner window, and do some extra processing to make sure that the windows we isolate are for the desired owner window.
Let's examine a function that counts how many total tool windows a given owner window has, both floating and docked.
Collapsetypedef struct {
UINT count;
HWND container;
} DOCKCOUNTPARAMS;
/***************** DockingCountFrames() *****************
* Counts the number of tool windows for the specified
* owner window.
*
* container = Handle to owner window.
*/
UINT WINAPI DockingCountFrames(HWND container)
{
DOCKCOUNTPARAMS dockCount;
// Initialize count to 0, and store the desired owner window
dockCount.count = 0;
dockCount.container = container;
// Enumerate/count the floating tool windows
EnumWindows(countProc, (LPARAM)&dockCount);
// Enumerate/count the docked tool windows
EnumChildWindows(container, countProc, (LPARAM)&dockCount);
// Return the total count
return dockCount.count;
}
/******************* countProc() ********************
* This is called by EnumChildWindows or EnumWindows
* for each window.
*
* hwnd = Handle of a window.
* lParam = The LPARAM arg we passed to EnumChildWindows
* or EnumWindows. That would be our DOCKCOUNTPARAMS.
*/
static BOOL CALLBACK countProc(HWND hwnd, LPARAM lParam)
{
DOCKINFO * dwp;
// Is this one of the tool windows for the desired owner window?
if (GetClassWord(hwnd, GCW_ATOM) == DockingFrameAtom &&
(dwp = (DOCKINFO *)GetWindowLong(hwnd, GWL_USERDATA)) &&
dwp->container == ((DOCKCOUNTPARAMS *)lParam)->container)
{
// Yes it is. Increment count.
((DOCKCOUNTPARAMS *)lParam)->count += 1;
}
// Tell operating system to continue.
return TRUE;
}
Notice that we use countProc() as the callback for both EnumWindows and EnumChildWindows. And we pass our own initialized DOCKCOUNTPARAMS structure to our callback. First, we call EnumWindows to enumerate the floating windows. Then we call EnumChildWindows to enumerate the docked windows for our desired owner. So, let's examine countProc(). The entire key to making this work is to fetch and check the class ATOM for the window. If it matches the ATOM we got when we registered our own docking window class (returned by RegisterClassEx), then we know this is one of our tool windows. And if it is one of our tool windows, we know that its GWL_USERDATA field should contain its DOCKINFO. And note that the owner window handle has been stored in the DOCKINFO's container field. So we need only compare this handle with the owner handle passed to DockingCountFrames in order to determine if it is a tool window for the desired owner window.
Various other features of the library
The discussion above details all of the most important aspects of our docking library's features. But, there are some more, incidental features which are optional. You can enable any of these features for a given tool window just by setting the appropriate value into its DOCKINFO's dwStyle field. For example, you can force a tool window to always stay docked or floating. You can force it to keep its original size. You can restrict to which sides of the owner window the tool window may be docked.
When DockingAlloc creates a DOCKINFO, none of these extra features are enabled.
--------------------------------------------------------------------------------
An application
Up to this point, we've discussed only the code in the docking library. Since the whole intent of the library is to be used by an application, now we'll turn our attention to a sample application. There is a sample C application called DockTest included with the library. This example creates one owner window. The owner has a View -> Tool Window menu item you can select to create a tool window. You can then move the tool window around, docking and undocking it, to get a feel for how the implementation works. Each time you select this menu item, another tool window is created, so you can see how multiple tool windows can be floated and docked.
The owner window we create is an MDI window, and its window procedure is frameWndProc. You can open a document window with the File -> New menu item, and see how the docked tool windows interact with a document window. (But, as we'll see later, the application needs to do a little work to manage this interaction.)
Creating a tool window
Let's examine how the application creates a tool window. This happens when the View -> Tool Window menu item is selected, so the place where we create the tool window is in frameWndProc's handling of WM_COMMAND for menu ID IDM_VIEW_TOOLWINDOW. Below is a slightly simplified version of what needs to be done to create a tool window:
Collapsevoid createToolWindow(HWND owner)
{
DOCKINFO *dw;
HWND frame;
// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
// Create a Docking Frame window (ie, the tool window).
if ((frame = DockingCreateFrame(dw, owner, "My title")))
{
// Create the child window that will be hosted inside of the Docking
// Frame window (ie, the contents of the tool window's client area)
// and save it in the DOCKINFO's focusWindow field. We'll create an
// EDIT control to be the contents, but you can utilize any standard
// control, or a child window of your own class.
if((dw->focusWindow = CreateWindow("EDIT", 0,
ES_MULTILINE|WS_VSCROLL|WS_CHILD|WS_VISIBLE,
0,0,0,0,
frame,
(HMENU)IDC_MYEDIT, GetModuleHandle(0), 0)))
{
// Show the Docking Frame.
DockingShowFrame(dw);
// Success!
return;
}
// Destroy the tool window if we can't create its contents.
// NOTE: The docking library will free the above DOCKINFO.
DestroyWindow(frame);
}
MessageBox(0, "Can't create tool window", "ERROR", MB_OK);
}
else
MessageBox(0, "No memory for a DOCKINFO", "ERROR", MB_OK);
}
First, we call DockingAlloc to get a DOCKINFO structure. We pass the desired initial state, which will be one of DWS_FLOATING, DWS_DOCKED_LEFT, DWS_DOCKED_RIGHT, DWS_DOCKED_TOP, or DWS_DOCKED_BOTTOM, depending upon whether we want the tool window initially created floating, or docked to one of the four borders. The docking library creates a DOCKINFO and initializes it to default values, returning a pointer.
At this point, we could modify the DOCKINFO if we want something other than the default features. In the above code, we simply go with the defaults.
Next, we call DockingCreateFrame to create the actual tool window. We pass the DOCKINFO we just got, the handle to our owner window, and the desired title for the tool window (which is shown only when the tool window is floating). DockingCreateFrame will create the tool window and return its handle. The tool window is not created visible, so nothing has yet shown up onscreen.
Now, a tool window with nothing inside of it would not be of much use. So we need to create something inside of the tool window that is of use to the end user. We can say that the tool window needs some "contents". Specifically, we need to create some WS_CHILD window which has the tool window as its parent. This can be any standard control, such as an Edit box, list box, tree-view control, etc. Or it could be a window of our own class. In the above code, we simply create a multi-line Edit control. Note that we have set the tool window to be this control's parent, and also specified the WS_CHILD style. This will cause the Edit control to be visually embedded inside of the tool window, and automatically move with the tool window. The size and position of the Edit control is not important now, because it will be sized and positioned later, before the tool window is finally made visible. We stuff the handle to this control into the DOCKINFO's focusWindow field. The docking library will automatically size this control to fill the client area of the tool window, and also give the control the focus whenever the user activates that tool window.
Finally, we call DockingShowFrame. This first sends a WM_SIZE message to our owner window (which is where we will do the final sizing/positioning of the tool window and its contents-window), and then makes the tool window visible.
That's all there is to creating a tool window. At this point, the docking library will manage the docking and undocking of this window.
Handling WM_SIZE message in the owner
The docking library transparently handles most aspects of the tool windows. But there are a couple times when it needs help from the application. One such time is whenever the owner window is resized. Given a new size for the owner window, it stands to reason that any docked tool window may also need to be resized and repositioned so that it stays docked to the desired side of the owner window. For this reason, the owner window will have to do the following when it receives a WM_SIZE:
Collapse case WM_SIZE:
{
HDWP hdwp;
RECT rect;
// Do the default handling of this message.
DefFrameProc(hwnd, MainWindow, msg, wParam, lParam);
// Set the area where tool windows are allowed.
// (Take into account any status bar, toolbar etc).
rect.left = rect.top = 0;
rect.right = LOWORD(lParam);
rect.bottom = HIWORD(lParam);
// Allocate enough space for all tool windows which are docked.
hdwp = BeginDeferWindowPos(DockingCountFrames(hwnd,
1) + 1); // + 1 for the MDI client
// Position the docked tool windows for this owner
// window. rect will be modified to contain the "inner" client
// rectangle, where we can position an MDI client.
DockingArrangeWindows(hwnd, hdwp, &rect);
// Here we resize our MDI client window so that it fits into the area
// described by "rect". Do not let it extend outside of this
// area or it (and the client windows inside of it) will be obscured
// by docked tool windows (or vice versa).
DeferWindowPos(hdwp, MainWindow, 0, rect.left, rect.top,
rect.right - rect.left, rect.bottom - rect.top,
SWP_NOACTIVATE|SWP_NOZORDER);
EndDeferWindowPos(hdwp);
return 0;
}
First, we pass the WM_SIZE to DefFrameProc to let the operating system do the default sizing of the owner window. We need to do this first so that the owner window's size is finalized before we go ahead and resize/reposition the docked tool windows.
The docking library has a function called DockingArrangeWindows that redraws all of the docked windows for a given owner. So to completely redraw its docked windows, all the owner needs to do is call this one function. But, there are a couple prerequisites. First, the owner must fill in a RECT with the dimensions of its client area, and pass this to DockingArrangeWindows. Secondly, the owner window must call the Windows API BeginDeferWindowPos to reserve enough space for all the docked windows. (The docking library has a function called DockingCountFrames which can be called to retrieve the total number of docked tool windows in an owner.) We use BeginDeferWindowPos so that, if there are many tool windows, we defer the final painting until after all of them are sized and positioned. This is more efficient and doesn't cause any unsightly visual artifacts for the end user to witness.
One very important aspect to note is that, after DockingArrangeWindows resizes and repositions all of the docked tool windows, it updates the RECT so that it encompasses only the owner client area not occupied by the tool windows. In other words, the RECT is the remaining, blank client area. We take this remaining area, and we resize/reposition our MDI child so that it fills only this remaining area. In this way, our document windows are not obscured by docked tool windows, and vice versa.
Handling WM_NCACTIVATE message in the owner
Another instance where our docking library needs help from the application is whenever the owner window receives a WM_NCACTIVATE message. Remember earlier we discussed how to keep all tool windows' titlebar activation in sync with the owner window. Now, we need the owner window to let the docking library know whenever it receives a WM_NCACTIVATE message. The owner window will need to do the following:
case WM_NCACTIVATE:
{
DOCKPARAMS dockParams;
dockParams.container = dockParams.hwnd = hwnd;
dockParams.wParam = wParam;
dockParams.lParam = lParam;
return(DockingActivate(&dockParams));
}
We simply fill in a DOCKPARAMS struct (defined in DockWnd.h) with the values we receive from the WM_ACTIVATE message, and also fill in the owner window handle (in DOCKPARAMS container field) and the handle of the window receiving the WM_ACTIVATE (which here, is the owner window). Then we call DockingActivate which takes care of syncing all tool windows' titlebar activation.
Handling WM_ENABLE message in the owner
Another instance where our docking library needs help from the application is whenever the owner window receives a WM_ENABLE message. Remember earlier we discussed how to keep all tool windows' enabled state in sync with the owner window. Now, we need the owner window to let the docking library know whenever it receives a WM_ENABLE message. The owner window will need to do the following:
case WM_ENABLE:
{
DOCKPARAMS dockParams;
dockParams.container = dockParams.hwnd = hwnd;
dockParams.wParam = wParam;
dockParams.lParam = lParam;
return(DockingEnable(&dockParams));
}
This is almost the same as the WM_NCACTIVATE handling, but it concerns the WM_ENABLE message, and we call a function named DockingEnable. DockingEnable takes care of syncing all tool windows' enabled state.
Handling messages sent to standard controls
You'll note that we used a standard Edit control as the content of our tool window. As you should know, an Edit window sends messages to its parent for certain actions. For example, when the end user alters the contents of the Edit control, a WM_COMMAND message is sent with a notification code of EN_CHANGE.
But remember that the parent window is the tool window, and our tool window procedure (dockWndProc) is in the docking library. So how does an application get a hold of that message?
There is a DockMsg field in the DOCKINFO. Into this field, we will stuff a pointer to a function in our application. We do this after we DockAlloc the DOCKINFO as so:
// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
// Set our own function for the docking library to call.
dw->DockMsg = myMessages;
...
Whenever dockWndProc receives a message that it doesn't handle, such as a WM_COMMAND or WM_NOTIFY, it will call our application function, passing the DOCKINFO of the tool window, as well as the message, WPARAM, and LPARAM parameters.
Here is an example of the function we could add to handle a EN_CHANGE from our IDC_MYEDIT edit control:
LRESULT WINAPI myMessages(DOCKINFO * dwp, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
{
if (LOWORD(wParam) == IDC_MYEDIT)
{
if (HIWORD(wParam) == EN_CHANGE)
{
// Here we would handle the EN_CHANGE, and then return
// 0 to tell the docking library we handled it.
return 0;
}
}
}
}
// Return -1 if we want the docking library to do default handling.
return -1;
}
Note: Each DOCKINFO can have its own DockMsg function, so you do not need to worry about control ID conflicts between tool windows.
Multiple child windows inside a tool window
Above, we used a single Edit control to fill the client area of the tool window. But what if we would like several controls inside the tool window, for example, an Edit control as well as a push button labeled "Clear" which clears the text from the Edit control?
This is entirely possible, but there are a couple of prerequisites. First, when we create the Edit and button controls, we must make both of them children of the tool window. Secondly, we must provide a function that will resize and reposition the controls, and stuff a pointer to this in the DOCKINFO's DockResize field after we DockAlloc the DOCKINFO.
// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
// Set our own functions for the docking library to call.
dw->DockMsg = myMessages;
dw->DockResize = myResize;
...
When the docking library calls our function, it passes the DOCKINFO for the tool window, as well as a RECT that encompasses the area that we need to fill. Here is an example of a function we could add to resize and reposition the edit and button controls so that the button stays near the bottom border of the tool window, and the edit control fills the rest of the area:
void WINAPI< myResize(DOCKINFO * dwp, RECT * area)
{
HWND child;
// Position the button above the bottom border
child = GetDlgItem(dw->hwnd, IDC_MYBUTTON);
SetWindowPos(child, 0, rect->left + 10,
rect->bottom - 20, 50, 18, SWP_NOZORDER|SWP_NOACTIVATE);
// Let the edit fill the remaining area
child = GetDlgItem(dw->hwnd, IDC_MYEDIT);
SetWindowPos(child, 0, rect->left, rect->top,
rect->right - rect->left, (rect->bottom - rect->top) - 22,
SWP_NOZORDER|SWP_NOACTIVATE);
}
Closing a tool window
When an owner window is destroyed, all of its tool windows are also automatically destroyed (except if you use the DWS_FREEFLOAT style. In that case, your owner window should handle WM_DESTROY and call DockingDestroyFreeFloat). The docking library will normally free the DOCKINFO for each tool window destroyed.
If you wish to manually close a tool window, you simply call the Windows API DestroyWindow, passing the handle to the desired tool window. Again, the docking library will normally free the DOCKINFO.
If you wish to override the docking library's default behavior of freeing the DOCKINFO, then you must write your own function, and stuff a pointer into the DOCKINFO's DockDestroy field. The docking library will call this function (passing it the DOCKINFO) whenever the tool window has been destroyed. It is your responsibility to eventually free the DOCKINFO by passing it to DockingFree. One use for this is to keep a DOCKINFO allocated (for a given tool window) throughout the lifetime of your application. You will reuse this same DOCKINFO with DockingCreateFrame each time the end user reopens that tool window. Because the DOCKINFO stores the last size and position of the tool window, this means that the tool window will reappear where it was located right before it was previously destroyed. You will then free the DOCKINFO only when your application is ready to terminate.
Saving/Restoring a tool window size/position
Each time that you run your application, you will normally want to restore the same sizes and positions for the tool windows that the end user set upon the last time your application was run. The docking library has two functions to help save and restore tool window positions. The data is saved to the Windows registry under some key of your choosing. The size/position of each tool window is saved separately as values under that key.
Before you free a tool window's DOCKINFO, you should first create/open some registry key of your choosing in order to save that tool window's settings. Then, you will pass the DOCKINFO, and a handle to this open key, to DockingSavePlacement. The docking library will save that tool window's settings. Here is an example of how we could save the settings of a tool window under the registry key "HKEY_CURRENT_USER\Software\MyKey\MyToolWindow":
HKEY hKey;
DWORD temp;
// Open/Create the "Software\MyKey\MyToolWindow" key under CURRENT_USER.
if (!RegCreateKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
0, 0, REG_OPTION_NON_VOLATILE, KEY_ALL_ACCESS, 0, &hKey, &temp))
{
// Let the docking library save this tool window's settings.
DockingSavePlacement(dw, hKey);
// Close registry key.
RegCloseKey(hKey);
}
Whenever your program runs, it should restore those settings by calling DockingLoadPlacement right after DockAlloc'ing the DOCKINFO for that tool window. Here is an example of restoring the previously saved settings:
// Allocate a DOCKINFO structure.
if ((dw = DockingAlloc(DWS_DOCKED_BOTTOM)))
{
HKEY hKey;
// Open the "Software\\MyKey\\MyToolWindow" key under CURRENT_USER.
if (!RegOpenKeyEx(HKEY_CURRENT_USER, "Software\\MyKey\\MyToolWindow",
o, KEY_ALL_ACCESS, &hKey))
{
// Let the docking library restore this tool window's settings.
DockingLoadPlacement(dw, hKey);
// Close registry key.
RegCloseKey(hKey);
}
...
Note: If there are no previously saved settings for the tool window, then none of the DOCKINFO fields are altered
No comments:
Post a Comment