Windows 2000 Style Wizards

Environment: VC6 Windows 9x/NT/2000

A Page from the Windows 2000 Style Wizard

Introduction

There are two main differences between the traditional MFC-CPropertySheet wizards and the Windows 2000 wizards:

  1. The Windows 2000 wizard has a white background.
  2. The "page" portion of the Windows 2000 wizard extends all the way to the edge of the dialog.
The Microsoft GUI designers are fickle. I remember developing for Windows 3.1 and going through a lot of trouble to make grey dialog backgrounds instead of white backgrounds. Now we go through a lot of trouble to make white backgrounds instead of grey ones.

This article presents two classes that allows you to create Windows 2000 style wizards relatively easily. Both classes are actually derived from CDialog, rather than CPropertySheet or CPropertyPage. The classes make use of some techniques presented in Zoran M.Todorovic's CodeGuru article on stacked dialog boxes, however there are extensive code changes from Zoran's work.

Another big advantage of this method of creating wizards is that you have total control over the placement and text of the wizard buttons, and you can even add other control outside of the wizard pages.

The Classes

The CNewWizDialog Class

CNewWizDialog is derived from CDialog, but it is analagous to CPropertySheet when creating a standard MFC wizard. You create a dialog template with Back, Next, Cancel, and Finish buttons. You create a CNewWizDialog-derived class for your dialog resource.

The CNewWizPage Class

CNewWizPage is also derived from CDialog, however it is analagous to CPropertyPage when creating a standard MFC wizard. You create a dialog resource for each page of the wizard. You create a CNewWizPage-derived class for each wizard page.

CNewWizDialog and CNewWizPage have very similar class interfaces to CPropertySheet and CPropertyPage respectively, so it should not be too difficult to convert your existing wizards to windows 2000 style wizards.

Creating the Main Dialog

1. In the Visual C++ resource editor, create a dialog template for the main wizard window. Be sure to give it Next, Back, Finish, and Cancel buttons. Buttons should have the following identifiers to match the standard MFC wizard:

Cancel -- IDCANCEL
Finish -- ID_WIZFINISH
Back -- ID_WIZBACK
Next -- ID_WIZNEXT

Placing the Cancel and Finish Buttons

In the traditional MFC wizards, The Finish button and the Cancel button are in the same position. One is hidden when the other is shown. If you want to mimic that behavior in these classes, it is simple enough, but you will have to make changes to the CNewWizDialog::EnableFinish(BOOL bEnable) function to show are hide the proper buttons.

The Wizard Page Placeholder Control

You will also need to give your main wizard dialog a frame rectangle (picture) control to act as a placeholder. If you make the frame "etched", you will get a nicer appearance. Your dialog should look something like the one below.

Sample Image

2. Using ClassWizard, create a CDialog-derived class for your dialog resource. Change the base class from CDialog to CNewWizDialog. It is very important that you also change your message map to call the base class CNewWizDialog instead of CDialog as shown below. Otherwise, your wizards buttons will not work!


BEGIN_MESSAGE_MAP(CMasterDlg, CNewWizDialog)
 //{{AFX_MSG_MAP(CMasterDlg)
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

3. Override WM_INITDIALOG. Before calling the base class implementation of OnInitDialog, you must call CNewWizDialog::SetPlaceholderID() passing the control ID of the place holder rectangle as a parameter.

Be sure to call the base class implementation CNewWizDialog::OnInitDialog and not CDialog::OnInitDialog as show below:


BOOL CMasterDlg::OnInitDialog() 
{
 // you must call this function in OnInitDialog 
 // before you call the base class!
 SetPlaceholderID(IDC_SHEETRECT);

 // make sure to call the proper base class
 CNewWizDialog::OnInitDialog();
	
 return TRUE;  // return TRUE unless you set the focus to a control
               // EXCEPTION: OCX Property Pages should return FALSE
}

Creating the Wizard Pages

Follow the these steps for each page in your wizard.

1. Create a dialog resource for the page. The dialog should have the following properties:

  • Child Style
  • No Border
  • Not Visible
  • Disabled
Place the desired contols on the dialog.

