Tabbed Views (2)

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

Many times in an SDI interface the programmer would like to have many
views of the same document.  Look at the MFC sample collect. 
It consists of an SDI app which has nine different views for each of the
data types in its document.  The view is switched by selecting a menu
item.  There are several other different ways this user interface
could be organized and presented to the user. One
would be by presenting a TabCtrl to the user and have a Tab for each
view.  This is the approach I wanted to take in my own SDI application. 
Following then will be the code for CTabCtrlView, a view which replaces
the standard SDI client window and
handles the displaying of the multiple view. An AVI of the control
follows:


As an example of this control I will describe how to modify the MFC
collect sample so it uses my new control as its interface.  The first
thing you must do is derive your own class from CTabCtrlView.  There
are several virtual functions you must override.  This allows you
to add your own application specific Tab items and Send Message Commands. 
The first function to override is InitTabs.  This function is responsible
for inserting the Tabs into the TabCtrl.  Proceed as you would for
any CTabCtrl object.:

void CMyTabView::InitTabs(CTabCtrlView* pView)
{
        TC_ITEM TabCtrlItem;

        TabCtrlItem.mask = TCIF_TEXT | TCIF_IMAGE;
        TabCtrlItem.pszText = “CStringList”;
        TabCtrlItem.iImage = 1;
        m_TabCtl.InsertItem( 0, &TabCtrlItem );
        TabCtrlItem.pszText = “CTypedPtrList”;
        m_TabCtl.InsertItem( 1, &TabCtrlItem );
        TabCtrlItem.pszText = “CList”;
        m_TabCtl.InsertItem( 2, &TabCtrlItem );
        TabCtrlItem.pszText = “CDWordArray”;
        m_TabCtl.InsertItem( 3, &TabCtrlItem );
        TabCtrlItem.pszText = “CTypedPtrArray”;
        m_TabCtl.InsertItem( 4, &TabCtrlItem );
        TabCtrlItem.pszText = “CArray”;
        m_TabCtl.InsertItem( 5, &TabCtrlItem );
        TabCtrlItem.pszText = “CMapStringToString”;
        m_TabCtl.InsertItem( 6, &TabCtrlItem );
        TabCtrlItem.pszText = “CTypedPtrMap”;
        m_TabCtl.InsertItem( 7, &TabCtrlItem );
        TabCtrlItem.pszText = “CMap”;
        m_TabCtl.InsertItem( 8, &TabCtrlItem );

        //Important You must always call the base class        
        CTabCtrlView::InitTabs(pView);
        return;
}

The next step is to override the function HandleTabs().  This function
will send a message back to the main window for each tab that needs processing. 
The way MFC is written it is the job of the main frame to handle switching
view and assigning the view. Instead of reinventing the wheel I decided
it would be easier to send a message to the main frame and allow it to
handle the changing of the views. The HandleTabs for the new MFC collect
should look like this:

BOOL CMyTabView::HandleTabs(int sel)
{
        //The MFC doc/view model expects the
        //mainFrame to handle changing view
        //so we let the mainframe do it.

        CWnd* Wnd = AfxGetMainWnd();

        switch(sel)
        {
        case 0:
                Wnd->SendMessage(WM_COMMAND, ID_STRINGLIST, 0);
                break;
        case 1:
                Wnd->SendMessage(WM_COMMAND, ID_TYPEDLIST, 0);
                break;
        case 2:
                Wnd->SendMessage(WM_COMMAND, ID_INTLIST, 0);
                break;
        case 3:
                Wnd->SendMessage(WM_COMMAND, ID_DWORDARRAY, 0);
                break;
        case 4:
                Wnd->SendMessage(WM_COMMAND, ID_TYPEDPTRARRAY, 0);
                break;
        case 5:
                Wnd->SendMessage(WM_COMMAND, ID_POINTARRAY, 0);
                break;
        case 6:
                Wnd->SendMessage(WM_COMMAND, ID_MAPSTRINGTOSTRING, 0);
                break;
        case 7:
                Wnd->SendMessage(WM_COMMAND, ID_TYPEDPTRMAP, 0);
                break;
        case 8:
                Wnd->SendMessage(WM_COMMAND, ID_MAPDWORDTOMYSTRUCT, 0);
                break;
        default:
                ASSERT(0);
                return FALSE;
        }
        return TRUE;
}


As you notice each Tab sends the main frame the corresponding COMMAND message
that the menu would have if the user would have selected a menu item. 
This implementation allows you to be redundant by using tabs or the menu
to change the view.

