Multi-Page Interface

Environment: VC6 and .NET

Introduction

Recently, I had to design the user interface for an FTP server. The main window had several pages (log, configuration, statistics, and so forth). On some of these pages, I needed many splitter windows and even some subpages containing nother splitter windows.... Finally, I had about 30 views! I needed to find a simple and flexible way to design such a complex interface.

The structure of the MPI interface is fully static. It means that views cannot be created, destroyed, or repositioned in the run-time. For many cases, like the mentioned FTP server, this is perfectly enough. Of course, there are fancier solutions, like those imitating the VS.NET docking windows, but this is not always necessary and often simply not worth the effort.

Also please note that this interface is not designed for traditional, document-oriented applications. It's meant for applications that perform some tasks, services, and so on, not having the Open/Save commands in the File menu nor an associated file extension. However, feel free to modify the MPI design to suit your needs.

Overview

Pages

The main pages of the interface are simply MDI child windows that are always maximized and don't have the system menu, so that they can't be closed or resized. I've chosen this solution because it's very simple to implement and works fine with the MFC architecture. Of course, the MPI interface may contain just one page.

Above the child frame, there is a rebar control containing my menu bar and toolbar (described in my previous article, toolbar/iebars.html). You don't have to use IEBars, but it just works (and looks) very well together. I also created a toolbar with the TBSTYLE_LIST style (with images and text in the buttons), CListToolBar, which is used to switch between the MPI pages. You may also use this class for other toolbars, as it supports standard MFC UpdateUI processing. You may use a different mechanism to switch pages if you need.

Splitters

Creating splitters and views manually in the child frame's is simple, as long as you don't have many nested splitters. The CMPIChildFrame class creates all spliters and views automatically. Usually, you will use this class instead of CChildFrame, but you may inherit your CChildFrame from CMPIChildFrame if you really need to. The layout can be defined by using just several macros in your application's InitInstance.

The standard MFC splitter window is tragic, so I used my previously created class, CDualSplitWnd. It only handles static splitters with two panes, which is all we need because it may be nested many times to obtain the requested layout. It retains a ratio between the size of the panes when it's resized, and it redraws both panes immediately when dragged with the mouse. The initial size ratio may be specified for the splitter.

Sometimes, it's useful when one of the splitter's panes has a particular size and cannot be resized. I designed another splitter, CBarSplitWnd, for this purpose. The constant pane's size may be given explicitly or determined automatically if the pane is a CScrollView derived class (particularly a CFormView class). This is especially useful for creating command bars using a CFormView.

Sub-pages

The most interesting feature of MPI is the ability to create sub-pages that may be switched with a tab control. As you may see in the image above, sub-pages may contain splitter windows and even own sub-pages. All you need to do to define the tabs is to create a toolbar resource containing the images and names of the tabs.

Using in Your Application

Note: for more information about the CMenuBar and CAlphaToolBar classes, please refer to the toolbar/iebars.html article.

Step 1: Create an MDI application using the MFC wizard.

Step 2: Add BarSplitWnd, DualSplitWnd, MPIChildFrame, MPIDocTemplate, MPITabCtrl, and MPITabWnd .cpp/.h files to your project.

Step 3: If you use IEBars, add AlphaImageList, AlphaToolBar, MenuBar, and ListToolBar as well.

Step 4: Remove ChildFrm.cpp/.h or change the base class of CChildFrame to CMPIChildFrame.

Step 5: Add the following lines to stdafx.h:

#define _WIN32_WINNT 0x0501

#include <afxtempl.h>

Step 6: If you use IEBars, add the CMenuBar and CAlphaToolBar to your main frame. Make sure you add all necessary message handlers.

Step 7: In CMainFrame, override OnUpdateFrameTitle() and add a public function UpdateMenu():

void CMainFrame::OnUpdateFrameTitle(BOOL bAddToTitle)
{
  CMDIFrameWnd::OnUpdateFrameTitle(FALSE);
}

void CMainFrame::UpdateMenu(HMENU hMenu)
{
  SetMenu(NULL);

  if (hMenu)
    m_wndMenuBar.AttachMenu(hMenu);
}

Step 8: If you have more than one page, add the CListToolBar to the main frame in a similar way as the CAlphaToolBar (see the demo project). Create a toolbar resource for it and create one button for each page. Enter the name of each button in the Prompt field after '\n'. Then manually add command handlers for the toolbar to the mainframe:

in MainFrm.h:

  afx_msg void OnMPIUpdate(CCmdUI* pCmdUI);
  afx_msg void OnMPICommand(UINT nID);

in MainFrm.cpp:

