Share via


From the June 2000 issue of MSDN Magazine.

This article may contain URLs that were valid when originally published, but now link to sites or pages that no longer exist. To maintain the flow of the article, we've left these URLs in the text, but disabled the links.MIND

 
Web Q&A
Creating Sophisticated Tabbed Views with CTabView and the TabDemo App
Jeff Prosise

Code for this article: WICKED0600.exe (91KB)


S

ince the day I first laid eyes on MFC's CCtrlView class, I've been intrigued by the possibilities it offers. In case you don't know, CCtrlView is the base class for the MFC view classes that encapsulate Windows® controls. Its derivatives include CEditView, which builds a view around a multiline edit control; CRichEditView, which wraps rich edit controls; CListView, which wraps list view controls; and CTreeView, which wraps tree view controls.
      Because CCtrlView provides the logic required to convert an ordinary Windows control into a full-fledged MFC view, you can use it as a base class for control-based view classes of your own. The perfect example of a CCtrlView derivative is CTabView, a custom view class that uses the Windows tab control as the basis for a tabbed view (see Figure 1). In this column I'll document how CTabView works and how you can use it in your own applications.

Figure 1 A Custom Tabbed View
Figure 1 A Custom Tabbed View

      CTabView is the answer to the often-asked question, "How can I create a view that has tabs like a property sheet?" It turns out that creating the view is the easy part, but things quickly start to get complicated after that. You can simply derive a class from CCtrlView, write a constructor that calls CCtrlView's constructor with the tab control's WNDCLASS name ("SysTabControl32") as the first parameter, and pass MFC a reference to the derived class in the application's document template, as shown in Figure 2.
      Once a tabbed view is created, adding pages to it is a simple matter of calling CTabCtrl::InsertItem to add tabs. How do you call CTabCtrl functions on a CTabView? Easy. Just call GetTabCtrl to get a CTabCtrl reference to the underlying tab control. To demonstrate, the following for loop adds tabs with the captions Page 1 through Page 5 to a CTabView:

 

  for (int i=0; i<5; i++) {
  
CString string;
string.Format (_T ("Page %d"), i + 1);
TC_ITEM item;
item.mask = TCIF_TEXT;
item.pszText = (LPTSTR) (LPCTSTR) string;
GetTabCtrl ().InsertItem (i, &item);
}

 

      So far, so good. But now comes the hard part. You've created a tabbed view and added pages to it. But what do you put on those pages? It's simple enough to create child window controls (push buttons and listboxes, for example) in the CTabView, but what happens when the user clicks a tab to go to another page? Assuming you want each page of the tabbed view to host a different set of controls, you have to delete (or hide) the existing controls and create (or redisplay) the controls on the new page.
      Your next step is to trap the WM_NOTIFY messages that are transmitted each time a tab is selected and update the view by displaying the new page's controls. For an added touch, you could also build your CTabView class to create pages from dialog resources in much the same way that property sheet pages use dialog resources. While you're at it, why not factor out that logic into a base class that others can derive from to create tabbed views?
      The good news is that a full-featured implementation of CTabView already exists, and you can download it from the link at the top of this article. My CTabView uses dialog resources to define the contents of its pages. It's also smart enough to switch pages when a tab is selected. All you have to do is derive from it, define your dialog resources, and call CTabView:: AddPage once per page to add dialog-like pages to the view. The base class does the rest (see Figure 3).

Using CTabView

      To put CTabView to work, follow these simple steps:

  1. Create a standard doc/view app and add the following four files to the project: ChildDialog.h, ChildDialog.cpp, TabView.h, and TabView.cpp.
  2. #include TabView.h in any source code files that reference your view class. Then modify your view class so that it derives from CTabView.
  3. Use the Visual C++® dialog editor to create the dialog resourcesâ€"one per pageâ€"that depict the pages in the tabbed view. Modify the properties of the dialog resources you create by setting Style = Child and Border = None. Also, make sure that the Visible box is unchecked, as shown in Figure 4.
  4. Add a WM_CREATE handler to the derived view class and call AddPage from it to add pages to the view.
Figure 4 Creating the Dialog Resources
Figure 4 Creating the Dialog Resources

      Each call to AddPage adds one page to the view. AddPage is overloaded to accept either a resource name or resource ID in the third parameter. The first and second parameters are the title that appears on the tab and the page ID, respectively. The page ID is simply an integer value that uniquely identifies the page, similar to a control ID. For example, the statement

 

  AddPage (_T ("Page 1"), 100, IDD_PAGE1);
  

 