Note: You should make each wizard page the same size as the placeholder on your main wizard dialog.

2. Use ClassWizard to create a CDialog-derived class for the dialog resource. Add any handlers for controls that you may require.

3. Change the base class from CDialog to CNewWizPage. It is very important that you change the base classes for OnInitDialog, the constructor, and the message map.


BEGIN_MESSAGE_MAP(CSetupPage, CNewWizPage)
 //{{AFX_MSG_MAP(CSetupPage)
 //}}AFX_MSG_MAP
END_MESSAGE_MAP()

4. Override any of the following functions to add functionality and data validation to your wizard page:
// these functions behave the same as CPropertyPage
virtual void CNewWizPage::OnCancel();
virtual BOOL CNewWizPage::OnKillActive();
virtual void CNewWizPage::OnSetActive();
virtual BOOL CNewWizPage::OnQueryCancel( );
virtual LRESULT CNewWizPage::OnWizardBack();
virtual LRESULT CNewWizPage::OnWizardNext();
virtual BOOL CNewWizPage::OnWizardFinish();

Putting it All Together

Once you have created all of your dialog classes as described above, you put the wizard together just as you would with a standard MFC CPropertySheet-based wizard. The only difference is that you must pass the ID of each pages's dialog resource to CNewWizDialog::AddPage().
CMasterDlg Dlg(this);
	
CSetupPage SetupPage;
CHardwarePage HardwarePage;
CPrinterPage PrinterPage;


Dlg.AddPage(&HardwarePage, CHardwarePage::IDD);
Dlg.AddPage(&SetupPage, CSetupPage::IDD);
Dlg.AddPage(&PrinterPage, CPrinterPage::IDD);

if (Dlg.DoModal() == ID_WIZFINISH)
{
 AfxMessageBox("Finished Wizard");
}
else
{
 AfxMessageBox("Cancelled Wizard");
}

Implementing the White Background

One of the new features in the Windows 2000 style wizards is a white background. This is pretty easy to implement, and it is also easy to disable if you do not like it.

1. In the declaration for CNewWizPage, we give the page it's own brush.

class CNewWizPage : public CDialog
{

[snip]

protected:
 CBrush m_Brush;

[snip]

2. Override WM_INITDIALOG and create a white brush.

BOOL CNewWizPage::OnInitDialog() 
{
 CDialog::OnInitDialog();
	
 // create the white brush for the background
 m_Brush.CreateSolidBrush(RGB(255, 255, 255));

 return TRUE;  // return TRUE unless you set the focus to a control
               // EXCEPTION: OCX Property Pages should return FALSE
}

3. Override the WM_CTLCOLOR message, and return the white brush and set text background colors as appropriate.


HBRUSH CNewWizPage::OnCtlColor(CDC* pDC, CWnd* pWnd, UINT nCtlColor) 
{
 HBRUSH hbr = CDialog::OnCtlColor(pDC, pWnd, nCtlColor);
	
 switch (nCtlColor)
 {	   
  case CTLCOLOR_STATIC:
   pDC->SetTextColor(RGB(0, 0, 0));
   pDC->SetBkColor(RGB(255,255,255));
  
  case CTLCOLOR_EDIT:
   pDC->SetTextColor(RGB(0, 0, 0));
   pDC->SetBkColor(RGB(255,255,255));

  case CTLCOLOR_LISTBOX:
   pDC->SetTextColor(RGB(0, 0, 0));
   pDC->SetBkColor(RGB(255,255,255));

  case CTLCOLOR_SCROLLBAR:
   pDC->SetTextColor(RGB(0, 0, 0));
   pDC->SetBkColor(RGB(255,255,255));

  case CTLCOLOR_BTN:
   pDC->SetTextColor(RGB(0, 0, 0));
   pDC->SetBkColor(RGB(255,255,255));

  case CTLCOLOR_DLG:	    
   return m_Brush;
 }	
	
 // TODO: Return a different brush if the default is not desired
 return m_Brush;
}

Potential Problems

Many of the data validation functions for CNewWizPage return FALSE if the dialog data is not validated as desired. I did not spend a lot of time checking to make sure the classes worked as required when FALSE is returned from these functions. If you are doing a lot of data validation, be sure to test your wizard with invalid data so that it behaves as desired.

Downloads

Download demo project - 151 Kb
Download source - 7 Kb


Comments

