Visual Studio.NET Style Tear Off Panes – Part II

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 second in the series.

Contents

Introduction

In Part I, we developed a framework for the inserting, moving, hiding, and unhiding of panes using public methods of XTearOffPaneManager class. In this article, we shall extend the class to add support for tear off tabs. If you have seen the Visual Studio.NET GUI, you would have noticed that most of the GUI are clubbed within tabs to better manage the real estate. Apart from that, the tabs can be individually “torn” off from their current host and dragged and dropped onto some other host. The tabs can be individually docked to different locations just as we did with panes in the last article.

Figure 1.

Goals

Let us start by looking at what goals we set for ourselves at the end of Part I.

  • 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

We’ll be looking at all of these except 4 and 6. Goals 4 and 6 will be taken up in subsequent articles.

Enter Tabs…

Figure 2.

Refer to Figure 2. I call the window marked in red a tab. As is normally done with a property sheet, the active tab can be changed by means of a tab control (shown marked in green). We need to add support for the following:

  • Ability to insert tabs into panes.
  • Ability to drag a button in the tabcontrol and dock it to any edge of the visible panes, effectively moving the tab.
  • Ability to drag a button in the tabcontrol and make it float, effectively making a tab floating.
  • Ability to double-click on a button in the tabcontrol and make it floating or docking, effectively toggling the state of the tab.
  • Ability to drag a pane hosting tabs and dock it to any edge of the visible panes, effectively moving the whole pane and all the tabs it hosts.
  • Ability to drag a pane hosting tabs and make it float.

Design

To realize the requirements above, I introduce what is called as a tab host. A pane, which we discussed in Part I, can either be a tab hosting pane or a non-tab hosting pane. When I say it is a tab hosting pane, it means that that pane actually hosts at least one tab. I do this for a convenience that is mostly dictated by the drag drop requirements. I will discuss this later. For simplicity, let us say, if a pane has tabs, like the one shown in Figure 2, it is a tab hosting pane; otherwise, it is not. Again, a given pane can be a floating one or a non-floating one. A floating pane is one that is contained in a mini framewindow. Non-floating panes are those contained in the main window.

A tab can have two panes associated with it: a floating pane or a non-floating pane. To start with, a tab has no floating pane associated. However, at all times, a tab always has a non-floating pane associated. Whenever a tab is made to float (on its own or as a result of floating a pane that hosts this tab), the tab gets associated with a floating pane. I maintain this pane information for the tabs in two separate maps in the XTearOffPaneManager class. This is for convenience and will be used when a tab is being toggled from a floating state to non floating and vice versa.

A tab can be hidden or visible. When a tab is hidden, I add this tab info into an array of hidden tabs in XTearOffPaneManager class. This info is again maintained this way for convenience. When there is a need to show the tab, XTearOffPaneManager fetches the hosting pane, the tab text and such, and inserts it to the pane and makes it visible.

How It Works

To insert tabs, we make use of the view class XTearOffPaneView that we introduced in the previous article. In this article, we will enhance the view class by adding a public method InsertTab. There are two overloaded versions of this method: one that takes a RUNTIME_CLASS information for the tab window class and one which takes a CWnd pointer of the tab window.

CWnd* XTearOffPaneView::InsertTab(const CString szText,
                                  const int nTabID,
                                  CWnd* pWndTab,HICON hIcon)
CWnd* XTearOffPaneView::InsertTab(const CString szText,
                                  const int nTabID,
                                  CRuntimeClass* pRuntimeClass,
                                  HICON hIcon)

Parameters

  • szText: Label to be used for the tab.
  • nTabID: ID of the tab. This must be unique.
  • pWndTab: CWnd pointer of the tab window to be inserted.
  • hIcon: Handle of icon to be placed alongside the label.
  • pRuntimeClass: Runtime class information of the tab window class to be inserted.

Return

NULL if failed to insert tab, the pointer to Tab window if successful

Remarks

This method can be used to insert a tab into a pane. Typically, this method will be called subsequent to an InsertPane call. XTearOffPaneManager’s InsertPane call, if successful, returns the pointer to the pane window. If the inserted pane is a tab hosting pane, i.e. it is a XTearOffPaneView object, one can call InsertTab to add tabs to this pane. Apart from this, this method will be called internally when a tab is being moved, made visible, made to float or dock, and so forth. There, we just met our Goal 1 requirement.

Drag and Drop at a Valid Insert Location

Now, let us move on to the other goals. To drag and drop tabs and panes, we still continue to use the XTearOffDragContext class, only enhanced significantly.

Dragging and dropping can be done on a tab button or on a pane caption bar. When the drag operation is started (by calling BeginDrag of the XTearOffDragContext class), the XTearOffDragContext saves the tab ID and the pane ID for the object being dragged. Let us call this the Start Tab ID (S.T.) and the Start Pane ID (S.P.), respectively. For example, if the drag operation starts on a tab host’s button, the S.P. is a valid value and the S.T. also is a valid value. If the drag operation is started by dragging the caption bar of a pane, the S.P. is a valid value, whereas a S.T. is not.

Similarly, at the end of the drag operation, the target tab and pane IDs are noted. For example, if the drag operation ends on a caption bar of another pane, the target pane ID (T.P.) is valid, whereas the target tab ID (T.T.) isn’t. Or, say, if a drag operation ends on a tab control, both T.P. and T.T. are valid. Again, if the drag operation ends neither on a caption nor a tab control, chances are the T.P. is valid. In that case, a further check has to be done for the insertAt value at the end of drag operation. The insertAt value indicates if the drop should be on to the right, left, top, or bottom of the target pane. If it returns an error, it means the result of the drag operation is a floating tab or pane. The source code will provide additional insight to the various conditions.

