Tabbed View Interface in an MFC Doc View Application






4.97/5 (16 votes)
Create a tabbed view interface in an MFC based Doc View application.
Introduction
This article is about creating multiple views under a single tab, which can be seen in IE-8, Google Chrome, or MS-DEV 2005 type applications with special tiling option.
The purpose of this article is to show how easily you can incorporate a tabbed interface to an existing MFC doc view project.
Background
In the changing times of Graphical User Interfaces, almost all application are opting for tabbed view interfaces. Similarly, the product that I worked on required a face change, and that is give the cool tabbed interface look to the existing MFC doc/view application. I know this type of work has been done before, but couldn't find a complete solution for multiple doc/view in the Internet when I started developing it.
Basic Concept
The basic idea behind implementing a tabbed interface is fairly easy. First, you subclass the MDIClient
inside the main frame. Then create a container as a child of the MDI client, which will hold the tab control, to be shown at the top of the views. Next, you handle OnSizeParent
(e.g., LRESULT HsTabContainer::OnSizeParent(WPARAM, LPARAM lParam)
) of the container, and accurately calculate the layout, and place the container in the MDI client area. During the sizing of the container, resize the child tab control as well.
Another thing to remember is to notify the tab control about a new view created, or a view destroyed, or a view being set active through notification messages, or through an interface like it is done in this case. This needs to be done to update the UI state of the tab control. Also, any selection change in the tab control should result in setting focus to the view, which the user selects as the active one in the tab.
Using the Code
In order to transform your application to the tabbed view interface, the first step is to subclass the MDI client window. The following code can be placed in the OnCreate
method of the CMainFrame
class after all other initialization is done.
ASSERT(m_hWndMDIClient);
m_wndMDIClient.SubclassWindow(m_hWndMDIClient);
Then you create a tab bar (a CWnd
derived custom class containing a custom tab control as a child inside) for the tabbed views.
CRect rc(0, 0, 0, 0);
m_wndMDIClient.GetTabBar()->Create(NULL, NULL,
WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN,
rc, this, ID_HSTABCONTAINER_DEFAULT);
Next, place the tab bar to the bottom of the z order and then place the MDI client window after that.
// manipulate Z-order so, that our tabbar is above the mdi client, but below any status bar
::SetWindowPos(m_wndMDIClient.GetTabBar()->m_hWnd, HWND_BOTTOM,
0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
::SetWindowPos(m_hWndMDIClient, m_wndMDIClient.GetTabBar()->m_hWnd,
0, 0, 0, 0, SWP_NOMOVE|SWP_NOSIZE);
The tab control inside the tab bar is also a CWnd
derived control, although a normal CTabCtrl
derived class could have been used with extra notification handlers.
Here, the tab control is derived from scratch just to give that flat look.
The following notification handlers of the tab control inside the tab bar needs to be called by the views whenever a view is added, deleted, or the active state changes, so that the tab bar can properly update its UI.
void NotifyNewViewAdded(CView* pNewView);
void NotifyViewDeleted(UINT uIndex);
void NotifyActiveViewChanged(CView* pActiveView, bool bRepaint = TRUE);
For example:
void CTabbedViewPrjView::OnSetFocus(CWnd* pOldWnd)
{
CView::OnSetFocus(pOldWnd);
HsMDIClient* pMDIClient = ((CMainFrame*)AfxGetMainWnd())->GetMDIClient();
pMDIClient->GetTabBar()->GetTabCtrl()->NotifyActiveViewChanged(this);
...
}
Similarly, you can call NotifyNewViewAdded
in the OnInitialUpdate
of the View, and NotifyViewDeleted
in OnDestroy()
of the view.
The interesting part of the drawing of the tab control is in the DrawTab
method. You can see from the following code, pDC->PolyLine
is used to draw the flat looking tabs:
/*----------------------------------------------------------------------
Name: DrawTab()
Type: Function
Purpose: Draw all the tabs
Param: pDC = DC that the tabs should be drawn on.
*/
void HsTabCtrl::DrawTab( CDC* pDC )
{
UINT uTabIndex; // tab that are drawn
//POINT ppoint[4]; // points that will be used to draw tab
POINT ppoint[20]; // points that will be used to draw tab
UINT uTabHeight; // the tab height
UINT uDistanceToNextTab; // distance between tabs
int iOffsetToLeft; // distance from left side of client edge
CRect rect;
CPen penBlack( PS_SOLID, 1, RGB(0,0,0) );
CPen* ppenOld;
if( m_bTabModified ) UpdateTabWidth( pDC ); // update tab ?
ppenOld = pDC->SelectObject( &penBlack ); // select a black pen
pDC->SetBkMode( TRANSPARENT ); // just text
GetClientRect( &rect ); // get client rect
//pDC->MoveTo( rect.left, rect.top ); // move to upper left
//pDC->LineTo( rect.right, rect.top ); // draw a line from left to right
pDC->MoveTo( rect.left, rect.bottom - 1 ); // move to upper left
pDC->LineTo( rect.right, rect.bottom - 1 ); // draw a line from left to right
uTabHeight = m_uHeight - 1;
uDistanceToNextTab = uTabHeight / 2;
iOffsetToLeft = m_iOffsetToLeft;
for( uTabIndex = 0; uTabIndex < (UINT)m_dwarrayTabWidth.GetSize(); uTabIndex++ )
{
// ***
// set all points for tab, then we will be able to draw it
// ***
int num_of_points = 6;
ppoint[0].x = iOffsetToLeft; // ""
ppoint[0].y = 0;
int init_x = iOffsetToLeft;
int init_y = 0;
//ppoint[5].x = iOffsetToLeft; // ""
//ppoint[5].y = 0;
//iOffsetToLeft -= uDistanceToNextTab; // "/"
ppoint[1].x = iOffsetToLeft - uDistanceToNextTab;
ppoint[1].y = uTabHeight / 2;
ppoint[2].x = iOffsetToLeft - uDistanceToNextTab;
ppoint[2].y = uTabHeight;
/*if(uTabIndex == m_uSelectedViewIndex)
{
ppoint[2].x = iOffsetToLeft - 2 * uDistanceToNextTab;
} */
//draw tab-text
pDC->TextOut( ppoint[0].x + m_uOffsetFontGap,
1,
m_sarrayViewName[uTabIndex] );
iOffsetToLeft += m_dwarrayTabWidth[uTabIndex]; // "____"
ppoint[3].x = iOffsetToLeft + uDistanceToNextTab;
ppoint[3].y = uTabHeight;
if( uTabIndex == (UINT)(m_dwarrayTabWidth.GetSize() - 1) ) // "\____/"
{
iOffsetToLeft += uDistanceToNextTab;
ppoint[4].x = iOffsetToLeft;
ppoint[4].y = 0;
ppoint[5].x = init_x;
ppoint[5].y = init_y;
num_of_points = 6;
}
else
{
ppoint[4].x = iOffsetToLeft;
ppoint[4].y = uTabHeight;
ppoint[5].x = iOffsetToLeft;
ppoint[5].y = uTabHeight / 2;
iOffsetToLeft += (uDistanceToNextTab);
ppoint[6].x = iOffsetToLeft;
ppoint[6].y = 0;
ppoint[7].x = init_x;
ppoint[7].y = init_y;
num_of_points = 8;
//iOffsetToLeft -= (uDistanceToNextTab / 2);
//iOffsetToLeft += uDistanceToNextTab;
}
if(uTabIndex == m_uSelectedViewIndex)
{
CRgn rgn;
CBrush brush;
brush.CreateSolidBrush( ::GetSysColor( COLOR_WINDOW ) );
rgn.CreatePolygonRgn( ppoint, num_of_points, ALTERNATE );
//pDC->SetBkColor(::GetSysColor( COLOR_WINDOW ));
pDC->FillRgn( &rgn, &brush );
pDC->TextOut( ppoint[0].x + m_uOffsetFontGap,
1,
m_sarrayViewName[uTabIndex]);
}
pDC->Polyline( ppoint, num_of_points );
}
pDC->SelectObject( ppenOld );
}
Another thing of interest for the tab is the selection of the view when a tab is clicked, and that part of the code is in OnLButtonDown
. Note the calling of the SelectView
method when the mouse is clicked in one of the tabs which becomes selected:
/*------------------------------------------------------------------
Name: OnLButtonDown()
Type: Message
Purpose: Check if klicked on a tab, and if so switch view
*/
void HsTabCtrl::OnLButtonDown(UINT nFlags, CPoint point)
{
int iOffsetToLeft;
UINT uDistanceToNextTab;
uDistanceToNextTab = (m_uHeight - 1) / 2;
iOffsetToLeft = m_iOffsetToLeft;
// *** check if clicked on a tab ***
for( UINT uCounter = 0; uCounter <
(UINT)m_dwarrayTabWidth.GetSize(); uCounter++ )
{
iOffsetToLeft += uDistanceToNextTab;
if( ( point.x >= (iOffsetToLeft - 1) ) &&
( point.x <= (int)(iOffsetToLeft + m_dwarrayTabWidth[uCounter] + 1) ) )
{
SelectView( uCounter );
break;
}
iOffsetToLeft += m_dwarrayTabWidth[uCounter];
}
CWnd::OnLButtonDown(nFlags, point);
}
Points of Interest
In my original project, I needed to tile the views using my own algorithm, and that part of the code can be found in HsAlgorithm.h and cpp. Interested users can look it up.
The special tiling can be seen if you choose the Window->Tile option from the menu bar.
If you click in the drop down arrow of the tab bar, a popup appears to check/uncheck the views to include into/exclude from tiling.
Acknowledgements
Bahrudin Hrnjica, for his TabbedRebarSrc
class, and Per Ghosh, for his splitterTabWnd
class.
Special thanks goes to Mazharul Islam Khan (the writer of the Simple XP COM tutorial) for getting me interested in submitting articles to CodeProject.
History
- Article submitted on July 20, 2010