Multi-page interface

Multi-page interface

Introduction

Recently I had to design the user interface for an FTP server. The main window had several pages (log, configuration, statistics etc.). On some of these pages I needed many splitter windows and even some subpages containing another splitter windows... finally I end up with about 30 views! I needed to find a simple and flexible way to design such 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 more fancy solutions, like those imitating 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 etc., 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 it simple, as long as you don't have many nested splitters. The CMPIChildFrame class creates all splitters and views automatically. Usually you will use this class instead of CChildFrame, but you may change the base class of CChildFrame to 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 since it may be nested many times to obtain the requested layout. It retains 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 which may be switched with a tab control. As you may see on 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 tabs names.

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 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 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 table 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 like Open, Save etc.

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

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);

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.

These macros define a bar splitter. One of the panes 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), the size is automatically determined.

This macro define 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.

Unique identifiers

Finding a specific view or splitter in a deeply nested splitter hierarchy can be a nightmare, especially if the layout of views is changed. In the 1.3 version of MPI an additional set of macros has been introduced. These macros are equivalent to the MPI_ macros, but they are prefixed MPID_ and have an additional parameter, uid. For example:

The uid is an unique identifier of a layout element (splitter, tabbed window or view) within a top-level page (child frame). It is a non-zero integer value. You may easily find the window with a given identifier using the following function:

CWnd* CMPIChildFrame::GetWndFromUID(int nUID);

Credits for this suggestion go to Till Toenshoff.

Example

This is an example of a complex layout structure which you may see on the picture above. Note the code indent which helps understanding the structure. All views have unique identifiers in this example.

BEGIN_MPI_FRAME(pDocTemplate, IDR_FIRST)
    MPI_VSPLIT(20)
        MPID_VIEW(1, CDemoView)
        MPI_HSPLIT(70)
            MPI_TABS(IDR_TABS)
                MPI_BAR_TOP_A
                    MPID_VIEW(2, CDemoBarView)
                    MPID_VIEW(3, CDemoView)
                MPI_VSPLIT(30)
                    MPI_HSPLIT(50)
                        MPID_VIEW_EX(4, CDemoView, 1)
                        MPID_VIEW_EX(5, CDemoView, 2)
                    MPI_TABS(IDR_TABS)
                        MPI_BAR_TOP_A
                            MPID_VIEW(6, CDemoBarView)
                            MPID_VIEW_EX(7, CDemoView, 3)
                        MPID_VIEW_EX(8, CDemoView, 4)
            MPID_VIEW(9, 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.

Version history

Version 1.3 (August 20, 2004)

Version 1.2 (September 2, 2003)

License

Multi-Page Interface, version 1.3 (August 20, 2004)
Copyright (C) 2003-2004 Michal Mecinski.

You may freely use and modify this code, but don't remove this copyright note.

THERE IS NO WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, FOR THIS CODE. THE AUTHOR DOES NOT TAKE THE RESPONSIBILITY FOR ANY DAMAGE RESULTING FROM THE USE OF IT.