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:
CList<CBasePane*> m_regCmdMsg;
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.
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);
}
- Add a list of
CDockablePane
s as member variable to your frame class: - Add two member functions to register and unregister the dockable pane as command target.
- Override
OnCmdMsg
to route command to registered dockable pane first: - In frame
OnDestroy
handler, remove all items in list.
Advanced 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
GetDockingManager()->DisableRestoreDockState(TRUE);
- To disable loading of docking layout from the Registry, make the following call in the frame constructor:
- 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