
Table of contents
Introduction
A dockable pane is a general purpose window container, like a view, that has two states with respect to dockability: docked or float in mini-frame.
The main difference with a view is that a view is built to display the main application content while
a pane provides context relative content to what the view has. For
example, the toolbox pane in Visual Studio is always active and filled with controls when you insert
a new dialog into the project and it will show up as an empty pane otherwise.
Dockable pane is a vital window that needs to support a complex application layout so that it can be shown or hidden any time to provide extra space
for your application desktop.
Idiom clarification
I'll use the word CMainFrame in this article to point to your derived class from
CFrameWndEx (or CMDIFrameWndEx), and I'll use the word 'pane' to refer to
CDockablePane implicitly.
And when I use CTreePane it means a derived class from CDockablePane that contains
a tree control as the main child and so CListPane etc.
Basic usage
Derive your own class
To add a dockable
pane to your project, the first step is to derive a new class from CDockablePane
and you must add two message handlers for OnCreate and
OnSize, and add a member child window as the main
content. Your simple CTreePane class should look like
this:
class CTreePane : public CDockablePane
{
DECLARE_MESSAGE_MAP()
DECLARE_DYNAMIC(CTreePane)
protected:
afx_msg int OnCreate(LPCREATESTRUCT lp);
afx_msg void OnSize(UINT nType,int cx,int cy);
private:
CTreeCtrl m_wndTree ;
};
And your OnCreate event handler should call the base implementation and create your child tree like this:
int CTreePane::OnCreate(LPCREATESTRUCT lp)
{
if(CDockablePane::OnCreate(lp)==-1)
return -1;
DWORD style = TVS_HASLINES|TVS_HASBUTTONS|TVS_LINESATROOT|
WS_CHILD|WS_VISIBLE|TVS_SHOWSELALWAYS | TVS_FULLROWSELECT;
CRect dump(0,0,0,0) ;
if(!m_wndTree.Create(style,dump,this,IDC_TREECTRL))
return -1;
return 0;
}
In the OnSize handler you should size your control to fill the entire dockable pane client area.
Failing to do so will make
you see what is underneath your pane before showing, because the dockable pane registers its window class with
a Shallow (Null) brush that erases the background, and for the very same
reason if you decide not to fill the entire client area you should handle OnPaint to draw the remaining client area.
void CTreePane::OnSize(UINT nType,int cx,int cy)
{
CDockablePane::OnSize(nType,cx,cy);
m_wndTree.SetWindowPos(NULL,0,0,cx,cy, SWP_NOACTIVATE|SWP_NOZORDER);
}
Preparing
pane in CFrameWnd
To support
a dockable pane in your frame you must first derive from the Ex family of frames
(CFrameWndEx, CMDIFrameWndEx, ..)
and in the OnCreate handler you should initialize the docking manager by setting
the allowable docking area, general properties, smart docking mode, …etc.
int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
...
CDockingManager::SetDockingMode(DT_SMART);
EnableAutoHidePanes(CBRS_ALIGN_ANY);
...
}
The next step is to add a pane member variable to the main frame and setting its default value to
null in the constructor and adding a command handler to the main frame to display
the pane.
void CMainFrame::OnTreePane()
{
if(m_treePane && m_treePane->GetSafeHwnd())
{
m_treePane->ShowPane(TRUE,FALSE,TRUE);
return ;
}
m_treePane = new CTreePane;
UINT style = WS_CHILD | CBRS_RIGHT |CBRS_FLOAT_MULTI;
CString strTitle = _T("Tree Pane");
if (!m_treePane->Create(strTitle, this,
CRect(0, 0, 200, 400),TRUE,IDC_TREE_PANE, style))
{
delete m_treePane;
m_treePane = NULL ;
return ;
}
m_treePane->EnableDocking(CBRS_ALIGN_ANY);
DockPane((CBasePane*)m_treePane,AFX_IDW_DOCKBAR_LEFT);
m_treePane->ShowPane(TRUE,FALSE,TRUE);
RecalcLayout();
}
Since the dockable pane is inherited from the control bar, you can apply all styles of control bars on
the dockable pane. Two interesting styles matter:
CBRS_FLOAT_MULTI make the dockable pane float as a unit when attached to
a tab
- An alignment style like
CBRS_LEFT gives the pane the initial alignment.
The DockPane function then docks your pane to the chosen side of your frame, to make your pane dock relative to each other's use:
m_treePane ->DockToWindow(m_listPane,CBRS_ALIGN_BOTTOM)
and to make your pane initially float, use the FloatPane function with
the screen coordinate rectangle:
m_treePane->FloatPane(rect);
And to make it initially auto hidden, use the ToggleAutoHide Function.
Tabbed pane is a concept of docking panes to each other to form a regular tab
control with individual panes inside. Applying some command
will affect all panes like Auto Hide, and others will affect only the active pane like Close.
To add your CListPane to a previously created CTreePane,
you only need to add another line:
CDockablePane* pTabbedBar = NULL;
if(m_listPane && m_listPane->GetSafeHwnd())
m_treePane->AttachToTabWnd(m_listPane, DM_SHOW, TRUE,&pTabbedBar);
To make your tab have Outlook style, pass AFX_CBRS_OUTLOOK_TABS as
the seventh argument to the Create function when creating your pane.
Destroying pane on Close
Closing a dockable pane from its caption bar only hides the pane and does not destroy it.
To destroy the pane when closing it, you have to add a handler to the pre-registered MFC message AFX_WM_ON_PRESS_CLOSE_BUTTON that
is sent from the dockable pane to its parent frame in the middle of its
OnLButtonDown message:
ON_REGISTERED_MESSAGE(AFX_WM_ON_PRESS_CLOSE_BUTTON,OnClosePane)
LRESULT CMainFrame::OnClosePane(WPARAM,LPARAM lp)
{
CBasePane* pane = (CBasePane*)lp;
int id = pane->GetDlgCtrlID();
pane->ShowPane(FALSE, FALSE, FALSE);
RemovePaneFromDockManager(pane,TRUE,TRUE,TRUE,NULL);
AdjustDockingLayout();
pane->PostMessage(WM_CLOSE);
PostMessage(WM_RESETMEMBER,id,0);
return (LRESULT)TRUE;}
First, we hide the pane and then we remove it from the docking manager, then we adjust
the docking layout of the frame. After that we post a WM_CLOSE message to
the pane and not send it because this
message is generated in the middle of OnLButtonDown and the handle must be valid to complete the message handler.
WM_CLOSE will generate a WM_DESTROY message to the destroy pane. After that I post my own register message
WM_RESETMEMBER to delete my member variable
and rest its value to NULL. And you should always return true to prevent closing because we
have already closed it, and
closing it will surprise CDockablePane when trying to hide the pane with
an invalid handle and will result in an exception and crash.
LRESULT CMainFrame::OnResetMember(WPARAM wp,LPARAM)
{
int id = (int)wp;
switch(id)
{
case IDC_TREE_PANE:
delete m_treePane;
m_treePane = NULL ;
break;
To prevent a pane from closing all together, just remove AFX_CBRS_CLOSE when you create it and it will
be destroyed when the parent frame is destroyed.
Command routing between CFrameWnd
and CDockablePane
Command routing is the concept of chaining different class' message maps together to enable
a non-window object to receive and handle a message (CWinApp
and CDocument
for example). That mechanism is controlled by a virtual function OnCmdMsg defined in
the class CCmdTarget. The function scans the
message map and returns true if it finds a handler for the command, otherwise false.
For example to disable all document commands just override it in the Document derived class and simply return
false without calling the parent implementation. Another usage is multiple inheritance to chain your message map to multiple parents and prioritize one parent handler over another.
The default command routing for an SDI frame:
- active view then attached document
- this frame object
- application object
By default a dockable pane doesn't receive commands from the mainframe. To add this functionality, follow these steps:
- Add a list of
CDockablePanes as member variable to your frame class:
CList<CBasePane*> m_regCmdMsg;
- Add two member functions to register and unregister the dockable pane as command target.
void CMainFrame::RegCmdMsg(CBasePane* pane)
void CMainFrame::UnregCmdMsg(CBasePane* pane)
A good place to call RegCmdMsg is before it is first displayed,
and for UnregCmdMsg, in the previous OnClosePane handler just before posting
the close message.
- Override
OnCmdMsg to route command to registered dockable pane first:
BOOL CMainFrame::OnCmdMsg(UINT id,int code , void *pExtra,AFX_CMDHANDLERINFO* pHandler)
{
POSITION pos = m_regCmdMsg.GetHeadPosition();
while (pos)
{
CBasePane* pane = m_regCmdMsg.GetAt(pos);
if(pane->IsVisible() &&
pane->OnCmdMsg(id,code,pExtra,pHandler))
return TRUE;
m_regCmdMsg.GetNext(pos);
}
return CFrameWndEx::OnCmdMsg(id,code,pExtra,pHandler);
}
- In frame
OnDestroy handler, remove all items in list.
Advance usage
ActiveX in CDockablePane
If dockable pane can contain a control, it can contain any child dialog or view and even ActiveX, in my example I implemented PdfPane that hosts Acrobat
ActiveX and receives the ID_FILE_OPEN command from the main frame to open and load a PDF file to
the control (note that the pane must be active to receive the command event and
Acrobat reader must be installed in your machine and after loading the file, you have to double click its client area to display the file; this defect comes from
the ActiveX developer not from me).
Splitter and Toolbar inside CDockablePane
Adding toolbar
Toolbar is designed to work as a child of a parent frame, because the default command routing
of a toolbar always routes commands to the first parent frame it can find. To override the default behavior, you have to derive
a new class and override two functions:
class CPaneToolBar : public CMFCToolBar
{
virtual void OnUpdateCmdUI(CFrameWnd*, BOOL bDisableIfNoHndler)
{
CMFCToolBar::OnUpdateCmdUI((CFrameWnd*)
GetOwner(),bDisableIfNoHndler);
}
virtual BOOL AllowShowOnList() const { return FALSE; }
};
The AllowShowOnList function prevents your toolbar from appearing in
the toolbar customization dialog and OnUpdateCmdUI will make the toolbar search for its command
update routine in the pane message map instead of the frame message map.
After adding a member variable to your toolbar in the pane class, you can create
it in OnCreate like this:
if(!m_toolbar.Create(this, AFX_DEFAULT_TOOLBAR_STYLE, IDR_TREETOOLBAR))
return -1;
m_toolbar.LoadToolbar(IDR_TREETOOLBAR);
m_toolbar.SetOwner(this);
m_toolbar.SetRouteCommandsViaFrame(FALSE);
Sizing the toolbar is a straightforward process:
void TreePane::OnSize(UINT type,int cx,int cy)
{
CDockablePane::OnSize(type, cx, cy);
int cyTlb = m_toolbar.CalcFixedLayout(FALSE, TRUE).cy;
CRect rectClient;
GetClientRect(rectClient);
m_toolbar.SetWindowPos(NULL, rectClient.left, rectClient.top,
rectClient.Width(), cyTlb,SWP_NOACTIVATE | SWP_NOZORDER);
m_tree.SetWindowPos(NULL,rectClient.left, rectClient.top + cyTlb,
rectClient.Width() , rectClient.Height() - cyTlb , SWP_NOZORDER | SWP_NOACTIVATE);
}
Adding a SpliteWnd to pane
A splitter is a window to divide a specific window to multiple sizable areas in columns and rows; like
a toolbar is designed to work with a mainframe, a splitter is designed to work
with views. To make it work with a regular control we have to extend it and add
a member function to create and add the window to split:
class CPaneSplitter : public CSplitterWndEx
{
public :
BOOL AddWindow(int row, int col, CWnd* pWin,CString clsName,
DWORD dwStyle,DWORD dwStyleEx, SIZE sizeInit);
};
BOOL CPaneSplitter::AddWindow(int row, int col, CWnd* pWnd ,
CString clsName , DWORD dwStyle,DWORD dwStyleEx, SIZE sizeInit)
{
m_pColInfo[col].nIdealSize = sizeInit.cx;
m_pRowInfo[row].nIdealSize = sizeInit.cy;
CRect rect(CPoint(0,0), sizeInit);
if(!pWnd->CreateEx(dwStyleEx,clsName,NULL,dwStyle,rect,this,IdFromRowCol(row, col)))
return FALSE;
return TRUE;
}
In my accompanying example, I have created a pane CSplitePane with
a shell list and a shell tree separated by the splitter window.
Common issue
Context menu problem

When you create
a dockable pane, an annoying context menu appears even if you right click on
the child client area. To make this menu disappear you have to override OnShowControlBarMenu
and return TRUE. To make it appear only when clicking on the caption bar use
the following code:
BOOL TreePane::OnShowControlBarMenu(CPoint pt)
{
CRect rc;
GetClientRect(&rc);
ClientToScreen(&rc);
if(rc.PtInRect(pt))
return TRUE; return CDockablePane::OnShowControlBarMenu(pt);
}
Smart docking mode

You might call CDockingManager::SetDockingMode(DT_SMART)
in the frame OnCreate handler to support
VS2005 docking style. The problem arises when your application supports a
different look (like Office2007 blue and black theme). When the application theme
changes, the docking mode automatically goes back to its default (DM_STANDARD).
This
happens in calling SetDefaultManager so after setting the new look you have to set
the docking mode to Smart again:
CMFCVisualManager::SetDefaultManager(RUNTIME_CLASS(CMFCVisualManagerOffice2007));
CDockingManager::SetDockingMode(DT_SMART);
Two classes contain the SetDockMode function, your pane and docking manager.
Setting the docking manager to standard and your pane to smart
will restrict your pane from docking to only four sides of your frame and will prevent docking to
another pane and will prevent floating by dragging.
Docking inside dialog
Creating a dockable pane as a child of a dialog is not permitted directly, but there
are two workarounds for this problem:
- Using a commercial product like Codejock library
that includes a special docking manager for dialogs.
- Create a frame window (without border, menu bar, or toolbar) as a child to your dialog and add
a dockable pane to that frame and form
view as the main content. The user will get the felling that the dockable pane is docked to
the dialog (see the frame dialog in the example code).
If advice matters, both approaches are highly discouraged. When using a dockable pane inside
a dialog, make the dialog complete cues and distract your user from the main question,
while
a dialog is meant to answer direct and simple questions to user. You should consider redesigning your task to multitask (remember, divide and concur).
Sometimes a complex dialog box is inevitable. I added to the example a very complex dialog (Phone Book) with five child dialogs to inspire you.
Docking inside a view
A solution like docking inside a dialog might work; it is not recommended and can be replaced by two approaches:
- Add a parent view and split it into a row and column of views, and when the main view gets closed all children views get closed.
- Use a regular view and in
OnInitialUpdate, create a new dockable pane with
a frame as parent and make interdependency between two objects through storing a reference
member variable. When closing either of the two, make the other know about it and update itself accordingly.
Pane and WM_GETMINMAXINFO
You can't restrict a pane max and minimum
size with this message like in a regular window. When I tried, the message handler never got called.
A pane
allows you to set only the minimum size by callinh SetMinSize(CSize(100,100)).
Your minimum size will be respected only when a pane isn't attached to the tabbed
pane.
Docking
to Desktop window
You can't create
a dockable pane based application like a dialog based application because dockable
pane expects a frame as parent (Ex family) which contains docking manager as a member
which is vital to a dockable pane to behave correctly.
Why do you need
to dock to Desktop?
Suppose you need
to display weather for user or an RSS feed, news headline, sport …
I followed the
following steps and I failed, I only list it because you might know better than
me and help me in fixing it in dash board below:
- Create an invisible window as parent of my frame
(
WS_POPUP | WS_EX_TOOLWINDOW) to prevent taskbar button.
- Create my frame and make it run initially in fullscreen mode and hide it.
- Try to dock my window to top right corner of my frame and oops, nothing shows up.
Alternative
solution that might work and I didn't try it:
- Create flat frame without border, menu, or
toolbar.
- Size it to occupy 1/4 desktop screen width and
set its position to the right side of the desktop.
- Create and display the docking pane and set its
size to occupy the entire frame area; when dockable pane resizes, resize your
frame accordingly, and vice versa.
- When you auto hide, shrink your frame to occupy only
the control bar width and the entire screen height.
- When closing your pane, close your frame with it.
Tips
- To disable loading of docking layout from the Registry,
make the following call in the frame constructor:
GetDockingManager()->DisableRestoreDockState(TRUE);
Always define your dockable pane destructor as virtual,
this will save a lot of debugging time and possible memory leaks.
To prevent your pane from docking by user, pass CBRS_NOALIGN to
the pane's EnableDocking
function .
Conclusion
The main reason
that pushes me to write this article was poor documentation of Feature Pack
Classes and this sentence in MSDN that makes me furious and angry:
"This topic is included for completeness. For more
details, see the source code located in the VC\atlmfc\src\mfc
folder of your Visual Studio installation."
I hope this
article will make a good reference for CDockablePane and I hope you enjoy it.
Final
word
The example is
designed with expansion in mind, don't hesitate to ask for examples or
declarations and I'll update the source code example as soon as I can.
References