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:
BOOL CCeWizard::OnInitDialog()
{
BOOL bResult = CCePropertySheet::OnInitDialog();
HWND hWndTab;
hWndTab = ::GetDlgItem(m_hWnd, AFX_IDC_TAB_CONTROL);
if(hWndTab)
::ShowWindow(hWndTab, SW_HIDE);
ModifyStyle(0, WS_NONAVDONEBUTTON, SWP_NOSIZE);
SHDoneButton(m_hWnd, SHDB_HIDE);
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:
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
.
Handling the navigation commands is a simple matter. Here is the ID_BAR_BACK
handler:
void CCeWizard::OnBarBack()
{
SetActivePage(GetActiveIndex() - 1);
UpdateControls();
}
And now, the ID_BAR_NEXT
handler:
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:
void CCeWizard::UpdateControls()
{
int iIndex = GetActiveIndex(),
nPages = GetPageCount();
CToolBarCtrl& rToolBar = m_pWndEmptyCB->GetToolBarCtrl();
CWnd* pWndHdr;
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);
}
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:
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:
void CCeWizard::ResizePage()
{
CPropertyPage* pPage = GetActivePage();
if(pPage)
{
CRect rc;
pPage->GetWindowRect(&rc);
ScreenToClient(&rc);
rc.bottom += 22; 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:
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);
}
void CCeWizard::OnSettingChange(UINT uFlags, LPCTSTR lpszSection)
{
SHHandleWMSettingChange(m_hWnd,
(WPARAM)uFlags, (LPARAM)lpszSection, &m_sai);
}
And that's it!