Creating Tab Controls Using CTabCtrl


Click here to change the theme.

I have seen many questions asking how to use tab controls without CPropertySheet and CPropertyPage. I have also been seeing questions asking how to use modeless dialogs as child dialogs without using tab controls. I think that the solution for the first can also show the solution for the second, so this article will explain how to create a tab control containing modeless dialogs using MFC. This sample will show how to put a CTabCtrl derived class into an application using CFormView for a view. The solution would be very similar for dialogs.

I have two sample solutions. The first one is a MFC version of the Platform SDK solution in Tab Control. The alternative is a better solution that I have developed. The advantage of my alternative is that the dialogs for the tabs are not created each time the tab is shown.

Creating the Control and Dialogs

The first few things to do is to create a:

  • Tab control in your dialog
  • Class for the tab control (I called mine CTabbyControl)
  • Dialog for each of the tabs
  • Class for each tab's dialog

This is done the same regardless of which of the two solutions you choose.

After creating a class derived from CTabCtrl (CTabbyControl or whatever), use ClassWizard to create a "Control" Category member variable and for the "Variable Type" select CTabbyControl (or whatever your CTabCtrl derived class is). I called my member variable m_ctlTabControl.

Each of the tab's dialogs should have the dialog style set to "Child" and the "Title Bar" style should be unchecked (no title bar).

Since each tab's dialog will be modeless, it is necessary to override CDialog::OnOK and CDialog::OnCancel, as described in the documentation; therefore it is necessary to create a class for each tab's dialog. Normally, tab controls do not have "Ok" and "Cancel" buttons in their tabs, but without the buttons in the dialogs, it might be difficult to use the ClassWizard to add handlers for IDOK (OnOK) and IDCANCEL (OnCancel). So before deleting the buttons, add the handlers for them first. If you have already delete the buttons, then you can add them temporarily, add the handlers, then delete the buttons.

First Solution

Each of the tab's dialogs should have the "Visible" style checked (shown by default).

In the CTabCtrl derived class create a member variable for the modeless dialog that currently exists in the tab control (which varies based on which tab is selected). Then create the following member variables:

Member Variable Description

int m_DialogResourceId[2];

an array of dialog control ids; one element for each dialog template that corresponds to each tab

CRect m_ClientRect;

to store the size of the contents

CDialog m_Dialog;

the current dialog for the (one and only) selected tab

Then in the CTabCtrl derived class's constructor initialize the array of dialog control ids, as in the following:

m_DialogResourceId[0] = IDD_DIALOG1;
m_DialogResourceId[1] = IDD_DIALOG2;

Add a member function to the CTabCtrl derived class to create a modeless dialog to be the content for each tab; I called mine "CreateContents". This member function will be called once for each tab's dialog. Use something such as the following:

void CTabbyControl::CreateContents() {
	int CurSel = GetCurSel();
if (m_Dialog.m_hWnd)
	m_Dialog.DestroyWindow();
if (!m_Dialog.Create(m_DialogResourceId[CurSel], GetParent()))
	TRACE0("Dialog not created\n");
m_Dialog.MoveWindow(m_ClientRect);
GetParent()->RedrawWindow();
}

The tabs can be created in PreSubclassWindow for a tab control in a CFormView view or in OnInitDialog for a tab control in a dialog using something as in the following::

	TC_ITEM TabCtrlItem;
	CRect WindowRect;
// Call base class PreSubclassWindow or OnInitDialog here
TabCtrlItem.mask = TCIF_TEXT;
TabCtrlItem.pszText = "Tab 1";
if (InsertItem(0, &TabCtrlItem)==-1)
	MessageBox("InsertItem 0 failed");
TabCtrlItem.pszText = "Tab 2";
if (InsertItem(1, &TabCtrlItem)==-1)
	MessageBox("InsertItem 1 failed");
// Determine the size of the area for the contents
GetClientRect(&m_ClientRect);
AdjustRect(FALSE, &m_ClientRect);
// Determine the offset within the view's client area
GetWindowRect(&WindowRect);
GetParent()->ScreenToClient(WindowRect);
m_ClientRect.OffsetRect(WindowRect.left, WindowRect.top);
// Complete the initialization
SetCurSel(0);
CreateContents();	// creates the first tab

Finally, add a handler to the CTabCtrl derived class for the TCN_SELCHANGE notification message as a reflected message. In the handler, call CreateContents.

Alternative Solution

In this solution, the dialogs for each tab are created initially and then simply disabled/enabled and shown/hidden as needed. I have provided a sample project with this solution implemented for a dialog. I have used the name "CTabby" for my class derived from CTabCtrl.

The relevant member variables of  the CTabby class are:

Type Name Description
CWnd*m_ParentPointer to parent (the dialog), for efficiency only
intm_PreviousTabWhen switching tabs, the previous tab (the tab being switched from)
CTabOneDialog
CTabTwoDialog
m_TabOneDialog
m_TabTwoDialog
The tab's dialogs (usually one for each tab)
CDialog*m_pDialogs[2]Array of pointers to dialogs to allow access using the tab index (number)
intm_MaxWidthMaximum width of dialogs put into the tab control
intm_MaxHeightMaximum height of dialogs put into the tab control

