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
- 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.
- 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.
- 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.
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.
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.
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;
...
};
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(); }
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