Docking Toolbars - Part 1
13 minute read •
Introduction
This tutorial will show you how to create docking toolbars, using pure Win32 techniques. I’m going to split this topic in two separate pages. The first tutorial will cover the “floating” aspect of toolbars - i.e. how to get tool windows to stay floating on top of all other windows, how to get the window activation working correctly etc. The second tutorial will show you how to get these floating toolbars to “dock” to the side of a window, and discuss various methods of window management.
How to create a floating toolbar
A floating toolbar is just a standard window with the WS_POPUP
style set. 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 you might create and display a floating window:
// create a tool window
HWND hwnd = CreateWindowEx(
WS_EX_TOOLWINDOW,
"ToolWindowClass", "ToolWindow",
WS_POPUP | WS_SYSMENU | WS_THICKFRAME | WS_CAPTION,
200, 200, 400, 64,
hwndParent, NULL, GetModuleHandle(0), NULL
);
The WS_EX_TOOLWINDOW
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
and an owner window (hwndParent
).
A window can also be positioned to stay at the top of the Z-order with a call to SetWindowPos:
// position the window to appear above all others
SetWindowPos(hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE|SWP_NOACTIVATE);
Prevent window deactivation
The image shown above shows the main window’s titlebar as inactive. This is entirely normal, because only one window can be active at any one time in Windows. However, it has become normal practise for tool windows and main windows to appear active at the same time. I think it looks more natural this way, so we need to devise a strategy to keep windows appearing active, even if they aren’t.
The key to the problem is the WM_NCACTIVATE
message. This message is sent when a window’s non-client area 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
withwParam=TRUE
, this indicates that the non-client area (the titlebar and border) needs to become active. - When
wParam=FALSE
, this indicates that the non-client area needs to become inactive. - MSDN states that
lParam
will always be0
. However, I have observed thatlParam
indicates the window handle of the window being deactivated. This appears to be true under win95,98 and NT,2000. - A window can
return TRUE
to prevent the non-client area from being deactivated.
Now, when this message is passed to DefWindowProc
, two things happen. The first is to redraw the titlebar as either active or inactive, depending on the value of wParam. The second is to set an internal flag for the window which basically remembers if the window was painted as active or inactive. This enables subsequent WM_NCPAINT
updates to correctly paint the titlebar the way it was requested. It is therefore advisable to always pass WM_NCACTIVATE
to DefWindowProc
for proper processing.
If you havn’t realised already, this WM_NCACTIVATE
messages provides us with a way to make all our application tool windows look active, even if they aren’t. Try adding the following code to any window which you want to keep active (this includes any tool windows and the main frame window):
case WM_NCACTIVATE:
return DefWindowProc(hwnd, msg, TRUE, lParam);
By overriding any requests to make the non-client area appear inactive, we can create the effect we want.
Note that 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 set, instead of WS_POPUP
.
Proper window activation
The method shown above is all well and good, but there is a problem. The main window and tool windows will always appear active, even if the application is not in the foreground. Also, whenever we decide to display a message box or a normal dialog box, the main 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. The list below describes the series of window activation messages sent when one window becomes active, and another inactive.
WM_MOUSEACTIVATE is sent to the window to ask it whether or not the activation request should be allowed. The return value (i.e. MA_ACTIVATE
or MA_NOACTIVATE
) effects the subsequent activation messages.
WM_ACTIVATEAPP is sent when a window belonging to a different application is about to become active (or inactive). The return value should always be zero, and never effects subsequent message’s behaviour.
WM_NCACTIVATE is sent when a window’s non-client area needs to be activated or deactivated.
WM_ACTIVATE is sent last of all, to activate the window itself. The default window procedure sets the input focus to the window being activated.
In addition to these messages, I shall describe a couple of scenarios to get you thinking.
- With all of these activation messages, only two windows are actually involved - the window being deactivated, followed by the window being activated. So, even if we have many floating tool windows, not all of them will be affected by window activation. This means that:
- When our application is activated / deactivated, ALL toolwindows need to be updated to keep in sync with each other - either to be shown as activate or inactive.
- This also applies to activation within our own application - all toolwindows need to be kept in sync.
- When the main window is disabled due to a modal dialog or message box being displayed, then the tool windows (or any modeless dialogs) will remain unaffected. It is usually desirable to enable / disable all tool windows in a group to prevent the user from interacting with them whilst a modal dialog is on screen.
Popup window list
In order to keep all popup windows (including the main window) synched to the same state, we need to be able to identify all popup windows at any one time. Therefore we will define a function, GetPopupList
, which will return a list of all floating popup windows that we want to keep synchronised. The function will look like this:
int GetPopupList(HWND hwndMain, HWND hList[], DWORD nSize, BOOL fIncMain);
hwndMain specifies the parent or owner window of the popups that we want to return.
hList is the address of an array of window handles (HWNDs), which is where we will store the list of popup windows.
nSize specifies the number of elements in that array (so we don’t accidently overflow it).
fIncMain is a boolean flag, which specifies whether or not to include the main window (which is also a popup window) in the list.
There are many different ways to implement this function. In the source-download, I simply keep a private list of floating ToolWindows, and copy that into the array provided.
int GetPopupList(HWND hwndMain, HWND hList[], DWORD nSize, BOOL fIncMain)
{
int i, count = 0;
// just copy our private list of popups.
// Also check to see that each popup is owned by hwndMain.
for(i = 0; i < nNumDocWnds && i < nSize; i++)
hList[count++] = hPrivate[i];
if(fIncMain)
hList[count++] = hwndMain;
return count;
}
Depending on how you organise the windows in your project, you may need to provide a different implementation of this function. Another method to get a list of popup windows is to use the EnumThreadWindows
API call, and iterate over the popup windows which are owned by the current process. This would also return all windows such as dialog boxes, and non-toolbar windows, so would require more careful coding.
Generic WM_ACTIVATE handler
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 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
.
LRESULT HANDLE_ACTIVATE(HWND hwndMain, HWND hwnd, WPARAM wParam, LPARAM lParam)
{
HWND hwndList[MAX_DOCK_WINDOWS+1];
int i, nNumWnds;
BOOL fKeepActive = (wParam != WA_INACTIVE);
// find list of popups
nNumWnds = GetPopupList(hwndMain, hwndList, MAX_DOCK_WINDOWS+1, TRUE);
// Sync all other popups to the same state
for(i = 0; i < nNumWnds; i++)
SendMessage(hwndList[i], WM_NCACTIVATE, fKeepActive, 0);
return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}
It works, after a fashion. All popups activate and deactivate correctly, and all at the same time. This solution is not the best though.
The problem is that every popup window’s titlebar flashes whenever the active window changes. This is because of the way Windows sends the WM_ACTIVATE
message. This message is sent first of all to the window that is being deactivated. This causes our handler (above) to deactivate all the popup windows. WM_ACTIVATE
is then received for the active window, which then (correctly) activates all the popup windows. It is this anomaly that causes all the windows to flash.
A partial solution is to perform a check before synchronising all the popup windows. We know that if a window is being deactived, lParam
identifies the window being activated. And if this window is one of our popup windows, we can completely skip synchronizing all the windows, because the popup window that is about to become active will do this anyway.
if(fKeepActive == FALSE)
{
for(i = 0; i < nNumWnds; i++)
if(hwndList[i] == (HWND)lParam)
return DefWindowProc(hwnd, WM_ACTIVATE, wParam, lParam);
}
This prevents every popup window from briefly deactivating, then activating again. There is still a problem, albeit a minor one. The problem is, the single window that is being deactivated will still flicker briefly before being activated again. This is because it will already have received it’s WM_NCACTIVATE
message, which caused the window to deactivate. The window gets it’s activated look eventually, but this brief flicker is still visible.
Generic WM_NCACTIVATE handler
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 will go straight to the heart of the problem. We will write a handler for the WM_NCACTIVATE
message which will ensure that no unnecessary activation or deactivation will take place.
The function presented below can be called from any window that wants to be kept active alongside all the popup toolbars. The only requirement on your part is to provide a suitable GetPopupList
which includes all these windows.
The handler will perform several tasks, listed below.
- Retrieve a list of current popup windows.
- Search the list for the other window being activated/deactivated in our place (the window specified by
lParam
). If this other window is a toolwindow, then we need to remain active. - Synchronize all current popup windows to our (possibly new) state.
- Activate/deactivate ourselves, depending on our new state.
The code looks like this:
LRESULT HANDLE_NCACTIVATE(hwndMain, HWND hwnd, WPARAM wParam, LPARAM lParam)
{
static HWND hwndList[MAX_DOCK_WNDS];
int i, nNumWnds;
HWND hParam = (HWND)lParam;
BOOL fKeepActive = wParam;
BOOL fSyncOthers = TRUE;
nNumWnds = GetPopupList(hwndMain, hwndList, MAX_DOCK_WNDS, TRUE);
// UNDOCUMENTED FEATURE:
// If the other window being activated/deactivated
// (i.e. NOT this one) is one of our popups, then go (or stay) active.
for(i = 0; i < nNumWnds; i++)
{
if(hParam == hwndList[i])
{
fKeepActive = TRUE;
fSyncOthers = FALSE;
break;
}
}
// If this message was sent by the synchronise-loop (below)// then exit normally. If we don't do this, there will be an infinite loop!
if(hParam == (HWND)-1)
{
return DefWindowProc(hwnd, WM_NCACTIVATE, fKeepActive, 0);
}
// This window is about to change (inactive/active).// Sync all other popups to the same state
if(fSyncOthers == TRUE)
{
for(i = 0; i < nNumWnds; i++)
{
// DO NOT send this message to ourselves!!!!
if(hwndList[i] != hwnd && hwndList[i] != hParam)
SendMessage(hwndList[i], WM_NCACTIVATE, fKeepActive, (LONG)-1);
}
}
return DefWindowProc(hwnd, WM_NCACTIVATE, fKeepActive, lParam);
}
The code above uses an undocumented feature of the WM_NCACTIVATE
message which I observed whilst 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.
Updated 31-8-2006: It has been brought to my attention that lParam
will be NULL when the other window being activated/deactivate belongs to another thread - not just a different process. Therefore care should be taken when using this technique - all toolbars / windows involved in this activation scheme must belong to the same thread.
Generic WM_ENABLE handler
This task is optional, but will make your application slicker if you do include this functionality. This message handler is required for the following situation. Whenever a modal dialog box or message box is displayed, the main application window (and all it’s children) become disabled. This feature prevents the user from clicking on and activating the main window whilst the modal dialog is displayed.
However, popups, modeless dialogs, or any floating window will not be disabled. This means that the user can quite easily activate these windows whilst a modal dialog is displayed. In my opinion, this is not a good idea because the whole idea of a modal dialog is to present the user with a single, uninterruptable task. The solution is to write a simple WM_ENABLE
handler which we can call from the main window procedure. Whenever a WM_ENABLE
is received (either to enable or disable), the handler simply enables/disables all the current floating toolwindows to the same state.
LRESULT HANDLE_ENABLE(hwndMain, HWND hwnd, WPARAM wParam, LPARAM lParam)
{
static HWND hwndList[MAX_DOCK_WNDS];
int i, nNumWnds;
HWND hParam = (HWND)lParam;
nNumWnds = GetPopupList(hwndMain, hwndList, MAX_DOCK_WNDS, FALSE);
// Synchronise all toolwindows to the same state.
for(i = 0; i < nNumWnds; i++)
{
if(hwndList[i] != hwnd)
{
EnableWindow(hwndList[i], wParam);
}
}
//just do the default
return DefWindowProc(hwnd, WM_ENABLE, wParam, lParam);
}
Using the handler in your application
In order to use the WM_NCACTIVATE
message handler in your own application, you need to do two things. Firstly, you need to call the handler from ANY window that you want to keep active along side another. This includes the main window, and any popup windows that you to keep synchronised. The second requirement is to ensure that the GetPopupList
function returns the correct list of popups for your particular application.
Special care needs to be taken when calling HANDLE_NCACTIVATE
. As well as passing the window handle for which the message was received (the second parameter), the top-level owner window also needs to be specified, as the first parameter. For the top-level window in your application, the same window handle must be used for the first and second parameter:
case WM_NCACTIVATE:
return HANDLE_NCACTIVATE(hwnd, hwnd, wParam, lParam);
On the other hand, a popup window would call the handler slightly differently. The main top-level window still needs to be specified as the first parameter, so this window needs to be retrieved for any popup window whilst processing this message:
case WM_NCACTIVATE:
return HANDLE_NCACTIVATE(GetOwner(hwnd), hwnd, wParam, lParam);
The sample application in the source-download shows you exactly how to use the handler in your own applications, so you shouldn’t have any problems figuring it all out.
Conclusion
Hopefully you have learnt how to create “proper” floating tool windows. This technique has been around since the days of Windows 3.0 (and probably earlier), but clear information on the subject always seems to be difficult to find.
This article went on a bit longer than I originally wanted it to. However, I felt it was important to cover all the angles, to give the reader a better understanding of how windows really works.
The next article in this two-part series will show you how to make these floating tool windows into dockable windows, so stay tuned!