Visual Studio.NET Style Tear Off Panes – Part I

CodeGuru content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

This article discusses a framework that can be used to implement Visual Studio.NET style Tear Off panes. The framework uses the commonly used MFC classes to achieve the functionality. This article is the first in the series in which I intend to develop the framework in a step-by-step fashion.

Contents

Introduction

The Visual Studio.NET style framework for dropping/docking/tearing off panes and with support for auto hide features is becoming increasingly common. The framework allows for a better usage of the screen real estate and gives the user a wide set of options to maximize the use of this real estate based on one’s convenience.

Figure 1.

Some amount of playing around with it made me realize that this calls for a neat framework. My goal was to realize it using the familiar MFC framework as the backbone but not relying too heavily on it. I have tried to use standard MFC controls as much as possible.

In this first article, we shall take a look at how the backbone for the framework is developed. Some features, such as auto hide, sliding panes, and floating panes will be avoided in this article; I plan to tackle these and other issues in a follow-up article.

The features that will be implemented in this article are:

  • Nested pane structure.
  • Repositioning of panes.
  • Show/Hide panes.
  • Drag’n’drop of panes with feedback about new position.

The FrameWork

Goals

Let us tackle these one by one by trying to list the goals the framework needs to achieve.

  • Should be easily pluggable into a SDI/MDI framework.
  • Should be able to accomodate normal view classes for the panes.
  • Should allow dragging and dropping.
  • Should allow hiding/unhiding of panes.

Design

Figure 2.

The design is pretty straightforward. The framework uses a class, called XTearOffPaneManager, to manage the panes. XTearOffPaneManager holds an array of panes. If you look at Figure 2, all the square blocks represent the individual panes. The blocks are either leaf nodes or have child nodes. XTearOffPaneManager uses a structure called XTearOffPaneInfo to keep information about these panes. Some of the attributes of the structure specify what alignment is allowed on the pane, if it is visible or not, the CWnd object pointer of the pane window, and a link to child nodes and parent nodes, if any. The red colored blocks are of the XTearOffSplitterWnd class that derives from CSplitterWnd and the blue colored blocks are of any class specified by the user when creating the panes.

Figure 3.

To insert a new pane aligned to the left of say, pane 1, the following steps are followed (refer to Figure 3):

  • A new splitterWnd (D) is created; that means a new red block.
  • Pane 1 is moved as a child of this splitterWnd.
  • The new pane (5) is attached as the other child of this splitterWnd.
  • The splitterWnd replaces the node for the initial parent (A) of Pane 1.

To move a pane to another location say, to move pane 1 to position to the left of pane 4, the following steps are followed:

  • The parent of pane 1, i.e. pane A, replaces pane 4.
  • Pane 4 is moved as a child of pane A.
  • The previous sibling of pane 1, which was pane B, becomes the child of pane A’s parent node if one existed.

To hide a pane, the steps followed are:

  • The pane is first hidden.
  • If as a result, no more panes in the splitter are visible, the splitter itself needs to be hidden. This process needs to be done all the way up.

Notes

  1. The pane inserted as the root pane can never be hidden. To show a pane, a similar logic is followed. For example, if a pane is made visible and its parent node is not visible, it is made visible too and so on all the way up.
  2. The parent child relationship exists at two levels, on the node level and on the window level. Whenever the parent child relationship among the nodes changes, a change in the parent-child relationship at the window level also needs to be changed. So, one can see explicit calls to SetParent in the code that implements the repositioning.
  3. Just as the window relationship is important, the concept of the control ID for the panes is important too. Having a proper control ID for the panes(here I do not mean the panes but the individual panes of the splitter window) is central to the proper functioning of the CSplitterWnd class. Hence, there are methods such as RecalcDialogIDs() that assist in properly calculating the IDs.

Essential components

Figure 4.

Below is a breakup of the components of this framework. We shall discuss some of these one by one

XTearOffPaneManager