BEGIN_MESSAGE_MAP(CMainFrame, CMDIFrameWnd)
  ...
  ON_UPDATE_COMMAND_UI_RANGE(ID_MPI_FIRST, ID_MPI_THIRD,
                             OnMPIUpdate)
    ON_COMMAND_RANGE(ID_MPI_FIRST, ID_MPI_THIRD, OnMPICommand)
END_MESSAGE_MAP()

void CMainFrame::OnMPIUpdate(CCmdUI* pCmdUI)
{
  pCmdUI->Enable();

  CMPIChildFrame* pFrame = (CMPIChildFrame*)MDIGetActive();

  if (pFrame)
  {
    POSITION pos = AfxGetApp()->GetFirstDocTemplatePosition();
    CMPIDocTemplate* pDocTemplate = 
                     (CMPIDocTemplate*)AfxGetApp()->
                     GetNextDocTemplate(pos);

    ASSERT_KINDOF(CMPIDocTemplate, pDocTemplate);

    int nIndex = pDocTemplate->FindChildFrame(pFrame);

    pCmdUI->SetCheck(nIndex == (int)pCmdUI->m_nID -
           ID_MPI_FIRST);
  }
}

void CMainFrame::OnMPICommand(UINT nID)
{
  POSITION pos = AfxGetApp()->GetFirstDocTemplatePosition();
  CMPIDocTemplate* pDocTemplate = (CMPIDocTemplate*)AfxGetApp()->
                                  GetNextDocTemplate(pos);

  ASSERT_KINDOF(CMPIDocTemplate, pDocTemplate);

  CMPIChildFrame* pFrame = pDocTemplate->
                           GetChildFrame(nID - ID_MPI_FIRST);

  ASSERT_KINDOF(CMPIChildFrame, pFrame);

  MDIActivate(pFrame);
}

Replace ID_MPI_FIRST and ID_MPI_THIRD with your own IDs. You may also add those items to the menu resource.

Note: Make sure that the button IDs are consecutive numbers. You have to edit the values of the IDs if you reorder, insert, or remove buttons. Also make sure that the order of buttons is the same as the order you create the child frames.

Step 9: Create a string resource for each page containing text displayed in the title bar of the main window when that page is activated. You may remove the standard IRD_YourAppTYPE string and the icon resource.

Step 10: You may create individual menu and accelerator tables for each page. If a page doesn't have its own menu or accelerators, the default resources of the main frame will be used for that page. Remove the Close item from the File menu. Don't use standard document operations such as Open, Save, and so forth.

Step 11: Replace you application's InitInstance() with the following code:

BOOL CDemoApp::InitInstance()
{
  InitCommonControls();

  CWinApp::InitInstance();

  SetRegistryKey(_T("Local AppWizard-Generated Applications"));

  CMPIDocTemplate* pDocTemplate;
  pDocTemplate = new CMPIDocTemplate(
    IDR_MAINFRAME,
    RUNTIME_CLASS(CDemoDoc),
    RUNTIME_CLASS(CMPIChildFrame));
  AddDocTemplate(pDocTemplate);

  CMainFrame* pMainFrame = new CMainFrame;
  if (!pMainFrame->LoadFrame(IDR_MAINFRAME))
    return FALSE;

  m_pMainWnd = pMainFrame;

  CDocument* pDocument = pDocTemplate->CreateNewDocument();

  BEGIN_MPI_FRAME(pDocTemplate, IDR_FIRST)
    ...page layout...
  END_MPI_FRAME

  ...next pages...

  pMainFrame->MDIActivate(pDocTemplate->GetChildFrame(0));
  pMainFrame->ShowWindow(m_nCmdShow);
  pMainFrame->UpdateWindow();

  return TRUE;
}

IDR_MAINFRAME is the resource ID of the default menu and accelerator table for the MPI pages. Replace CDemoDoc with your document's class name. You need to include MPIDocTemplate.h, MPIChildFrame.h, and all your view class headers in the implementation file of your application.

For each page, insert the BEGIN/END_MPI_FRAME macros and define the layout using macros described below. The second argument for the BEGIN_MPI_FRAME macro is the resource ID of the string containing the name of the page, and optional menu and accelerator table for that page.

Layout macros

MPI_VIEW(_class)
MPI_VIEW_EX(_class, ex)

These macros define a single view created from a given class. The latter macro lets you define a startup parameter (which should be a pointer or an integer value) passed to the view object. It's very useful if you have several instances of a single view class and you want them to behave differently.

To read the startup parameter, you have to override Create() in your view's class and call the following static function:

DWORD CMPIDocTemplate::GetViewParam(pContext);

MPI_VSPLIT(prop)
MPI_HSPLIT(prop)