There needs to be a couple of changes to the main frame window code
as well. The first thing is to make an protected object from your derived
class. Now that the object exists we need to tell the main frame that this
window will be its client window. Override the OnCreateClient function
and change the function to look like this:


BOOL CMainFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
{
        if(!m_tabView.CreateStatic(this))
        {
                TRACE0(“Failed to CreatePropertySheet\n”);
                return FALSE;
        }       
        CRuntimeClass* pNewViewClass = RUNTIME_CLASS(CStringListView);
        m_tabView.SetTab(0);

        CSize size(160, 180);

        if (!m_tabView.CreateView(pNewViewClass, size, pContext))
        {
                TRACE0(“Failed to create first pane\n”);
                return FALSE;
        }
        
        SetActiveView(m_tabView.GetActiveView());
        return TRUE;
}


Now when the application creates the main client window your CTabCtrlView
object should appear as the client window for your application. The final
step is to change the existing OnExample function in the main frame window. 
We need to tell it that when it creates a view it should call CTabCtrl’s
Create View and not the one for the main Frame. Also we call the TabCtrlView’s
RecalcLayout function which will correctly position the new view in the
TabCtrl’s window area.

void CMainFrame::OnExample(UINT nCmdID)
{
        if (nCmdID == m_nCurrentExample)
                return;  // already selected

        // Set the child window ID of the active view to AFX_IDW_PANE_FIRST.
        // This is necessary so that CFrameWnd::RecalcLayout will allocate
        // this “first pane” to that portion of the frame window’s client
        // area not allocated to control bars.  Set the child ID of
        // the previously active view to some other ID; we will use the
        // command ID as the child ID.
        CView* pOldActiveView = GetActiveView();
                ::SetWindowLong(pOldActiveView->m_hWnd, GWL_ID, m_nCurrentExample);

        CRuntimeClass* pNewViewClass;
        switch (nCmdID)
        {
                case ID_STRINGLIST:
                        pNewViewClass = RUNTIME_CLASS(CStringListView);
                        break;
                case ID_TYPEDLIST:
                        pNewViewClass = RUNTIME_CLASS(CTypedPtrListView);
                        break;
                case ID_INTLIST:
                        pNewViewClass = RUNTIME_CLASS(CIntListView);
                        break;
                case ID_DWORDARRAY:
                        pNewViewClass = RUNTIME_CLASS(CDWordArrayView);
                        break;
                case ID_TYPEDPTRARRAY:
                        pNewViewClass = RUNTIME_CLASS(CTypedPtrArrayView);
                        break;
                case ID_POINTARRAY:
                        pNewViewClass = RUNTIME_CLASS(CPointArrayView);
                        break;
                case ID_MAPSTRINGTOSTRING:
                        pNewViewClass = RUNTIME_CLASS(CMapStringToStringView);
                        break;
                case ID_TYPEDPTRMAP:
                        pNewViewClass = RUNTIME_CLASS(CTypedPtrMapView);
                        break;
                case ID_MAPDWORDTOMYSTRUCT:
                        pNewViewClass = RUNTIME_CLASS(CMapDWordToMyStructView);
                        break;
                default:
                        ASSERT(0);
                        return;
        }

        // create the new view
        CCreateContext context;
        context.m_pNewViewClass = pNewViewClass;
        context.m_pCurrentDoc = GetActiveDocument();

        // New Code below
        CView* pNewView = m_tabView.CreateView(pNewViewClass, CSize(100,100), &context);

        if (pNewView != NULL)
        {
                // the new view is there, but invisible and not active…
                pNewView->ShowWindow(SW_SHOW);
                pNewView->OnInitialUpdate();
                SetActiveView(pNewView);
                m_tabView.RecalcLayout();       //<– New Code
                RecalcLayout();
                m_nCurrentExample = nCmdID;

                // finally destroy the old view…
                pOldActiveView->DestroyWindow();
        }
}


That’s about it, you should now have a version of the MFC collect sample
which uses my CTabCtrlView Class.   Now for a few words on the
class. It consists of two main parts.  A wrapper window which gets
set as the main frame’s client and the TabCtrl itself.  It is the
job of the wrapper window to resize the TabCtrl and the view to keep everything
in its proper place. The window also handles the initialization and handling
of the Tabs for the TabCtrl. The code for this class follows:

 