The heart of the framework, it holds an array of panes. This is the public interface for achieving the goals aforementioned. All possible operations are exposed as public methods of this class. The goals of the framework translates to calls to public methods of this class. The development of the framework took place in the order in which the goals are listed.

  • Goal 1—Should be easily pluggable into a SDI/MDI framework.
  • Application of such a framework applies mostly to SDI/MDI apps. The framework initialization starts with adding a root pane, using the class method.

    CWnd* InsertRootPane(CWnd* pPaneToInsert,
                         const int nPaneID,
                         const DWORD dwAlignFlags)
    

    Parameters

    pPaneToInsert—pointer to pane to insert
    nPaneID—ID of the pane to insert
    dwAlignFlags—allowed alignments for any drop onto this pane

    Return

    Pointer to the pane inserted if successful, NULL otherwise.

    Remarks

    In a normal SDI/MDI case, pPaneToInsert would be the view associated with the SDI/MDI window. The pane added as root pane cannot be hidden. Hence, it is important to ensure that you specify the right one here. No matter what panes you hide and show, this one is always visible. It’s similar to an SDI app always having at least one view. All other panes that are inserted henceforth will be inserted into or around this pane. This is the first step one would have to do to use the panemanager. nPaneID is the ID you want to associate with the pane. Additionally, one can specify what are the valid align flags that this pane allows. For example, if you would want this pane to allow other panes to be dragged onto its left edge, you could specify TEAROFF_ALIGN_LEFT as the third parameter. The default is TEAROFF_ALIGN_ANY, which means that it allows panes to be dropped into any of the four edges.

  • Goal 2—Should be able to accomodate normal view classes for the panes.
  • To achieve this, there are two methods that allow inserting panes. One takes a runtime class info as parameter, in which case the XTearOffPaneManager class will be responsible for creating the window, and another that directly takes a CWnd* as a parameter.

    CWnd* InsertPane(const int nRefPane,
                     const XTearOffInsertLocation eInsertAt,
                     CWnd* pPaneToInsert,const int nPaneID,
                     const DWORD dwAlignFlags =
                           TEAROFF_ALIGN_ANY);
    
    CWnd* InsertPane(const int nRefPane,
                     const XTearOffInsertLocation eInsertAt,
                     CRuntimeClass* pRuntimeClass,const int
                     nPaneID, const DWORD dwAlignFlags =
                              TEAROFF_ALIGN_ANY);
    

    Parameters

    nRefPane—This is the ID of the target pane in which the new pane will be inserted
    eInsertAt—This is the location in the refPane where the new pane has to be positioned
    pPaneToInsert—The new pane to be inserted
    nPaneID—The pane ID for the new pane
    dwAlignFlags—The align flags for the new pane
    pRuntimeClass—Run time class of new pane to be inserted

    Return

    Pointer to the pane inserted if successful, NULL otherwise.

    Remarks

    This method is used subsequently to insert additional panes. There are two overloaded methods, one that takes a CWnd* pointer and one that takes a RUNTIME_CLASS info. In either case, you need to specify the pane into which you want to drop and the location where you would want to insert it. If, for some reason, the nPaneID specified has already been added, or if the reference pane does not allow inserting at the specified location, the method returns NULL.

  • Goal 3—Should allow dragging and dropping of panes.
  • The actual mechanism of a drag/drop operation is implemented in the XTearOffDragContext class. However, the XTearOffPaneViewManager class provides a public method for the final step of the drag drop/operation, which is the repostioning of the panes following the drag operation. To achieve this, XTearOffViewManager exposes a public method MovePane. The XTearOffDragContext class uses this method for producing the final result.

    BOOL MovePane(const int nPaneToMove,const int nTargetPane,
                  const XTearOffInsertLocation eInsertAt)
    

    Parameters

    nPaneToMove—Pane to be moved to new location
    nTargetPane—Drop target for the pane
    eInsertAt—Location where to position the pane to insert

    Return

    TRUE if move successful, FALSE otherwise.

    Remarks

    Used to move a pane from one position to the other.

  • Goal 4—Should allow hiding/unhiding of panes.
  • The framework should allow hiding and unhiding of panes to allow the user to hide the pane when not needed and unhide it when required. XTearOffPaneViewManager exposes a public method, ShowPane, to achieve this.

    BOOL ShowPane(const int nPane, const BOOL bShow)

    Parameters

    nPane—ID of pane to show/hide
    bShow—FALSE to hide, show otherwise

    Return

    TRUE if move successful, FALSE otherwise.

    Remarks

    Used to show or hide a pane. Note that this method fails on the root pane which is always visible.