  • Skipping pages,...

    Posted by Legacy on 01/11/2004 12:00am

    Originally posted by: Nick

    Hello.,
    Thanks for your great article on Codeguru . I use your class CNewWizDialog , CNewWizPage for creating class wizard . I've 7 pages in my wizard . In certain conditions , I want to jump from 2nd page to 5th page by skipping 3rd & 4th page . Could you please tell me , how can I accomplish this task.?

    Thanks Again for your anticipation.!

    Warm Regards,
    TINKLE.

    Reply
  • Disable all button in a CProperty Page

    Posted by Legacy on 10/06/2003 12:00am

    Originally posted by: Moi

    How can i disable the button next, back simultanouesly ?

    Reply
  • XP Style Property Page?

    Posted by Legacy on 06/18/2003 12:00am

    Originally posted by: Mohit

    I am having some trouble with displaying property pages using the XP Style controls. I am using EnableThemeDialogTexture method. This only changes the look of the property page but not the controls (buttons etc.). Please help.


    Thanks in advance

    Reply
  • ActivatePage does not enable/disable buttons

    Posted by Legacy on 05/21/2003 12:00am

    Originally posted by: Stefan Hagel

    Very nice control. But if you jump to a certain page
    
    with "ActivatePage" the next, back, and finish buttons
    are not enabled/disabled anew.
    I added the following piece of code to the "ActivatePage"
    method which solves this problem if you have more than one
    page. If you have exacly one page in your wizard, you
    should need to scan some additional cases.

    if (pPage == GetLastPage())
    {
    EnableBack(TRUE);
    EnableNext(FALSE);
    EnableFinish(TRUE);
    }
    else if (pPage == GetFirstPage())
    {
    EnableBack(FALSE);
    EnableNext(TRUE);
    EnableFinish(FALSE);
    }
    else
    {
    EnableBack(TRUE);
    EnableNext(TRUE);
    EnableFinish(FALSE);
    }


    Reply
  • Reducing flicker with optimized redraw

    Posted by Legacy on 03/10/2003 12:00am

    Originally posted by: David Boyd

    I've enjoyed this control but I found it to have an annoying flicker switching between panes, especially on the image on the left side of the dialog.
    
    

    Here's a solution that optimizes the redraw.

    1. For all panes, give the picture control a unique id like IDC_IMAGE

    2. In NewWizDialog.cpp, make your OnInitDialog() look like this:

    BOOL CNewWizDialog::OnInitDialog()
    {
    CDialog::OnInitDialog();

    ModifyStyleEx (0, WS_EX_CONTROLPARENT);

    ASSERT(m_nPlaceholderID != 0);
    // your dialogs OnInitDialog
    // make the first page of the wizard active
    SetFirstPage();

    // This does an initial, complete redraw
    InvalidateRect(NULL, TRUE);

    return TRUE;
    }

    3. In NewWizDialog.cpp, make your ActivatePage() look like this:

