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_Parent | Pointer to parent (the dialog), for
efficiency only |
int | m_PreviousTab | When 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) |
int | m_MaxWidth | Maximum width of dialogs put into the
tab control |
int | m_MaxHeight | Maximum 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.