XTearOffDragContext

This is a class that is used to implement the drag and drop operation for the panes. The code for this class has been lifted from MFC’s CDockContext class, only made a little simpler. The only hook provided is in the WM_MOUSEMOVE handler and WM_LBUTTONUP handlers, wherein the XTearOffDragContext keeps querying the XTearOffPaneManager class for the draw rect to give proper feedback to the user of the result of the drag operation. Here, two helper methods of XTearOffPaneManager, GetPaneIDFromPoint() and GetInsertLocation(), give the necessary information for XTearOffDragContext to draw the proper drag rectangle. The XTearOffPaneManager, given the screen coords of the point, checks whether the point lies within any of the visible panes and whether it allows drop. If it does, it returns the drop rectangle back to the caller. On LBUTTONUP, the DragContext class will call the MovePane method to finally execute the move.

XTearOffSplitterWnd

This is derived from CSplitterWnd and provides a few helper methods to change the row count and column count. Note that, in this framework, a visible splitter can have just one pane (the other one being hidden).

XTearOffPaneView

This is derived from CView and demonstrates the use of the XTearOffDragContext class to implement drag and drop. This view class will be later enhanced to add support to host tabs and to support tearing off of individual tabs and dropping them on other panes. This will be implemented in the follow-up article. These are some things that need to be done to support dragging of your own view class.

  • Add a XTearOffDragContext member to the class
  • Add a XTearOffPaneManager* member to the class and set this member to the PaneManager object pointer that is used by the application.
  • We need to initiate the drag somehow. For this, XTearOffPaneView traps WM_NCLBUTTONDOWN for HTCAPTION case, and calls StartDrag on the dragcontext member. The dragging and dropping is taken care of by the context class henceforth.
  • Additionally, XTearOffPaneView also traps WM_NCLBUTTOWN for HTCLOSE case, to Hide the pane window if clicked on the close button.

How to Use the Framework

