Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A Wizard-like property sheet for the Pocket PC

0.00/5 (No votes)
19 Sep 2003 5  
Implementing a wizard-like dialog on the Pocket PC using property sheets.

Sample Image - CeWizard.jpg

Introduction

It is common knowledge that the Pocket PC does not support wizard dialogs. Although the documentation says that the PSH_WIZARD flag is supported for property sheets, it does not seem to work as it should.

This article describes an implementation of a wizard-like dialog supported by property sheets, using a very simple tweak. There are other resources on creating wizard dialogs, most notably Daniel S.'s implementation using a dialog: QA: How can I create a wizard style dialog?. This article is the update of an article I had previously published: QA: How can I use a property sheet to implement a Wizard?.

In this second version of the article, the "visual glitch" reported by John Simmons is solved.

Pocket PC property sheets

In an article published here - see Property sheet callbacks in the Pocket PC 2002 - the property sheet callback mechanism is introduced and made compatible with MFC, through the CCePropertySheet class. By using a customized callback function, one can add the header and footer found in most Settings property sheets. The header and footer are set up by handling the PSCB_GETTITLE and PSCB_GETLINKTEXT callback messages.

What this article does not tell you is how the property sheet is assembled by the system, and how you can use this information for your own purposes. The property sheet is a dialog that contains the following items:

  • The property page dialog
  • The header. This is an optional static child control whose ID is 0x3028.
  • The tab control. It is a child control whose ID is 0x3020. MFC identifies it as AFX_IDC_TAB_CONTROL
  • The footer. This is an optional rich ink control whose text - set by PSCB_GETLINKTEXT - is interpreted as the parameter of a EM_INSERTLINKS message (see richink.h for more information).

The main idea of this article is that the tab control can be hidden and that wizard-like navigation can be implemented using SetActivePage(), GetActiveIndex() and GetPageCount().

Implementation

We are now prepared to implement a wizard-like dialog using a property sheet (CCeWizard). This class derives from CCePropertySheet, thereby allowing the user to insert headers and footers. The first thing we need to do is to set-up the dialog:

// CCeWizard::OnInitDialog
//
//        Initializes the wizard property sheet
//
BOOL CCeWizard::OnInitDialog() 
{
    BOOL            bResult = CCePropertySheet::OnInitDialog();
    HWND            hWndTab;
    
    //
    // Hide the tab
    //
    hWndTab = ::GetDlgItem(m_hWnd, AFX_IDC_TAB_CONTROL);
    if(hWndTab)
        ::ShowWindow(hWndTab, SW_HIDE);

    //
    // Hide the OK button
    //
    ModifyStyle(0, WS_NONAVDONEBUTTON, SWP_NOSIZE); 
    SHDoneButton(m_hWnd, SHDB_HIDE);

    //
    // Populate the toolbar
    //
    PopulateToolBar();

    UpdateControls();

    return bResult;
}

We will get to the PopulateToolBar() and UpdateControls() methods a bit later. Now, you may notice that after hiding the tab control, the line that separates the header from the dialog is gone too. Apparently, this line is a part of the tab control, and thus it is hidden. To circumvent this problem, we have to draw it ourselves, in the OnPaint method:

// CCeWizard::OnPaint
//
//        Paints the dialog
//
void CCeWizard::OnPaint() 
{
    CPaintDC    dc(this);
    CRect        rc;

    if(!m_strTitle.IsEmpty())
    {
        GetClientRect(&rc);

        dc.MoveTo(0, 23);
        dc.LineTo(rc.right, 23);
    }
}

Note that the line is drawn only if there is a title (m_strTitle belongs to the CCePropertySheet class).

Navigation

Now, we have to worry about navigating through the wizard: after hiding the tab control, we must provide a means for the user to flip through the several pages (property pages). The best place to put the control buttons is on the command bar. The CCeWizard class provides two options for placing controls on the command bar (although you can certainly override this functionality): graphic buttons or text buttons. If you want to provide graphic buttons (see top image), create a toolbar on the resource editor with at least four buttons: ID_BAR_OK, ID_BAR_CANCEL, ID_BAR_BACK and ID_BAR_NEXT. When you create your CCeWizard object, pass the toolbar ID as the second parameter to the constructor. To show text buttons (see next image), use 0 as the second parameter on the class constructor, and define the following string resources: IDS_BAR_OK, IDS_BAR_CANCEL, IDS_BAR_BACK and IDS_BAR_NEXT.

Wizard with text buttons

Handling the navigation commands is a simple matter. Here is the ID_BAR_BACK handler:

// CCeWizard::OnBarBack
//
//        Moves to the previous page
//
void CCeWizard::OnBarBack() 
{
    SetActivePage(GetActiveIndex() - 1);

    UpdateControls();
}

And now, the ID_BAR_NEXT handler:

// CCeWizard::OnBarNext
//
//        Moves to the next page
//
void CCeWizard::OnBarNext() 
{
    SetActivePage(GetActiveIndex() + 1);

    UpdateControls();
}

Updating controls

Now, let's see how to update the wizard's controls. This task is necessary in order to let the application's user know where in the wizard he or she is. This is done in two ways: updating the navigation buttons and reporting the progress in the wizard's header. All of this is achieved in just one method:

