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.


May 1999

Microsoft Systems Journal Homepage

C++ Q&A

Code for this article: May99CQA.exe (58KB)

Paul DiLascia is the author of Windows ++: Writing Reusable Code in C++ (Addison-Wesley, 1992) and a freelance consultant and writer-at-large. He can be reached at askpd@pobox.com or http://pobox.com/~askpd.

If you remember, last month (April 1999) I showed you how to write a class called CFolderTabCtrl that implements a simple folder-style tab control like the ones found in Microsoft® Excel and Visual Studio®. To demonstrate CFolderTabCtrl, I wrote a little test program FLDRTAB. It's a dialog-based app with a CFolderTabCtrl; when you click one of the tabs, FLDRTAB displays a message indicating which tab you clicked (see Figure 1).
Figure 1 FLDRTAB

    Figure 1 FLDRTAB

    FLDRTAB is fine for a test program, but if you want to mimic the look of Microsoft Excel or Visual Studio, there's a bit more work to do. In these applications, the folder tab control shares space with the horizontal scroll bar (see Figure 2). Well gosh, how do they do that? In this column—Folder Tabs, Part Two—I'll show you how to use two new classes, CFolderFrame and CFolderView.
Figure 2 Folder Tabs and Scroll Bars

    Figure 2 Folder Tabs and Scroll Bars

Figure 3 Original DibView

    Figure 3 Original DibView

    

Way back in the March 1997 issue, in one of my "MFC Goodies" articles, I wrote a bitmap viewer called DibView. DibView displays both the image and, below it, the information in the BITMAPINFOHEADER struct (see Figure 3). For this month's column, I modified DibView to use the new folder tab classes. The new, improved DibView displays the image and BITMAPINFOHEADER information in separate "pages" selected using the folder tab control. Figure 4 shows DibView with the Image page selected. Figure 5 shows it with the Bitmap Info page selected. There's also a Hex page (raw hex data), which is not shown. Personally, I rather doubt the new DibView represents a UI improvement since it would be better to show all the information at one time, but I'm not out to win any UI design awards in this month's column. I only want to show you how to implement the folder tab/scroll bar combo.
Figure 4 Modified DibView Image Page

    Figure 4 Modified DibView Image Page

    Whenever you want to modify or enhance the MFC application framework in some way, you should aim to touch the framework in as few places as possible. Brain surgery, not demolition, is the model. Keeping that in mind, the basic strategy I adopted was to insert a new window, CFolderFrame, between the frame and the view. Figure 6 illustrates the basic architecture. The main frame (or MDI child frame) contains CFolderFrame as a child window; CFolderFrame in turn contains the view, scroll bars, and folder tab control, and manages the interaction between them.
Figure 5 DibView Bitmap Info Page

    Figure 5 DibView Bitmap Info Page

    From the perspective of a programmer using CFolderFrame, there're only a few things you have to do. First—and this is where you get to apply the brain surgery scalpel—you have to override your frame's OnCreateClient function to create the folder frame window.



   BOOL CMainFrame::OnCreateClient(..., 
     CCreateContext* pcc)
   {
     return m_wndFolderFrame.Create(this,
       RUNTIME_CLASS(CDIBView), pcc,
       IDR_FOLDERTABS);
   }

    DibView supports both SDI and MDI versions. In the case of an MDI app, you want to override OnCreateClient in the MDI child frame. OnCreateClient is the function where MFC normally creates the view. Instead, you should create a CFolderFrame. Don't call the base class OnCreateClient! The folder frame will in turn create a view using the runtime class and the create context that you passed. IDR_FOLDERTABS is the ID of the string resource that holds the names of the tabs. If you want to specify tab names dynamically, you can omit this argument and call CFolderFrame::GetFolderTabCtrl to get the folder tab control, then CFolderTabCtrl::AddItem to add items (see last month's column for more details).