These macros define a vertical or horizontal splitter with two panes. This macro must be followed by two macros defining the panes content, which may be a single view, another splitter, or any other layout element. The prop parameter defines the initial ratio of panes (per cent). A value of 50 means that both panes are of equal size.

MPI_BAR_TOP(size)
MPI_BAR_BOTTOM(size)
MPI_BAR_LEFT(size)
MPI_BAR_RIGHT(size)

These macros define a bar splitter. One of the panes must have a constant size (given in pixels). If that pane contains a CScrollView-derived view (for example, a CFormView), you may use the macro MPI_BAR_TOP_A (and corresponding) and the size is automatically determined.

MPI_TABS(id)

This macro defines a set of sub-pages with a tab control. The ID parameter is the identifier of the toolbar resource used to create the tab control. This macro should be followed by as many macros as the number of buttons in the toolbar. Each tab may be a single view or any other element, including nested tabs.

This is an example of a complex layout structure that you may see in the picture above. Note the indents that help you understand the structure.

BEGIN_MPI_FRAME(pDocTemplate, IDR_FIRST)
    MPI_VSPLIT(20)
        MPI_VIEW(CDemoView)
        MPI_HSPLIT(70)
            MPI_TABS(IDR_TABS)
                MPI_BAR_TOP_A
                    MPI_VIEW(CDemoBarView)
                    MPI_VIEW(CDemoView)
                MPI_VSPLIT(30)
                    MPI_HSPLIT(50)
                        MPI_VIEW_EX(CDemoView, 1)
                        MPI_VIEW_EX(CDemoView, 2)
                    MPI_TABS(IDR_TABS)
                        MPI_BAR_TOP_A
                            MPI_VIEW(CDemoBarView)
                            MPI_VIEW_EX(CDemoView, 3)
                        MPI_VIEW_EX(CDemoView, 4)
            MPI_VIEW(CDemoView)
END_MPI_FRAME

Be careful to specify exactly as many child elements for each element as necessary. There should be only one root element in the structure (in the simplest case, it may be just one MPI_VIEW element). If you make a mistake, one of the assertions in CMPIChildFrame::CreateClientHelper will fail.

Author's Home Page

You may find the newest versions of my articles at www.mimec.w.pl.

Downloads

Download demo project - 60 Kb
Download source - 18 Kb


Comments

  • Tab selections

    Posted by Legacy on 12/13/2003 12:00am

    Originally posted by: Hein Mueck

    How can i obtain an interface (pointer) to specific tabs? How could the application itself select a tab-view?

    thanks!

    Reply
  • How do you read a file?

    Posted by Legacy on 11/29/2003 12:00am

    Originally posted by: Paul

    Your work is beautiful, but for those not as expert please clarify how I may read a file into the EditView window without using a dialog box.

    In a normal doc/view program, I would use OpenDocumentFile. This is incompatible with your program.


    Reply
  • You did a great job!! nevertheless there are some problems !!

    Posted by Legacy on 11/09/2003 12:00am

    Originally posted by: tigon7476

    error C2065: 'BTNS_BUTTON' : undeclared identifier
    error C2065: 'BTNS_AUTOSIZE' : undeclared identifier
    error C2065: 'BTNS_SEP' : undeclared identifier
    MainFrm.cpp
    MenuBar.cpp
    error C2065: 'BTNS_BUTTON' : undeclared identifier
    error C2065: 'BTNS_AUTOSIZE' : undeclared identifier
    error C2065: 'SPI_GETKEYBOARDCUES' : undeclared identifier
    error C2065: 'SPI_GETFLATMENU' : undeclared identifier
    error C2065: 'TPM_VERPOSANIMATION' : undeclared identifier
    error C2065: 'MIIM_FTYPE' : undeclared identifier
    error C2065: 'COLOR_MENUHILIGHT' : undeclared identifier
    error C2065: 'DT_HIDEPREFIX' : undeclared identifier

    there are some problems as upper errors....
    I'm working as Windows 2000 and VC++ 6.0. would you please tell me why these errors happened ?

    I had met these errors your another sample files...
    so I wonder why these errors happened so much !!!

    Reply
Leave a Comment
  • Your email address will not be published. All fields are required.

Top White Papers and Webcasts

  • Packaged application development teams frequently operate with limited testing environments due to time and labor constraints. By virtualizing the entire application stack, packaged application development teams can deliver business results faster, at higher quality, and with lower risk.

  • The hard facts on SaaS adoption in over 80,000 enterprises: Public vs. private companies Mid-market vs. large enterprise GoogleApps, Office365, Salesforce & more Why security is a growing concern Fill out the form to download the full cloud adoption report.

Most Popular Programming Stories

More for Developers

Latest Developer Headlines

RSS Feeds