Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Understanding CDockablePane

0.00/5 (No votes)
19 Aug 2015 4  
A good reference for CDockablePane

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:

// make list pane attach to treepane if it's already created
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;//prevent close , we already close it
}

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:

  1. active view then attached document
  2. this frame object
  3. 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;
//register pane as command target
void CMainFrame::RegCmdMsg(CBasePane* pane)
//remove pane from command target list
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)
{
  //route cmd first to registered dockable pane
  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);
}
  1. Add a list of CDockablePanes as member variable to your frame class:
  2. Add two member functions to register and unregister the dockable pane as command target.
  3. Override OnCmdMsg to route command to registered dockable pane first:
  4. 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);
// All commands will be routed via this control ,
// not via the parent frame:
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;//hide a pane contextmenu on client rea
  //show on caption bar
  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

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here