Pocket PC 的类似向导的属性表





4.00/5 (8投票s)
使用属性表在 Pocket PC 上实现类似向导的对话框。

引言
众所周知,Pocket PC 不支持向导对话框。尽管文档称属性表支持 PSH_WIZARD 标志,但似乎并未按预期工作。
本文介绍了一种使用非常简单的技巧,通过属性表实现类似向导对话框的方法。还有其他关于创建向导对话框的资源,其中最著名的是 Daniel S. 使用对话框的实现:QA: How can I create a wizard style dialog?。本文是我之前发布的文章的更新版本:QA: How can I use a property sheet to implement a Wizard?。
在本文的第二版中,解决了 John Simmons 报告的“视觉故障”。
Pocket PC 属性表
在本文这里发表的一篇文章 - 请参阅 Property sheet callbacks in the Pocket PC 2002 - 中介绍了属性表回调机制,并通过 CCePropertySheet 类使其与 MFC 兼容。通过使用自定义的回调函数,可以添加大多数设置属性表中找到的页眉和页脚。页眉和页脚通过处理 PSCB_GETTITLE 和 PSCB_GETLINKTEXT 回调消息来设置。
本文未说明的是系统如何组装属性表,以及如何将此信息用于您自己的目的。属性表是一个包含以下项目的对话框:
- 属性页对话框
- 页眉。这是一个可选的静态子控件,其 ID 为 0x3028。
- 选项卡控件。这是一个子控件,其 ID 为 0x3020。MFC 将其标识为 AFX_IDC_TAB_CONTROL
- 页脚。这是一个可选的富文本控件,其文本 - 由 PSCB_GETLINKTEXT设置 - 被解释为EM_INSERTLINKS消息的参数(有关更多信息,请参阅richink.h)。
本文的主要思想是,可以隐藏选项卡控件,并且可以使用 SetActivePage()、GetActiveIndex() 和 GetPageCount() 来实现类似向导的导航。
实现
现在我们可以实现一个使用属性表(CCeWizard)的类似向导的对话框。此类派生自 CCePropertySheet,从而允许用户插入页眉和页脚。我们需要做的第一件事是设置对话框。
// 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;
}
稍后我们将处理 PopulateToolBar() 和 UpdateControls() 方法。现在,您可能会注意到,在隐藏选项卡控件后,分隔页眉和对话框的线条也消失了。显然,这条线是选项卡控件的一部分,因此它被隐藏了。为了绕过这个问题,我们必须在 OnPaint 方法中自己绘制它。
// 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);
    }
}
请注意,仅当有标题时(m_strTitle 属于 CCePropertySheet 类)才会绘制该线条。
导航
现在,我们必须处理向导的导航:在隐藏选项卡控件后,我们必须提供一种方法让用户翻阅各个页面(属性页)。放置控件按钮的最佳位置是命令栏。CCeWizard 类提供两个选项来将控件放置在命令栏上(尽管您可以绝对覆盖此功能):图形按钮或文本按钮。如果您想提供图形按钮(请参阅顶部图片),请在资源编辑器中创建一个工具栏,其中至少包含四个按钮:ID_BAR_OK、ID_BAR_CANCEL、ID_BAR_BACK 和 ID_BAR_NEXT。当您创建 CCeWizard 对象时,将工具栏 ID 作为第二个参数传递给构造函数。要显示文本按钮(请参阅下一张图片),请在类构造函数中使用 0 作为第二个参数,并定义以下字符串资源:IDS_BAR_OK、IDS_BAR_CANCEL、IDS_BAR_BACK 和 IDS_BAR_NEXT。

处理导航命令很简单。这是 ID_BAR_BACK 处理程序。
// CCeWizard::OnBarBack
//
//        Moves to the previous page
//
void CCeWizard::OnBarBack() 
{
    SetActivePage(GetActiveIndex() - 1);
    UpdateControls();
}
现在是 ID_BAR_NEXT 处理程序。
// CCeWizard::OnBarNext
//
//        Moves to the next page
//
void CCeWizard::OnBarNext() 
{
    SetActivePage(GetActiveIndex() + 1);
    UpdateControls();
}
更新控件
现在,让我们看看如何更新向导的控件。此任务是必要的,以便让应用程序用户了解他们在向导中的位置。这有两种方式:更新导航按钮并在向导的页眉中报告进度。所有这些都在一个方法中实现。
// 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();
}
请注意,无论导航按钮是图形还是文本,其状态的更新方式相同。
插入工具栏
图形和文本工具栏都使用同一个方法插入。
// 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);
    }
}
终止向导
不应使用直接调用 EndDialog() 来终止向导。我的亲身经历告诉我,这样做不会调用适当的 DDX 和 DDV 例程。相反,我们直接将 IDOK 和 IDCANCEL 命令发送到属性表。
视觉故障
代码的第一版没有考虑到隐藏选项卡控件的一个不可避免的副作用:属性表不知道它已隐藏,因此它会愉快地调整子属性页的大小,就好像选项卡存在一样。发生的情况是,一些对话框空间被占用了(选项卡控件本应在的区域)。这是 John Simmons 注意到的(这也是为什么他在图片中引用了他的名字)。谢谢你,John!
解决此故障涉及调整活动页面的大小。这是在以下方法中完成的。
// 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);
    }
}
此方法从代码中的多个位置调用,尤其是在 UpdateControls() 内部。
在测试代码后,我发现使用 SIP 会恢复到旧行为:较低的 22 像素条又被占用了。为了解决这个问题,CCeWizard 必须处理 WM_ACTIVATE 和 WM_SETTINGCHANGE 消息。处理程序只是调用适当的 shell 方法。
// 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);
}
就是这样!