This framework is suited for SDI or MDI applications. A typical way to use it would be:

  • Include the files XTearOffPaneManager.cpp/.h, XTearOffSplitterWnd.cpp/.h, XTearOffPaneView.cpp/.h, and XTearOffDragContext.cpp/.h to your project.

  • Add a XTearOffPaneManager object to the mainframe or childframe class.
  • class CMainFrame : public CFrameWnd
    {
    
    protected:    // create from serialization only
       CMainFrame();
       DECLARE_DYNCREATE(CMainFrame)
       XTearOffPaneManager m_oPaneManager;
    ...
    };
    
  • Add a method to the MainFrame/ChildFrame class to initialize the pane structure you want and call it say, InitializePanes().
  • class CMainFrame : public CFrameWnd
    {
    
    protected:    // create from serialization only
       CMainFrame();
       DECLARE_DYNCREATE(CMainFrame)
    
    // Attributes
    public:
    
    // Operations
    public:
      void InitializePanes();
    .....
    };
    
    void CMainFrame::InitializePanes()
    {
      CString szListText;
      //1. Root pane
      //Insert the root pane. This always the first step
      //Root pane ID = 1. The active view is of XTearOffPaneView
      //class
      XTearOffPaneView* pRootPane =
           (XTearOffPaneView*)m_oPaneManager.InsertRootPane
           (GetActiveView(),1);
      ASSERT(pRootPane);
      //to allow dragging on this pane, we need to set the pane
      //manager to the XTearOffPaneView object
      pRootPane->SetPaneManager(&m_oPaneManager);
      szListText.Format(_T("Pane ID : %d
            Allows :  TEAROFF_ALIGN_ANY
            Class : XTearOffPaneView"),1);
      m_oPage.m_oPaneListItems.Add(szListText);
    
      //2. Add a CEditView now, for e.g. let us align it to left of
      //root pane i.e. pane 1
      CEditView* pEditView =
           (CEditView*)m_oPaneManager.InsertPane
           (1,XTearOffInsertAtTop,RUNTIME_CLASS
           (CEditView),2);
      ASSERT(pEditView);
      GetActiveDocument()->AddView((CView*)pEditView);
      //let us populate some data into it now
      pEditView->GetEditCtrl().SetWindowText(_T("Sample edit text"));
      szListText.Format(_T("Pane ID : %d
            Allows : TEAROFF_ALIGN_ANY
            Class : CEditView"),2);
      m_oPage.m_oPaneListItems.Add(szListText);
    
      //3. Add a CListView now, for e.g. let us align it to right
      //of edit view pane i.e. pane 2
      CListView* pListView =
        (CListView*)m_oPaneManager.InsertPane
        (2,XTearOffInsertAtRight,RUNTIME_CLASS(CListView),3);
      ASSERT(pListView);
      GetActiveDocument()->AddView((CView*)pListView);
      //let us populate some data into it now
      pListView->GetListCtrl().ModifyStyle(0,LVS_REPORT);
        pListView->GetListCtrl().InsertColumn(0,
                   _T("Column header"),LVCFMT_LEFT,100);
        pListView->GetListCtrl().InsertItem(0,
                   _T("first list item"));
        pListView->GetListCtrl().InsertItem(0,
                   _T("second list item"));
      szListText.Format(_T("Pane ID : %d
            Allows : TEAROFF_ALIGN_ANY
            Class : CListView"),3);
        m_oPage.m_oPaneListItems.Add(szListText);
    
      //4. Add a CTreeView now, for e.g. let us align it to the
      //bottom of the edit view pane i.e. pane 2
      CTreeView* pTreeView =
        (CTreeView*)m_oPaneManager.InsertPane
        (2,XTearOffInsertAtBottom,RUNTIME_CLASS(CTreeView),4);
      ASSERT(pTreeView);
      GetActiveDocument()->AddView((CView*)pTreeView);
      HTREEITEM hItem = pTreeView->
                        GetTreeCtrl().InsertItem(_T("Root"));
        pTreeView->GetTreeCtrl().InsertItem(_T("First child"),
                                            hItem);
        pTreeView->GetTreeCtrl().InsertItem(_T("Second child"),
                                            hItem);
        pTreeView->GetTreeCtrl().Expand(hItem,TVE_EXPAND);
      szListText.Format(_T("Pane ID : %d
            Allows : TEAROFF_ALIGN_ANY
            Class : CTreeView"),4);
        m_oPage.m_oPaneListItems.Add(szListText);
    
      //5. Add another XTearOffPaneView now, which supports
      //dragging, for e.g. let us align it to the bottom of the
      //tree view pane i.e pane 4
      XTearOffPaneView* pTearOffView =
        (XTearOffPaneView*)m_oPaneManager.InsertPane
        (4,XTearOffInsertAtLeft,RUNTIME_CLASS(XTearOffPaneView),5);
      ASSERT(pTearOffView);
      GetActiveDocument()->AddView((CView*)pTearOffView);
      pTearOffView->SetPaneManager(&m_oPaneManager);
      szListText.Format(_T("Pane ID : %d
            Allows : TEAROFF_ALIGN_ANY
            Class : XTearOffPaneView"),5);
        m_oPage.m_oPaneListItems.Add(szListText);
    
      RecalcLayout();
    }
    
  • At some point when the mainframe, view, and so forth have been created, for example, just before exiting from the Winapp class’ InitInstance, call the method InitializePanes().
  • BOOL CTearOffPanesApp::InitInstance()
    {
    ................
    ................
        // The one and only window has been initialized, so show
        // and update it.
        m_pMainWnd->ShowWindow(SW_SHOW);
        m_pMainWnd->UpdateWindow();
    
      ((CMainFrame*)m_pMainWnd)->InitializePanes();
    
        return TRUE;
    }
    

About the Included Demo

I have included a demo application demonstrating the use of the methods exposed by XTearOffPaneManager class. It is an SDI application. In addition to the initial layout, you can add your own panes, show/hide them, and execute each of the XTearOffPaneManager’s methods by using the Pane Manager -> Manage panes menu. A dialog is presented with a list of three methods and an execute button for each. On pressing execute, the corresponding method is called with the parameters shown in the GUI. This can be used as a unit test tool. The code PaneManipulationPage class will show a typical way of calling these APIs.

What’s Next

In the next article I wish to address some of these issues:

  • Bug fixes
  • Addition of tabs to the panes
  • Drag and drop tabs within and across panes
  • Hide/Show at tab level
  • Auto hide enabling of tabs
  • Floating panes
  • Notification of events

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read