#if !defined(AFX_CTabCtrlView_H__8E652EC1_5159_11D1_96A0_0E6B8A000000__INCLUDED_)
#define AFX_CTabCtrlView_H__8E652EC1_5159_11D1_96A0_0E6B8A000000__INCLUDED_

#if _MSC_VER >= 1000
#pragma once
#endif // _MSC_VER >= 1000
// CTabCtrlView.h : header file
//

#include "afxcmn.h"

class CTabCtrlView;

class CViewTabCtl : public CTabCtrl
{
// Construction
public:
        CViewTabCtl();

// Attributes
public:

// Operations
public:
        
// Overrides
        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CViewTabCtl)
        //}}AFX_VIRTUAL

// Implementation
public:
        void SetView(CTabCtrlView* pView);
        virtual void RecalcLayout(CRect& rect, CWnd* wnd);
        virtual ~CViewTabCtl();
        virtual void DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct);

        // Generated message map functions
protected:
        CTabCtrlView* m_pView;
        CString m_sGrayFont;
        CString m_sSelFont;
        CDC m_dc;
        CFont m_selFont;
        virtual BOOL HandleTabs(int sel);
        //{{AFX_MSG(CViewTabCtl)
        afx_msg void OnSelchange(NMHDR* pNMHDR, LRESULT* pResult);
        //}}AFX_MSG

        DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////
// CCTabCtrlView window

// The actual tab control
// Wrapper Window Handles Interfacing to MFC doc View Model
// and resizing of the Tab Ctrl
class CTabCtrlView : public CWnd
{
// Construction
public:
        CTabCtrlView();

// Attributes
public:

// Operations
public:

// Overrides
        // ClassWizard generated virtual function overrides
        //{{AFX_VIRTUAL(CCTabCtrlView)
        //}}AFX_VIRTUAL

// Implementation
public:
        virtual BOOL HandleTabs(int sel);
        void SetTab(int Tab);
        virtual void SetView();
        void RecalcLayout();
        CView* GetActiveView();
        BOOL CreateStatic(CWnd* pParentWnd, DWORD dwStyle = WS_CHILD | WS_VISIBLE, UINT nID = AFX_IDW_PANE_FIRST);
        virtual CView* CreateView(CRuntimeClass* pViewClass, SIZE sizeInit, CCreateContext* pContext );
        virtual ~CTabCtrlView();

        // Generated message map functions
protected:
        virtual void InitTabs(CTabCtrlView* pView);
        CViewTabCtl m_TabCtl;
        CView* m_ActiveView;
        
        BOOL CreateCommon(CWnd* pParentWnd, SIZE sizeMin, DWORD dwStyle, UINT nID);
        //{{AFX_MSG(CCTabCtrlView)
        afx_msg void OnSize(UINT nType, int cx, int cy);
        afx_msg BOOL OnEraseBkgnd(CDC* pDC);
        afx_msg void OnPaint();
        //}}AFX_MSG

        DECLARE_MESSAGE_MAP()
};

/////////////////////////////////////////////////////////////////////////////

/////////////////////////////////////////////////////////////////////////////
// CViewTabCtl window


//{{AFX_INSERT_LOCATION}}
// Microsoft Developer Studio will insert additional declarations immediately before the previous line.

#endif // !defined(AFX_CTabCtrlView_H__8E652EC1_5159_11D1_96A0_0E6B8A000000__INCLUDED_)


/////////////////////////////////////////////////////////////////////////////
C++ Code file
/////////////////////////////////////////////////////////////////////////////

// CTabCtrlView.cpp : implementation file
//

// core headers
#include "afx.h"
#include "afxplex_.h"
#include "afxcoll.h"
#include "afxcmn.h"

#include "stdafx.h"

#include "TabCtrlView.h"

#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif

#define CX_BORDER 1
#define CY_BORDER 1

#define MAX_COLORS 10

// The following colors give a Tab Ctrl an OS/2 look
COLORREF colorRef[MAX_COLORS] =
{
        RGB(0,225,255),
        RGB(0,240,190),
        RGB(128,128,255),
        RGB(240,200,175),
        RGB(240,240,150),
        RGB(175,130,175),
        RGB(240,140,0),
        RGB(255,200,0),
        RGB(255,160,120),
        RGB(255,200, 175)
};


/////////////////////////////////////////////////////////////////////////////
// CTabCtrlView

CTabCtrlView::CTabCtrlView()
{
}

CTabCtrlView::~CTabCtrlView()
{
}