adds a page titled Page 1 to the view, assigns the page the page ID of 100, and assigns to the page the controls in the dialog resource whose resource ID is IDD_PAGE1. When the page is activated, the controls are displayed automatically.
      A CTabView-derived class inherits a complementary function named RemovePage that programmatically removes a page from the view. Because RemovePage uses a zero-based page index to identify the page to be removed, this statement removes the first page from the view:

 

  RemovePage (0);
  

 

      AddPage and RemovePage are just two of many operations, or nonvirtual member functions, that a derived view class inherits from CTabView. A complete list of member functions, both virtual and nonvirtual, appears in Figure 5. GetPageCount, for example, returns the number of pages in the view, while GetActivePage returns the index of the page that is currently showing.
      ActivatePage switches to another page programmatically. Like most CTabView functions that interact with a page, ActivatePage requires a zero-based page index as input. If you don't know the index of a page but do know the page ID, use GetPageIndex to convert the page ID into an index. The statement

 

  int nIndex = GetPageIndex (100);
  

 

initializes nIndex with the index of the page whose ID is 100, while the following statement activates the page whose ID is 100.

 

  ActivatePage (GetPageIndex (100));
  

 

      If CTabView lacks a function that you need, it's possible that MFC's CTabCtrl class provides it. You can call CTabCtrl functions on a CTabView by first calling GetTabCtrl to get a CTabCtrl reference. For example, the statement

 

  GetTabCtrl ().SetMinTabWidth (128);
  

 

uses CTabCtrl::SetMinTabWidth to set the minimum width of the tabs to 128 pixels. By the same token, you can apply tab control styles (TCS_TABS, TCS_BUTTONS, and so on) to a CTabView in PreCreateWindow to modify the view's appearance and behavior. The following PreCreateWindow override gives the tabs a button-like appearance:

 

  BOOL CMyTabView::PreCreateWindow (CREATESTRUCT& cs)
  
{
cs.style |= TCS_BUTTONS;
return CTabView::PreCreateWindow (cs);
}

 

      If Microsoft® Internet Explorer 3.0 or higher is installed, you can use the new tab control styles introduced in Comctl32.dll version 4.70. For example, applying the TCS_BOTTOM style to the control moves the tabs to the bottom of the window:

 

  BOOL CMyTabView::PreCreateWindow (CREATESTRUCT& cs)
  
{
cs.style |= TCS_BOTTOM;
return CTabView::PreCreateWindow (cs);
}

 

You can also use the TCS_VERTICAL style to create a CTabView with tabs on the left, or combine TCS_VERTICAL and TCS_ RIGHT to create a tabbed view whose tabs are on the right.
      One of CTabView's more interesting characteristics is the fact that it uses child dialogsâ€"dialog boxes with the style WS_ CHILDâ€"to house the controls that it displays. When called to add a page, AddPage creates a modeless dialog box from the dialog resource named in the function's third parameter. The dialog is positioned in the view's upper-left corner and parented to the view. Because it has neither a border nor a title bar, the dialog blends invisibly into the view. Consequently, the controls in the dialog appear to belong to the view, not the dialog.
      For the most part, the existence of the child dialogs is transparent to the developer. For example, child dialogs route the WM_ COMMAND and WM_NOTIFY messages and ActiveX® control events that they receive to the view they're parented to. As a result, you can put handlers for events and notification messages in the derived view class and the handlers will be activated just as if the controls belonged to the view. If IDC_BUTTON is the ID of a button in one of the view's dialog templates, placing the statement

 

  ON_BN_CLICKED (IDC_BUTTON, OnButtonClicked)
  

 

in the view's message map invokes the view's OnButtonClicked function whenever the push button is clicked.
      CTabView's use of child dialogs does have one very important implication for the code that you write. If the controls were children of the view, you could call GetDlgItem on the view to obtain a CWnd pointer to a control, like this:

 

  CWnd* pWnd = GetDlgItem (IDC_BUTTON);
  

 

But because the controls are actually the children of a dialog box that is itself a child of the view, you must go through the dialog to get a pointer to a control object.

 

  CWnd* pWnd = GetPage (nIndex)->GetDlgItem (IDC_BUTTON);
  

 

If you'd prefer to use SubclassDlgItem to create a type-specific reference to a control, do this:

 

  CButton wndButton;
  
wndButton.SubclassDlgItem (IDC_BUTTON, GetPage (nIndex));

 