When a pane is dragged and dropped elsewhere, the steps followed are:

  • A new tab hosting pane is inserted at the target location.
  • Tabs are removed from the start pane and inserted into the new pane until there are no more tabs left in the start pane.
  • The resulting operation if makes a pane have no tabs, the pane is made hidden.

When a tab is dragged and dropped elsewhere, the steps followed are:

  • If it is being dropped on a caption bar of another tab hosting pane or on a tab control of a tab hosting pane, the tab is removed from the start pane and inserted into the target pane.
  • If it is not the case above, but the drop point is still within a valid insert location, a new tab hosting pane is inserted at the insert location and the tab is removed from the start pane and inserted into the new pane.

Drag and Drop at a Valid Insert Location (Floating)

To make a tab or a pane float, XTearOffPaneManager exposes two public methods: FloatPane and FloatTab. These methods are called internally by the framework during a drag and drop operation. However, if needed, the client of XTearOffPaneManager object can make use of this call, too. To make a pane or a tab float, the insertAt value at the end of drag operation has to be PANE_ERR. This indicates that the tab or pane is being dropped onto neither a pane nor a tab. Hence, it is to be made floating.

When a pane is made to float, the steps followed are:

  • A new mini frame window is created.
  • A new tab hosting pane is inserted as the root pane for this frame.
  • tabs are removed from the start pane and inserted into the new pane till there are no more tabs left in the start pane.

When a tab is made to float, the steps followed are:

  • A new mini frame window is created.
  • A new tab hosting pane is inserted as the root pane for this frame.
  • The tabs are removed from the start pane and inserted into the new pane.

Double-Clicking on a Caption Bar or on a Tab Button (Toggle Floating)

When the user double clicks on the caption bar of a tab hosting pane, the following has to happen. If it happens on a non-floating pane, each of the tabs in the pane should be inserted into its floating pane if one exists in the floating pane ID map for the given tab. If no floating pane ID exists, a new framewindow and a new tab hosting pane are created and the tab is removed from the non-floating pane and inserted into the new floating pane. Else, if the double-click happens on a floating pane, each of the tabs in the pane should be inserted into the non-floating pane associated with the tab.

When the user double-clicks on a tab button of a tab hosting pane, the following has to happen. If it happens on a non-floating tab, the tab should be inserted into its floating pane if one exists in the floating pane ID map for the given tab. If no floating pane ID exists, a new framewindow and a new tab hosting pane are created and the tab is removed from the non-floating pane and inserted into the new floating pane. Else, if the double-click happens on a floating tab, the tab in the pane should be inserted into the non-floating pane associated with the tab. If the frame is not visible, it is made visible.

The above operations are done by XTearOffPaneManager’s TogglePane and ToggleTab methods.

Extras

In this article, I introduce two more helper classes:

  • XFlatTabCtrl: This implements a simple tab control kind of functionality with a look similar to that found in .NET-style panes. This class handles the toggling of the tab by trapping WM_LBUTTONDBLCLK message.
  • XTearOffMiniFrameWnd: This implements a simple miniframe window to handle the behavior of close operation. It handles a WM_CLOSE message to send a message to all descendant windows with a user-defined message so that the panes get a chance to hide all their tabs.

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, XTearOffMiniFrameWnd.cpp/.h, XFlatTabCtrl.cpp/.h, XTearOffPaneView.cpp/.h, and XTearOffDragContext.cpp/.h in 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()
    {
      //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);
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Third"),3,
                           RUNTIME_CLASS(CEditView)));
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Fourth"),4,
                           RUNTIME_CLASS(CTreeView)));
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Fifth"),5,
                           RUNTIME_CLASS(CEditView)));
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Sixth"),6,
                           RUNTIME_CLASS(CTreeView)));
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Seventh"),7,
                           RUNTIME_CLASS(CTreeView)));
      GetActiveDocument()->AddView((CView*)pRootPane
                         ->InsertTab(_T("Eighth"),8,
                           RUNTIME_CLASS(CTreeView)));
    
      //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);
    
      //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"));
    
      //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);
    
      //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);
      GetActiveDocument()->AddView((CView*)pTearOffView
                         ->InsertTab(_T("First"),1,
                           RUNTIME_CLASS(CEditView)));
      GetActiveDocument()->AddView((CView*)pTearOffView
                         ->InsertTab(_T("Second"),2,
                           RUNTIME_CLASS(CTreeView)));
    
      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:

  • Auto hide enabling of tabs.
  • Notification of events.
  • Bug fixes and enhancements.
  • For the sake of documenting, here are some bugs/enhancements that I have noted down, but haven’t attempted to address, just because of time contraints and an eagerness to have the core functionality up and running.

    • Problem with floating panes. In some cases, when there are multiple floating frame windows, a floating frame window doesn’t come to the top when clicked and activated.
    • At times, when a pane is dragged and dropped, the resulting pane size is kind of constricted.
    • Currently, it is not possible to make a non-tab hosting pane floating.
    • When a framewindow is no longer needed—for example, all panes have been moved to some other frame—the window is still lying around. Need to optimize this.
    • During the drag operation, the drag display is not exactly WYSIWYG when the drag is happening over the caption bar of a tab hosting pane.
    • It is not possible to drag and drop a tab within the same pane, in effect, repositioning the tab within the same pane.
    • Panes with tabs appearing at the top, basically a XTearOffPaneView derivative with a tab control at top rather than in the bottom.
    • Use a registered message instead of a (WM_USER + x) message I use right now for WM_CLOSE_FRAME.

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read