BEGIN_MESSAGE_MAP(CTabCtrlView, CWnd)
        //{{AFX_MSG_MAP(CTabCtrlView)
        ON_WM_SIZE()
        ON_WM_ERASEBKGND()
        ON_WM_PAINT()
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CTabCtrlView message handlers

BOOL CTabCtrlView::CreateStatic(CWnd * pParentWnd, DWORD dwStyle, UINT nID)
{
        ASSERT(pParentWnd != NULL);
        ASSERT(dwStyle & WS_CHILD);
        ASSERT(!(dwStyle & SPLS_DYNAMIC_SPLIT));

        // create with zero minimum pane size
        if (!CreateCommon(pParentWnd, CSize(0, 0), dwStyle, nID))
                return FALSE;

        // all panes must be created with explicit calls to CreateView
        return TRUE;
}





BOOL CTabCtrlView::CreateCommon(CWnd * pParentWnd, SIZE sizeMin, DWORD dwStyle, UINT nID)
{
        ASSERT(pParentWnd != NULL);
        ASSERT(sizeMin.cx >= 0 && sizeMin.cy >= 0);
        ASSERT(dwStyle & WS_CHILD);
        ASSERT(nID != 0);

        // create with the same wnd-class as MDI-Frame (no erase bkgnd)
        if (!CreateEx(0, NULL, NULL, dwStyle, 0, 0, 0, 0,
          pParentWnd->m_hWnd, (HMENU)nID, NULL))
                return FALSE;       // create invisible

        //Create the Tab Control
        CRect rect;
        GetClientRect(rect);
        CImageList pImageList;


        m_TabCtl.Create(WS_VISIBLE | WS_CHILD | TCS_OWNERDRAWFIXED, rect, this, nID);

        //Overide this function to provide your Tabs
        InitTabs(this);
        return TRUE;
}

CView* CTabCtrlView::CreateView(CRuntimeClass * pViewClass, SIZE sizeInit, CCreateContext * pContext)
{
#ifdef _DEBUG
        ASSERT_VALID(this);
        ASSERT(pViewClass != NULL);
        ASSERT(pViewClass->IsDerivedFrom(RUNTIME_CLASS(CWnd)));
        ASSERT(AfxIsValidAddress(pViewClass, sizeof(CRuntimeClass), FALSE));
#endif

        BOOL bSendInitialUpdate = FALSE;

        CCreateContext contextT;
        if (pContext == NULL)
        {
                // if no context specified, generate one from the currently selected
                //  client if possible
                CView* pOldView = NULL;
                if (pOldView != NULL && pOldView->IsKindOf(RUNTIME_CLASS(CView)))
                {
                        // set info about last pane
                        ASSERT(contextT.m_pCurrentFrame == NULL);
                        contextT.m_pLastView = pOldView;
                        contextT.m_pCurrentDoc = pOldView->GetDocument();
                        if (contextT.m_pCurrentDoc != NULL)
                                contextT.m_pNewDocTemplate =
                                  contextT.m_pCurrentDoc->GetDocTemplate();
                }
                pContext = &contextT;
                bSendInitialUpdate = TRUE;
        }

        CWnd* pWnd;
        TRY
        {
                pWnd = (CWnd*)pViewClass->CreateObject();
                if (pWnd == NULL)
                        AfxThrowMemoryException();
        }
        CATCH_ALL(e)
        {
                TRACE0("Out of memory creating a splitter pane.\n");
                // Note: DELETE_EXCEPTION(e) not required
                return (CView*) NULL;
        }
        END_CATCH_ALL

        ASSERT_KINDOF(CWnd, pWnd);
        ASSERT(pWnd->m_hWnd == NULL);       // not yet created

        DWORD dwStyle = AFX_WS_DEFAULT_VIEW;

        // Create with the right size (wrong position)
        CRect rect(CPoint(0,0), sizeInit);
        if (!pWnd->Create(NULL, NULL, dwStyle,
                rect, this, 0, pContext))
        {
                TRACE0("Warning: couldn't create client pane for splitter.\n");
                        // pWnd will be cleaned up by PostNcDestroy
                return (CView*) NULL;
        }

        // send initial notification message
        if (bSendInitialUpdate);
//              pWnd->SendMessage(WM_INITIALUPDATE);
        m_ActiveView = (CView*) pWnd;
        return m_ActiveView;
}

void CTabCtrlView::OnSize(UINT nType, int cx, int cy) 
{
        if (nType != SIZE_MINIMIZED && cx > 0 && cy > 0)
                RecalcLayout();

        CWnd::OnSize(nType, cx, cy);
        return;
}

