Visual Studio.NET Style Tear Off Panes - Part I

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


Downloads

Comments

  • Problem with MDI project

    Posted by werner.keilholz on 06/28/2006 09:36am

    Hi, I could not get this to work with my MDI project: InitializePanes() adds the current View to the pane manager, using GetActiveView(). But in MDI projects, GetActiveView() returns NULL (there is no current View in MDI applications, ). Any ideas would be appreciated... Thanks, Werner

    • Got it to work...

      Posted by werner.keilholz on 07/20/2006 10:08am

      I finally found a solution that does it all (including MDI) - in case anyone else has the problem, the solution is at http://www.codeproject.com/docking/sizecbar.asp or http://www.datamekanix.com/ (the samples are SDI, but MDI works fine also by just following the instructions). Thanks Cristi Posea ! Werner

      Reply
    • Re: alternative

      Posted by werner.keilholz on 07/05/2006 06:43am

      Sorry, I don't see the slider in this example ... ? (I cannot change the size of the bar added on the left). I need the thing to be resizable (hence the need foir a slider), and add a text edit to it. You're right, the goal is to have the slider available all the time, no matter if there is a child frame/current document.

      Reply
    • alternative

      Posted by kirants on 07/04/2006 12:36pm

      From what you say, it sounds like you want to have the slider to be available for the mainframe all the time, whether a child frame is there or not. In that case, tearOff panes is overkill. I would suggest, you explore the idea of hosting a docking bar in the mainframe. Please look at this thread: http://www.codeguru.com/forum/showpost.php?p=1015768&postcount=4 The idea here is to show a dockbar at the left. Within this dockbar you can create any child control you want. Is this suitable ?

      Reply
    • GetActiveDocument in MDI

      Posted by werner.keilholz on 07/04/2006 04:23am

      Thanks a lot for your answer, I appreciate your help. My goal is to add a slider to the bottom of an MDI application, like the output window in the Developer Studio. I tried to follow the steps in the "How to Use the Framework" section (a great article by the way !), both adding the code to ChildFrm and CMainFrame. But in both cases, InitializePanes() fails, because it uses GetActiveDocument() to add a view. (In MDI applications, no active document exists upon startup, so GetActiveDocument returns NULL). Also, wouldn't adding the code to ChildFrm split each document window, rather than add a window at the bottom of the main frame ? I also looked at chapter 4, which is exactly what I need except that it is SDI. I tried to compile the AppWizard (hoping for a SDI/MDI option in it), but bldguid.h seems to be missing. (I don't know how to add the .awx file to my .NET 2003 application but I think it was made for an earlier version). Thanks again for your ideas ! Werner

      Reply
    • Not sure what you are attempting

      Posted by kirants on 06/29/2006 12:27am

      Hi, thanks for posting your concern. Am not sure what you are attempting though. If you could explain in more detail, probably I could suggest something. Have you checked the "How to Use the Framework" section ? Herein, I suggest what you need to do. In case of MDI, you need to do all this in the ChildFrm class and not the mainframe class. In the meantime, have you checked Part IV of the series and seen if that fits your needs ?

      Reply
    Reply
  • Not pre-formatted

    Posted by kirants on 08/29/2005 01:16pm

    First line. Second line. Third paragraph. This is a long line which I am typing in here to see if the text wraps favorably well after having submitted as comment. My guess is this will appear without the carriage returns.

    • Pre formatted

      Posted by kirants on 08/29/2005 01:20pm

      First line.
      
      Second line.
      
      Third paragraph. This is a long line which I am typing in here to see if the text wraps favorably well after having submitted as comment. My guess is this will appear without the carriage returns.

      Reply
    Reply
  • cannot download demo and sources

    Posted by palani_vs on 09/04/2004 12:11am

    hi can anyone help me to download the files. palani vs

    • I see no problem

      Posted by kirants on 09/07/2004 11:31am

      Hi there, I have no problem downloading the zip files. What kind of problem are you facing ? Do you want me to email those files to you ? PM me the the email ID if I should.

      Reply
    Reply
  • NICE !

    Posted by diego.moita on 05/10/2004 10:09pm

    Very interesting. I gave it a rate of "Excelent" and will use it in my code. You said you'll add floating panes to the next version. I think it is the same as overlaped window, right? That would be very cool. When will the next version be available?

    • Thanks

      Posted by kirants on 05/11/2004 11:03am

      Glad that it is useful for you in some way. I'm not sure when I'd be ready for the next article, but I'm working on it already, Also, not sure if I'd be able to add floating panes ( yes, an overlapped window ) in the next one. Maybe I still can. It's a little difficult to say since I have to do all this in whatever time is available after work. Anyways, stay tuned !!!

      Reply
    Reply
  • pretty cool~

    Posted by xufeisjtu on 05/06/2004 07:09am

    cool article~

    • Thank you..

      Posted by kirants on 05/06/2004 11:30am

      Please let me know of issues you find or recommendation. I may be able to incorporate them in the follow up article.

      Reply
    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • With JRebel, developers get to see their code changes immediately, fine-tune their code with incremental changes, debug, explore and deploy their code with ease (both locally and remotely), and ultimately spend more time coding instead of waiting for the dreaded application redeploy to finish. Every time a developer tests a code change it takes minutes to build and deploy the application. JRebel keeps the app server running at all times, so testing is instantaneous and interactive.

  • You probably have several goals for your patient portal of choice. Is "community" one of them? With a bevy of vendors offering portal solutions, it can be challenging for a hospital to know where to start. Fortunately, YourCareCommunity helps ease the decision-making process. Read this white paper to learn more. "3 Ways Clinicians can Leverage a Patient Portal to Craft a Healthcare Community" is a published document owned by www.medhost.com

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds