Netscape 4.x Preferences Dialog

-->

When designing complex dialog boxes, one uses a property sheet whose functionality is encapsulated by MFC class CPropertySheet. However, there are several cases where alternative approach (dialog box similar to Netscape 4.x Preferences dialog box) is more flexible and user friendly.

  • When there are a lot of property pages in which case it is difficult to navigate thru the pages.
  • When there are several different sets of property pages whose visibility depends on the state of the application.
  • When there is a need for nested property pages.
Of course, there is always a possibility to derive a class from CPropertySheet and adapt it for the task at hand. However, this requires in depth knowledge of MFC implementation and also browsing the MFC source code which is not a trivial thing to do.

Basically, tree control contains a list names where each name is associated with a property page. This list may be (1) a simple list with no root, (2) list with a root which indicates the purpose of the visible set of property pages and (3) a list where one or more items contain one or more child items. When the user selects an item in a list, application automatically makes a property page associated with this item visible while all the other pages are invisible. It is also possible to replace the contents of the tree control with different lists according to the state of the application (for example, different list should be displayed for application configuration).

Implementation

Implementation of this dialog is split into several steps:

Step 1:

Design a dialog box. Put whatever you need on it. Also place a tree control and a picture control which is actually a placeholder for property pages. Let's assume that the ID of the tree control is IDC_TREE and the ID of the picture control is IDC_PLACEHOLDER. Picture control should be invisible.

Step 2:

Design a separate dialog for each of the property pages. Put whatever controls you need on each dialog. Each dialog shoould have the following properties:
  • No titlebar
  • Style Child
  • Disabled
  • No border
  • Invisible
Using class wizard, create a class for each property page dialog. Base class must be CDialog. Each property page must override virtual functions CDialog::OnCancel() and CDialog::OnOK() since their default implementation will close the dialog.

Step 3:

Create the following structure which describes all the data associated with a property page.
struct TItem {
  int OptionId;        // Unique identification of this page
  LPCTSTR Name;        // Property page name displayed in a tree control
  int DialogID;        // Resource ID of the property page dialog
  void *Handle;        // HTREEITEM
  void *Parent;        // HTREEITEM
  CDialog *Dialog;     // Pointer to property page dialog
  BOOL Created;        // Is the dialog create or not
  long HelpID;         // Help ID for a property page
};
Within your source file, create a constant array of these structures and initialize it.
static TItem Options[] = {
    { 1, "General settings",    IDD_PROPPAGE1, NULL, NULL, NULL, FALSE, HELP_GENERAL },
    { 2, "Login settings",      IDD_PROPPAGE2, NULL, NULL, NULL, FALSE, HELP_LOGIN },
    { 3, "Connection settings", IDD_PROPPAGE3, NULL, NULL, NULL, FALSE, HELP_CONNECTION },
    { 4, "About",               IDD_PROPPAGE4, NULL, NULL, NULL, FALSE, HELP_ABOUT },
};
You need also to implement the following function:
TItem *TMainDialog::insertOption(HTREEITEM hroot, int option, BOOL select)
{
    TItem *ptr = &(Options[option]);
    HTREEITEM handle;
    handle = GetDlgItem(IDC_TREE)->InsertItem(ptr->Name, hroot);
    ptr->Handle = (void *)handle;
    ptr->Parent = (void *)hroot;
    GetDlgItem(IDC_TREE)->SetItemData(handle,(DWORD)ptr);
    if (select)
        GetDlgItem(IDC_TREE)->SelectItem(handle);
    CDialog *dialog = createDialog(ptr->OptionId,ptr->DialogID);
    dialog->Create(ptr->DialogID,this);
    CRect rect;
    GetDlgItem(IDC_PLACEHOLDER)->GetWindowRect(&rect);
    ScreenToClient(&rect);
    dialog->SetWindowPos(NULL, rect.left, rect.top, 0, 0, 
      SWP_NOZORDER | SWP_NOSIZE | SWP_NOACTIVATE );
    dialog->EnableWindow(TRUE);
    ptr->Dialog = dialog;
    ptr->Created = TRUE;
    return ptr;
}// TPage1... are classes associated with property pages.
CDialog *TMainDialog::createDialog(int optionId, int dialogId)
{
    CDialog *dialog = NULL;
    switch (optionId) {
    case 1: dialog = new TPage1; break;
    case 2: dialog = new TPage2; break;
    case 3: dialog = new TPage3; break;
    case 4: dialog = new TPage4; break;
    };
    ASSERT(dialog != NULL);
    return dialog;
}
These two functions are responsible for property page creation. Notice that pointer to TItem structure is associated with an item in a tree control.

Step 4:

You need to fill the tree control and display the initial property page. This is done in OnOnitDialog() handler of the main dialog.
...
hroot = GetDlgItem(IDC_TREE)->InsertItem("Configuration");
insertOption(hroot,1,TRUE);    // Make first page visible
insertOption(hroot,2);
insertOption(hroot,3);
insertOption(hroot,4);
...
After this code, the tree control is filled, property pages are created and the first one is visible.

Step 5:

When the user selects another item in a tree control, you need to deactivate the currently visible property page and activate a new one (the one associated with the selected tree item). In order to perform this, following two functions are needed:
void TMainDialog::activateOption(TItem *item)
{
    ASSERT(item != NULL);
    Option = item;    // see below
    CDialog *dialog = Option->Dialog;
    ASSERT(dialog != NULL);
    dialog->ShowWindow(SW_SHOW);
    dialog->InvalidateRect(NULL);
    dialog->UpdateWindow();
}

void TMainDialog::deactivateOption(TItem *item)
{
    ASSERT(item != NULL);
    CDialog *dialog = item->Dialog;
    ASSERT(dialog != NULL);
    dialog->ShowWindow(SW_HIDE);
}
Option is a global variable of type TItem* which contains the currently selected item. In deactivateOption, visible property page is hidden. In activateOption, new property page is shown and updated (redrawn). Pointer to the new item structure is saved to global Option variable. These two functions are used in combination from the tree control handler triggered when the selection is changed (wither by the mouse or the keyboard). The following code handles the property page changes:
HTREEITEM handle = GetDlgItem(IDC_TREE)->GetSelectedItem();
if (handle != NULL) {
    TItem *item = (TItem*)GetDlgItem(IDC_TREE)->GetItemData(handle);
    if (item && (item != Option)) {
        deactivateOption(Option);
        activateOption(item);
    }
}

That's all. In a real world implementation, minor additions to the presented code are welcomed:

  • Create a base class for all property pages and add handlers executed when the page becomes active/inactive.
  • Add help for each page - trivial task since currently active page is accessable via Option variable and it contains a help ID for the page.
  • Presented code will create all the property pages and then only modify the visible flag for each page. It is possible to create/destroy pages when they are activated/deactivated. Which solution is appropriate depends on the application.

Conclusion

This is a very flexible solution for complex dialog boxes. It offers complete control of the creation/destruction of all the pages and allows dynamic modification of available property pages (not presented here). Since the basis is a plain dialog, it is possible to organize it anyway you like. The latest version of this article can be downloaded from my home page: www.scasoftware.com.