void CTabCtrlView::RecalcLayout()
{
        CWnd* pWnd = (CWnd*) GetActiveView();
        CRect rect;
        GetClientRect(&rect);
        m_TabCtl.RecalcLayout(rect, pWnd);
}

CView* CTabCtrlView::GetActiveView()
{
        return m_ActiveView;
}

BOOL CTabCtrlView::OnEraseBkgnd(CDC* pDC) 
{
        return FALSE;
}

void CTabCtrlView::OnPaint() 
{
        CPaintDC dc(this); // device context for painting
}

void CTabCtrlView::SetView()
{
        //In most cases your main app window
        //should handle this. This is becuase
        //the doc view model expects the view
        //to be attached to your main frame
}

void CTabCtrlView::SetTab(int Tab)
{
        m_TabCtl.SetCurSel(Tab);
}

void CTabCtrlView::InitTabs(CTabCtrlView* pView)
{
        m_TabCtl.SetView(pView);
        return;
}

BOOL CTabCtrlView::HandleTabs(int sel)
{
        ASSERT(FALSE);
        return FALSE;
}






/////////////////////////////////////////////////////////////////////////////
// CViewTabCtl

CViewTabCtl::CViewTabCtl()
{
        m_sSelFont = _T("Helv");
        m_sGrayFont= _T("Helv");
}

CViewTabCtl::~CViewTabCtl()
{
}


BEGIN_MESSAGE_MAP(CViewTabCtl, CTabCtrl)
        //{{AFX_MSG_MAP(CViewTabCtl)
        ON_NOTIFY_REFLECT(TCN_SELCHANGE, OnSelchange)
        //}}AFX_MSG_MAP
END_MESSAGE_MAP()

/////////////////////////////////////////////////////////////////////////////
// CViewTabCtl message handlers

void CViewTabCtl::OnSelchange(NMHDR* pNMHDR, LRESULT* pResult) 
{
        // TODO: Add your control notification handler code here
        int nSel = GetCurSel();
        
        HandleTabs(nSel);
        
        *pResult = 0;
}

BOOL CViewTabCtl::HandleTabs(int sel)
{
        return m_pView->HandleTabs(sel);
}

void CViewTabCtl::RecalcLayout(CRect & rect, CWnd * wnd)
{
        SetWindowPos(NULL, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER);

        int ind = GetCurSel();
        AdjustRect(FALSE, &rect);
        wnd->SetWindowPos(NULL, rect.left, rect.top, rect.right - rect.left, rect.bottom - rect.top, SWP_NOZORDER);
}

void CViewTabCtl::DrawItem(LPDRAWITEMSTRUCT lpDrawItemStruct)
{
        int nSel = lpDrawItemStruct->itemID;
        ASSERT(nSel > -1);
        ASSERT(nSel <  GetItemCount()); 

        TC_ITEM item;
        char text[255];

        item.pszText = text;
        item.mask = TCIF_TEXT;
        GetItem(nSel, &item);

        if(!m_dc.Attach(lpDrawItemStruct->hDC))
                return;

        CRect rect = CRect(&(lpDrawItemStruct->rcItem));

        rect.NormalizeRect();
        rect.DeflateRect(CX_BORDER, CY_BORDER);

        CBrush brush(colorRef[nSel - (nSel / MAX_COLORS) * MAX_COLORS]);
        
        m_dc.FillRect(rect, &brush);

        
        COLORREF tcolor;
        if (nSel == GetCurSel())
        {
                m_selFont.DeleteObject();
                m_selFont.CreatePointFont(100, LPCTSTR(m_sGrayFont), &m_dc);
                m_dc.SelectObject(m_selFont);
                tcolor = RGB(0,0,0);
                
        }
        else
        {
                m_selFont.DeleteObject();
                m_selFont.CreatePointFont(80, LPCTSTR(m_sSelFont), &m_dc);
                m_dc.SelectObject(m_selFont);
                tcolor = GetSysColor(COLOR_3DSHADOW);
        }
        m_dc.SetBkMode(TRANSPARENT);
        m_dc.SetTextColor(tcolor);
        m_dc.DrawText(text, rect, DT_VCENTER|DT_CENTER);
        m_dc.Detach();
        return;
}

void CViewTabCtl::SetView(CTabCtrlView * pView)
{
        m_pView = pView;
        return;
}

More by Author

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Must Read