// CCeWizard::UpdateControls
//
//        Updates the command bar buttons
//
void CCeWizard::UpdateControls()
{
    int                iIndex = GetActiveIndex(),
                    nPages = GetPageCount();
    CToolBarCtrl&    rToolBar = m_pWndEmptyCB->GetToolBarCtrl();
    CWnd*            pWndHdr;

    //
    // Set the header text
    //
    pWndHdr = GetDlgItem(AFX_IDC_HEADER_CONTROL);
    if(pWndHdr)
    {
        CString    strMsg,
                strHeader;

        strMsg.Format(_T(" (%d/%d)"), iIndex + 1, nPages);

        strHeader = m_strTitle + strMsg;

        pWndHdr->SetWindowText(strHeader);
    }

    //
    // Enable or disable the back and next buttons if needed
    //
    rToolBar.EnableButton(ID_BAR_BACK, iIndex > 0);
    rToolBar.EnableButton(ID_BAR_NEXT, iIndex < nPages - 1);
    ResizePage();
}

Note that the navigation button's state is updated in the same way whether it is graphic or text.

Inserting the toolbar

Both the graphics and the text toolbars are inserted using just one method:

// CCeWizard::PopulateToolBar
//
//        Loads a graphics or button toolbar
//
void CCeWizard::PopulateToolBar()
{
    CCeCommandBar*    pCmdBar;

    pCmdBar = (CCeCommandBar*)m_pWndEmptyCB;

    if(m_idToolBar)
        pCmdBar->LoadToolBar(m_idToolBar);
    else
    {
        TBBUTTON    tbButton;
        CString        strMenu;

        memset(&tbButton, 0, sizeof(TBBUTTON));
        tbButton.iBitmap = I_IMAGENONE;
        tbButton.fsState = TBSTATE_ENABLED;
        tbButton.fsStyle = TBSTYLE_BUTTON | TBSTYLE_AUTOSIZE;

        strMenu.LoadString(IDS_BAR_OK);
        tbButton.iString    = (int)(LPCTSTR)strMenu;
        tbButton.idCommand    = ID_BAR_OK;
        pCmdBar->SendMessage(TB_INSERTBUTTON, 0, (LPARAM)&tbButton);

        strMenu.LoadString(IDS_BAR_CANCEL);
        tbButton.iString    = (int)(LPCTSTR)strMenu;
        tbButton.idCommand    = ID_BAR_CANCEL;
        pCmdBar->SendMessage(TB_INSERTBUTTON, 1, (LPARAM)&tbButton);

        strMenu.LoadString(IDS_BAR_BACK);
        tbButton.iString    = (int)(LPCTSTR)strMenu;
        tbButton.idCommand    = ID_BAR_BACK;
        pCmdBar->SendMessage(TB_INSERTBUTTON, 2, (LPARAM)&tbButton);

        strMenu.LoadString(IDS_BAR_NEXT);
        tbButton.iString    = (int)(LPCTSTR)strMenu;
        tbButton.idCommand    = ID_BAR_NEXT;
        pCmdBar->SendMessage(TB_INSERTBUTTON, 3, (LPARAM)&tbButton);
    }
}

Terminating the wizard

Terminating the wizard should not be done using a direct call to EndDialog(). My own experience showed me the hard way that this will not call the appropriate DDX and DDV routines. Instead, we send the IDOK and IDCANCEL commands directly to the property sheet.

The visual glitch

The first version of the code did not consider an inevitable side effect of hiding the tab control: the property sheet doesn't know it's hidden, so it will happily resize the child property page as though the tab were there. What happened was that some dialog real estate was being stolen (the area where the tab control was supposed to be). This was noted by John Simmons (and that is why his name is referenced in the image). Thank you, John!

Solving the glitch involved resizing the active page. This is done in the following method:

// CCeWizard::ResizePage
//
//        Resize the active property page
//
void CCeWizard::ResizePage()
{
    CPropertyPage*    pPage = GetActivePage();

    if(pPage)
    {
        CRect    rc;

        pPage->GetWindowRect(&rc);
        ScreenToClient(&rc);
        rc.bottom += 22;            // MAGIC NUMBER!!!
        pPage->MoveWindow(&rc);
    }
}

This method is called from a number of places in the code, especially from inside UpdateControls().

After testing the code, I found that using the SIP would revert to the old behavior: the lower 22 pixel strip was being stolen again. To solve this CCeWizard must handle the WM_ACTIVATE and WM_SETTINGCHANGE messages. The handlers just call the appropriate shell methods:

// CCeWizard::OnActivate
//
//        Handle the SIP correctly
//
void CCeWizard::OnActivate(UINT nState, CWnd* pWndOther, BOOL bMinimized)
{
    HWND    hWnd = NULL;

    if(pWndOther)
        hWnd = *pWndOther;

    SHHandleWMActivate(m_hWnd, MAKELPARAM(nState, 
            bMinimized), (LPARAM)hWnd, &m_sai, 0);
}


// CCeWizard::OnSettingChange
//
//        Handle the SIP correctly
//
void CCeWizard::OnSettingChange(UINT uFlags, LPCTSTR lpszSection)
{
    SHHandleWMSettingChange(m_hWnd, 
       (WPARAM)uFlags, (LPARAM)lpszSection, &m_sai);
}

And that's it!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here