And to unsubclass the control, do this:

 

  wndButton.UnsubclassWindow ();
  

 

      CTabView defines four virtual functions that you can override in a derived class to respond to events that take place within a tabbed view. Figure 5 describes them in detail. The first, OnInitPage, is called each time a page is added to the view. Think of it as the equivalent of a WM_INITDIALOG message; it's your chance to initialize the controls on the page the moment they're created. Its parameter list contains both the index and the ID of the page that's being initialized. If you want to initialize some or all of the pages in a CTabView programmatically, override this function and switch on the page index or ID. The OnInitPage override shown in Figure 6 adds items to a listbox on Page 2 (index == 1) when that page is created.
      The code sample in Figure 6 returns FALSE after assigning input focus to the listbox. This prevents the view from setting the focus to the first control on the page. Returning TRUE from OnInitPage automatically sets input focus to the page's first control the first time the page is activated.
      Other CTabView overridables include OnActivatePage and OnDeactivatePage, which are called when the user switches from one page to another. OnDeactivatePage is called first with the index and ID of the old page. Then OnActivatePage is called with the index and ID of the new page. Note that these functions are not called when a page is activated programmatically with ActivatePage; they're only called in response to user input.
      When a page is destroyed, either because the view itself is destroyed or because RemovePage was called, the final CTabView overridable, OnDestroyPage, is called. Override this function to free application-specific resources allocated in OnInitPage.

CTabView Internals

      CChildDialog is the class CTabView uses for the dialog boxes that serve as unseen containers for controls. Its base class is CDialog, and its member functions serve two primary purposes. The OnOK and OnCancel overrides prevent the dialog from being inadvertently destroyed if a control is assigned the ID of an OK or Cancel button (IDOK or IDCANCEL). The OnCommand, OnNotify, and OnCmdMsg overrides forward notification messages sent by Windows controls and events fired by ActiveX controls to the dialog's parent (the view). This forwarding mechanism enables you to place notification handlers and event sinks in the view.
      The code for CTabView itself is found in TabView.h and TabView.cpp. The AddPage function is a good starting point for forays into CTabView internals. AddPage begins by adding an item (a tab) to the underlying tab control by calling CTabCtrl::InsertItem. Then it instantiates a CChildDialog object and creates a modeless dialog box from it using the dialog resource specified in the third parameter. Next, AddPage repositions the dialog in the view's upper-left corner (taking into account the size and location of the tabs), and resizes it to fit snugly inside the tab control. Finally, AddPage calls OnInitPage, records the window handle of the control that receives input focus the first time the page is activated, and displays the page if that page is the first one added.
      AddPage sizes the dialog boxes that it creates to fit the interior of the view to prevent a dialog that's wider or taller than the view from obscuring the view's border. CTabView's WM_SIZE handler ensures that the dialog size remains keyed to the view size for the duration of the view's lifetime.
      ON_NOTIFY_REFLECT entries in CTabView's message map activate CTabView::OnSelChanging and CTabView::OnSelChange when the user switches pages. OnSelChanging deactivates the old page by hiding the dialog box that's currently displayed. OnSelChange displays the new page by showing that page's dialog box. Because these message handlers provide an essential service to the view, you should call them in the base class if you override them in a derived class.

The TabDemo Application

      If you'd like to see a CTabView in action or would appreciate some sample code demonstrating its use, download TabDemo from the link at the top of this article. TabDemo is the application shown in Figure 1. It's a doc/view MFC app built around a CTabView derivative named CMyTabView. CMyTabView's WM_CREATE handler calls AddPage three times to add pages to the view:

 

  int nIndex = AddPage (_T ("Page 1"),
  
IDD_PAGE1, IDD_PAGE1);
nIndex = AddPage (_T ("Page 2"),
IDD_PAGE2, IDD_PAGE2);
nIndex = AddPage (_T ("Page 3"),
IDD_PAGE3, IDD_PAGE3);

 

Each page contains a simple set of controls. Message handlers for the button controls are provided in CMyTabView. Figure 7 shows them in abbreviated form. The Next Page and Previous Page buttons perform programmatic page switches by incrementing or decrementing the current page number and calling ActivatePage. The Delete This Page button on Page 3 deletes the current page by calling RemovePage.
      As you can see, CTabView vastly simplifies the task of creating tabbed views in MFC applications. Feel free to use it in your own applications or to extend it by adding your own features. Let me know what you think of it and perhaps I'll upgrade it someday if I collect enough cool ideas.

Drop Me a Line

      Are there tough Win32®, MFC, COM, Microsoft Transaction Services, or COM+ programming questions you'd like answered? If so, drop me a note at jeffpro@msn.com. Please include Wicked Code in the message title. I regret that time doesn't permit me to respond to individual questions, but rest assured that each and every one will be considered for inclusion in a future column.

Jeff Prosise is the author of Programming Windows with MFC (Microsoft Press, 1999). He also teaches Visual C++, MFC, and COM programming seminars. For more information, visit https://www.prosise.com.