    // returns TRUE if the new page is activated
    BOOL CNewWizDialog::ActivatePage(CNewWizPage* pPage)
    {
    ASSERT(m_nPlaceholderID != 0);
    ASSERT(pPage != NULL);
    ASSERT(::IsWindow(m_hWnd));

    // Turn off unnecessary redraw
    SetRedraw(FALSE);

    // if the page has not been created, then create it
    if (pPage->m_bCreated == FALSE)
    {
    if (pPage->Create(pPage->m_nDialogID, this) == FALSE)
    return FALSE;

    pPage->m_bCreated = TRUE;
    pPage->m_pParent = this;

    if (pPage->OnCreatePage() == FALSE)
    return FALSE;
    }

    // deactivate the current page
    if (!DeactivatePage())
    return FALSE;

    CRect rect;
    CWnd *pWnd = GetDlgItem(m_nPlaceholderID);
    ASSERT(pWnd != NULL);
    ASSERT(IsWindow(pWnd->m_hWnd) != FALSE);

    pWnd->GetWindowRect(&rect);
    ScreenToClient(&rect);
    pPage->SetWindowPos(NULL, rect.left, rect.top, 0, 0,
    SWP_NOZORDER | SWP_NOSIZE |
    SWP_NOACTIVATE );

    pPage->EnableWindow(TRUE);
    pPage->ShowWindow(SW_SHOW);

    // Allow redraw to happen again
    SetRedraw(TRUE);

    // Invalidate only the dialog controls
    pPage->GetDlgItem(IDC_IMAGE)->GetClientRect(rect);
    rect.left = rect.right;
    rect.right = rect.left + 500; // 500 is arbitrary

    // Invalidate just the dialog controls
    pPage->InvalidateRect(rect);
    pPage->UpdateWindow();

    pPage->OnSetActive();
    pPage->m_bActive = TRUE;

    return TRUE;
    }

    Reply
  • pages disappearered

    Posted by Legacy on 02/27/2003 12:00am

    Originally posted by: tom

    the problem that i encountered is sometimes when i pressed Enter key on the keyboard, all the property pages disappearered and left a blank base-dialog, and i need to click Next or Back button to reactive those pages.
    any help will be great appreciated.

    • my solution

      Posted by asyncevent on 04/06/2006 12:51pm

      override OnOK(),as masking enter pressed as in a standard dialog. like this: CTestPage::OnOK() { //CNewWizPage::OnOK(); }

      Reply
    • pages disappeared

      Posted by DanYELL on 02/03/2005 06:31pm

      Tom, Did you ever resolve this? I get the same thing. If its a multiline edit box, hitting enter takes me to the next line. If its a singleline edit box, the whole page goes blank. Very wierd. Please, any response you can give me will be greatly appreciated. Sincerely, Danielle Brina (an overworked graduate student)

      Reply
    Reply
  • Help

    Posted by Legacy on 02/16/2003 12:00am

    Originally posted by: Bruja

    very good article and code. but I need to jump to a diferent page on clicking NEXT depending on something that happens with the controls of the active page. How can I do it?

    Reply
  • Blank property sheet

    Posted by Legacy on 08/13/2002 12:00am

    Originally posted by: phil

    When i click close or finish in the wizard it closes but leave a plank property sheet with the standard buttons still open. I then have to click cancel to close it, does anyone know why this may be happening and a possible fix.

    Reply
  • tab order & mnemonics are not working

    Posted by Legacy on 08/06/2002 12:00am

    Originally posted by: tak

    The tab order on each page does not work. Only the tab order on the navigation buttons are working. In addition, if mnemnoic value is defined on each page such &Click me string in a label caption, pressing alternate key+'C' key cannot set the focus on the label.

    Any idea?

    Reply
  • String Table

    Posted by Legacy on 04/02/2002 12:00am

    Originally posted by: Rajesh

    hi,

    How i can modify the stringTable value dynamically. For Example in the String table contains IDS_HEADERTITLE "Sample Window".

    i want to change the IDS_HEADERTITLE. dynamically how i can do that?.

    thanks,
    Rajesh.(rajesh_s76@hotmail.com)

    Reply
  • Loading, Please Wait ...

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

Top White Papers and Webcasts

  • Live Event Date: December 11, 2014 @ 1:00 p.m. ET / 10:00 a.m. PT Market pressures to move more quickly and develop innovative applications are forcing organizations to rethink how they develop and release applications. The combination of public clouds and physical back-end infrastructures are a means to get applications out faster. However, these hybrid solutions complicate DevOps adoption, with application delivery pipelines that span across complex hybrid cloud and non-cloud environments. Check out this …

  • CentreCorp is a fully integrated and diversified property management and real estate service company, specializing in the "shopping center" segment, and is one of the premier retail service providers in North America. Company executives travel a great deal, carrying a number of traveling laptops with critical current business data, and no easy way to back up to the network outside the office. Read this case study to learn how CentreCorp implemented a suite of business continuity services that included …

Most Popular Programming Stories

More for Developers

RSS Feeds