Figure 6  CFolderFrame Architecture

    Figure 6 CFolderFrame Architecture

    Next, you have to add some code to your view. The most important thing is to derive your view from CFolderView instead of CScrollView. You are using a scroll view, aren't you? The whole problem I'm discussing here is how to make the folder tab share space with the scroll bar. If you're not using a scroll view, there's no problem—you can create the folder tab control as a child of your view.

    Once you've derived your view from CFolderView, you're ready to handle notifications from the tab control. All you have to do is override a new virtual function CFolderView::OnChangedFolder. CFolderView calls it when the user clicks on a new folder tab. For DibView, the implementation is simple: just store the new page number and repaint.



 void CDIBView::OnChangedFolder(int iPage)
 {
   m_iPage = iPage;
   UpdateScrollSizes();
   Invalidate();
 }

    Of course, you also have to modify your view's OnDraw function to draw the right page. For DibView, this means doing a switch on m_iPage to draw the image, BITMAPINFOHEADER, or hex data depending on whether m_iPage is 0, 1, or 2. Finally, you have to add a line in CDIBView::OnInitialUpdate to show the folder frame controls:



 // in CDIBView::OnInitialUpdate
 GetFolderFrame()->ShowControls(pDIB ?
   CFolderFrame::bestFit : CFolderFrame::hide);

    CFolderFrame::ShowControls lets you hide or show all the controls (folder tab and scroll bars). It's handy for SDI apps where the frame starts out empty—that is, with no document/view. For DibView, this results in pDIB==NULL, in which case CDIBView::OnInitialUpdate passes CFolderFrame::hide to hide the controls; otherwise it passes CFolderFrame::bestFit, which instructs the folder frame to make the folder tab exactly as wide as necessary to display all the tabs, and use the rest of the window width for the scroll bar. If you want to use some other algorithm, you can compute the width you want and call CFolderFrame::ShowControls with a specific width.

    To summarize: create the folder frame in your frame's OnCreateClient, derive your view from CFolderView, and modify it in the obvious ways. This involves a minimum of fuss and delving into MFC.

    OK, now let's take a look behind the curtain and see how CFolderFrame and CFolderView actually work. CFolderFrame has data members to hold the scroll bars and folder tab control and the tab control's width. When you call CFolderFrame::Create, it creates the folder frame first, then the view. It uses the context information to create the view just as MFC would. Note, however, that CFolderFrame creates the view as a child of itself, not of the main frame (or MDI child). As for the controls—folder tab control, horizontal and vertical scroll bars, and size box—CFolderFrame creates them in its OnCreate handler. Pretty simple. One important little detail you don't want to forget if you try this at home is to set the folder frame's WS_EX_CLIENTEDGE style so your window retains the sunken look characteristic of Windows® 9x and Windows NT® 4.0. CFolderFrame does it in PreCreateWindow.

    Once CFolderFrame has created its windows, it has to manage their sizes. Naturally, it does this in OnSize. The details are pure arithmetic, so I won't bore you here; see the source code in Figure 7. The folder frame gets the width of the folder tab from m_cxFolderTabCtrl. There's a function called SetFolderTabWidth to set it, but generally you don't need to use this function since the folder frame attempts to use the ideal width as returned from CFolderTabCtrl::GetDesiredWidth. Once OnSize is working properly, you can see how the folder tab control simulates a folder tab (see Figure 5): the white top edge of the selected tab blends with the white background of the view to simulate a continuous region, even though the space is composed of two separate windows.

    If all this seems straightforward, it's because I've saved the best part for last: how does scrolling work? How does the folder view get MFC to use the scroll bars in the folder frame instead of the ones MFC wants to use? When I first set out to implement the divided scroll bar—which is, after all, the raison d'etre for CFolderFrame—I expected this problem would give me a Motrin-sized brainache. Normally, the original CDIBView class (from the March 1997 issue) is derived from CScrollView, which hides and shows the scroll bars as it pleases. But I wanted the scroll view to use my scroll bars, the ones in the folder frame—at least for the horizontal scroll bar. How else to change its size, to make room for the folder tabs?

    So I began spelunking through CScrollView to see how I might perform this surgery when—lo and behold—I discovered that all I had to do was override a single virtual function: CWnd::GetScrollBarCtrl. This function takes an argument that's either SB_HORZ or SB_VERT, and returns the corresponding scroll bar window. The default implementation returns Null, in which case MFC, through a long chain of logic, creates the scroll bars it needs (by calling ::ShowScrollBar). But, CFolderView overrides GetScrollBarCtrl like so:



   CScrollBar* CFolderView::GetScrollBarCtrl(int nBar) const
   {return GetFolderFrame()->GetScrollBar(nBar);}
That is, it passes the buck to CFolderFrame:


   CScrollBar* CFolderFrame::GetScrollBar (int nBar)
   {
     return nBar==SB_HORZ ? &m_wndSBHorz
       : nBar==SB_VERT ? &m_wndSBVert : NULL;
   }
It's really that easy! The flexibility of the MFC design is truly amazing. I repeat: all you have to do to use your own scroll bars with a CScrollView is override GetScrollBarCtrl. For example, you might invent a super scroll bar with psychedelic colors or a sticky slider. Wow.

    There is just one little catch for folder tabs, however: since the scroll bars are children of the folder frame, not the view, CFolderFrame gets the scroll messages (WM_HSCROLL and WM_VSCROLL), so it has to manually route them to the view:



 void CFolderFrame::OnHScroll(...)
 {
   GetView()->SendMessage(WM_HSCROLL, ...);
 }
Ditto for WM_VSCROLL. Once you add these trivial pieces to the scrolling puzzle, scrolling works perfectly, just as it would in a vanilla CScrollView.

    In truth, GetScrollBarCtrl is the main reason for having CFolderView, though CFolderView also converts the FTN_TABCHANGED notification from the folder tab control into a handy virtual function call to OnChangedFolder, so handling the notification is just a matter of overriding the virtual function in your view.

    Well, there you have it. CFolderFrame and CFolderView don't implement every feature you might want from a folder tab UI—for example, they don't provide the left/right and begin/end buttons that Microsoft Excel shows in Figure 2—but they do provide the basic framework for adding such features. All you'd have to do is add the buttons as additional controls to the folder tab control, and pass the BN_CLICKED notifications up the food chain. Or maybe you need a button that causes your program to cycle rapidly through all the folder pages to produce a frenzied, manic behavior whenever you want to take your aggression out on your computer. Whatever.

    The entire DibView program is too long to print here; Figure 7 only shows the code for CFolderFrame and CFolderView. CFolderTabCtrl is the same as last month. Of course, you can always get the full source from here.

Have a question about programming in C or C++? Send it to Paul DiLascia at askpd@pobox.com


From the May 1999 issue of Microsoft Systems Journal