Be sure to initialize m_pDialogs and m_PreviousTab in the constructor. Set each element of m_pDialogs to a pointer to the corresponding dialog.

Then the following will create the tabs. This can be done in a member function of the CTabCtrl derived class. The member function can be a function you create and call from a form's OnInitialUpdate or a dialog's OnInitDialog. For dialogs, the following code can be done in an override of PreSubclassWindow.

	CRect DisplayArea;
	int i, n;
// Call base-class function here if relevant
m_Parent = GetParent();
if (InsertItem(0, "First")==-1)
	MessageBox("InsertItem 0 failed");
if (InsertItem(1, "Second")==-1)
	MessageBox("InsertItem 1 failed");
// The first tab's dialog's properties should include visible and enabled
if (!CreateTab(m_pDialogs[0], IDD_DIALOG1))
	MessageBox("The First Dialog was not created");
// The other tab's dialog's properties should include hidden and disabled
if (!CreateTab(m_pDialogs[1], IDD_DIALOG2))
	MessageBox("The Second Dialog was not created");
SetCurSel(0);
// get our position and size
// GetWindowRect receives the screen coordinates of the corners.
GetWindowRect(&DisplayArea);
m_Parent->ScreenToClient(&DisplayArea); // to our parent's coordinates
AdjustRect(FALSE, &DisplayArea); // Get current display area
DisplayArea.right = m_MaxWidth + DisplayArea.left;
DisplayArea.bottom = m_MaxHeight + DisplayArea.top;
AdjustRect(TRUE, &DisplayArea);
MoveWindow(&DisplayArea); // resize ourself
AdjustRect(FALSE, &DisplayArea);
for (i=0,n=(sizeof m_pDialogs/sizeof m_pDialogs[0]);i<n;++i)
	m_pDialogs[i]->MoveWindow(DisplayArea.left, DisplayArea.top,
		DisplayArea.Width(), DisplayArea.Height(), FALSE);

Note that each of the tab's dialogs should have the "Visible" style unchecked (hidden by default) and the Disabled style checked (disabled by default), except for the first dialog, which should be set to the opposite (shown and enabled by default).

The first thing done is to set m_Parent to the tab contro's parent, which should be the dialog or view that contains the tab control. This is only for efficiency. Then each of the tabs are inserted. Then the CreateTab member function is called to create each of the dialogs for the tabs. The parent of each tab dialog must be the same parent as the tab control's. The maximum width and height are determined for later use (see above).

The CreateTab member function calls the Create member function for the dialog to create a modeless dialog, but it also determines the width and height of the dialog and retains the maximum for all the dialogs. Note that the tab control's parent (the dialog containing the tab control) is uses for the parent of each dialog in the tab control. The tab control is not the parent!

The parameters for CreateTab are:

  • pDlg: a pointer to the dialog for the tab
  • nId: dialog id of the dialog

After CreateTab has been called to create a modeless dialog for each tab, the tab control and the dialogs for it are resized. GetWindowRect and ScreenToClient are used to get the current position and size of the tab control. Then CTabCtrl::AdjustRect is used to determine the current position of the tab control display rectangle, also known as the display area. This is the area that does not include the tab buttons and is the area typically used for the tab contents. We need to know the position so that we can use it when we call CTabCtrl::AdjustRect for the new size. We then set the new display area size by setting the right side and bottom of the display area. Then we use MoveWindow to resize ourselves (note that MoveWindow's size and position is relative to our parent). Then we call CTabCtrl::AdjustRect one more time (I think this is not necessary; I will attempt to simplify) to convert the tab control's position and size back to the position and size of the display area. Finally, each of the tab control's tab dialogs are resized.

CreateTab

BOOL CTabby::CreateTab(CDialog *pDlg, UINT nId) {
CRect WindowRect;
int tw, th;
if (!pDlg->Create(nId, m_Parent))
	return FALSE;
pDlg->GetWindowRect(&WindowRect);
tw = WindowRect.Width();
m_MaxWidth = tw < m_MaxWidth ? m_MaxWidth : tw;
th = WindowRect.Height();
m_MaxHeight = th < m_MaxHeight ? m_MaxHeight : th;
return TRUE;
}

The TCN_SELCHANGE notification message handler would be something such as:

if (m_PreviousTab == GetCurSel())
	return;
m_pDialogs[m_PreviousTab]->EnableWindow(FALSE);
m_pDialogs[m_PreviousTab]->ShowWindow(SW_HIDE);
m_PreviousTab = GetCurSel();
m_pDialogs[m_PreviousTab]->EnableWindow(TRUE);
m_pDialogs[m_PreviousTab]->ShowWindow(SW_SHOW);

Sample Project

I have provided a small (20 KB) VC 6 dialog-based sample project TabDialog to show how it can all fit together.

Additional Notes

If you need to retrieve data each time a dialog is switched to and/or store data each time a dialog is switched from, then the WM_ENABLE message can be very useful.