MFC没有提供现成的向导对话框,而是提供了CPropertySheet和CPropertyPage两个类,分别表示向导容器、向导页。为了制作出向导风格的对话框,需要从CPropertySheet继承一个自己的类,控制每个页面的显示,以及制作每个页面的对话框,并基于CPropertyPage类实现这些对话框类。关于使用CPropertySheet和CPropertyPage制作向导的文章,网上有很多介绍,我就不描述了。
    使用MFC提供的类实现的向导都有统一的风格,而很多时候,我们就需要不一样的向导,而MFC无法或不能很好实现这些功能,如定制自己的“上一步”、“下一步”按钮风格等。这里我就个人的经验,提供一个不使用CPropertySheet和CPropertyPage制作向导的方法,虽然稍微繁琐点,但是能够提供更多的灵活性,也能够锻炼能力。我将使用VSS2010完成所有设计,VS2012、VS2008和VC6的操作同样如此。
    首先新建一个MFC对话框项目MyWizard,VS2010会自动创建一个CMyWizardDlg对话框,我将以此为向导的主界面。在资源编辑器中打开这个对话框,删除默认的“Cancel”、“OK”,并增加三个按钮,分别作为”上一步”、”下一步”、”取消”,以及一个Static控件,它将控制向导页的显示范围,即所有向导页都将显示在此Static控件中。如下图所示:

自行设计MFC向导对话框_向导对话框

接着就要添加实际显示的向导页面了,这里我将添加三个对话框作为显示页,它们都是普通的对话框,需要设置边框属性(Border)为None,即没有对话框,风格属性(Style)为Child,即它们将作为子对话框显示在父窗口中,其他的属性值保留默认即可。添加的对话框分别如下图所示:

自行设计MFC向导对话框_Wizard_02

          页面一,IDD_STEP_ONE

自行设计MFC向导对话框_向导对话框_03

           页面二,IDD_STEP_TWO

自行设计MFC向导对话框_向导对话框_04

            页面三,IDD_STEP_THREE
这里我只是要演示向导对话框的生成,至于每个页面对话框上内容就无从理会了,大家可以根据需要自行修改。新建好这三个对话框后,就要为它们添加类了,在VSS2010中可以通过右键单击对话框,选择“添加类”菜单完成。需要注意的是,这里只需要提供类名称即可,其他设置保留默认,尤其是基类,默认的是CDialogEx或CDialog,而不是MFC的CPropertyPage类。我将添加CStepOneDlg、CStepTwoDlg、CStepThreeDlg三个类,如下图所示。

自行设计MFC向导对话框_MFC_05

为了使每个向导页都以相同的方式得知自己将要被激活,即作为当前页面显示,以及上一步、下一步操作,我设计了如下抽象类:
         class IWizardPage
         {
         public:
 
                  virtual DWORD OnWizardActive() = 0;
 
                  virtual DWORD OnWizardNext() = 0;
 
                  virtual DWORD OnWizardPrevious() = 0;
 
                  virtual DWORD OnWizardCancel() = 0;
         };
通过让CStepOneDlg、CStepTwoDlg、CStepThreeDlg都继承该抽象类并实现其中的方法,在主对话框进行相关操作时就能够以统一的方式通知每个属性页对话框了。下面将以CStepOneDlg为例,说明这些方法的实现内容:
    DWORD CStepOneDlg::OnWizardActive()
    {
        //显示当前页前可做准备工作,如初始化页面,加载数据等
        ……
        //显示当前页
        ShowWindow(SW_SHOW);
        return 0;
    }
 
    DWORD CStepOneDlg::OnWizardNext()
    {
        //判断当前页的状态,是否允许跳到下一步
        //如果状态非法,则返回非0值,表示需要停留在本页面
        ……
        //隐藏本窗口,返回0表示可以显示下一页了
        ShowWindow(SW_HIDE);
        return 0;
    }
 
    DWORD CStepOneDlg::OnWizardPrevious()
    {
        //判断当前页的状态,是否允许跳到上一步
        //如果状态非法,则返回非0值,表示需要停留在本页面
        ……
        //隐藏本窗口,返回0表示可以显示上一页了
        ShowWindow(SW_HIDE);
        return 0;
    }
 
    DWORD CStepOneDlg::OnWizardCancel()
    {
        //首先判断当前状态是否允许取消操作
        //如果状态非法,则返回非0值,表示需要停留在本页面
        ……
        //隐藏本窗口,返回0表示可以退出向导了
        ShowWindow(SW_HIDE);
        return 0;
    }
所有属性页对话框都将在主对话框上显示,要在CMyWizardDlg中增加三个变量:CStepOneDlg *m_lpStepOne、CStepTwoDlg *m_lpStepTwo、CStepThreeDlg *m_lpStepThree。因为这些对话框将要作为子页面嵌套在主对话框中,属于非模态对话框,需要手动创建,所以这里定义指针类型,并在构造函数中将它们初始化为NULL,以及在析构函数或OnDestroy函数中释放它们。
有了主窗口以及三个属性页后,要做的就是如何显示它们,在CMyWizardDlg类中增加如下两个函数,用于显示上一页、下一页的操作:
void CMyWizardDlg::ShowNextPage(int iStep)
{
    //所有按钮初始化状态可用
    GetDlgItem(IDB_PREV)->EnableWindow(TRUE);
    GetDlgItem(IDB_NEXT)->EnableWindow(TRUE);
    GetDlgItem(IDB_CANCEL)->EnableWindow(TRUE);
 
    // 页面显示位置
    CRect rect;
    GetDlgItem(IDC_STATIC_PANEL)->GetWindowRect(&rect);
    ScreenToClient(&rect);
 
    if (iStep == 1) //显示第一页
    {
        if (m_lpStepOne == NULL)
        {
            m_lpStepOne = new CStepOneDlg();
            m_lpStepOne->Create(IDD_STEP_ONE, this);
        }
        m_lpStepOne->MoveWindow(rect, TRUE);
        m_lpStepOne->OnWizardActive();
 
        GetDlgItem(IDB_PREV)->EnableWindow(FALSE);
    }
    else if (iStep == 2)    //显示第二页
    {
        //判断当前页是否可以进行下一步
        if (m_lpStepOne->OnWizardNext() != 0)
        {
            return;
        }
 
        if (m_lpStepTwo == NULL)
        {
            m_lpStepTwo = new CStepTwoDlg();
            m_lpStepTwo->Create(IDD_STEP_TWO, this);
        }
        m_lpStepTwo->MoveWindow(rect, TRUE);
        m_lpStepTwo->OnWizardActive();
    }
    else if (iStep == 3)    //显示第三页
    {
        //判断当前页是否可以进行下一步
        if (m_lpStepTwo->OnWizardNext() != 0)
        {
            return;
        }
 
        if (m_lpStepThree == NULL)
        {
            m_lpStepThree = new CStepThreeDlg();
            m_lpStepThree->Create(IDD_STEP_THREE, this);
        }
        m_lpStepThree->MoveWindow(rect, TRUE);
        m_lpStepThree->OnWizardActive();
 
        GetDlgItem(IDB_NEXT)->EnableWindow(FALSE);
    }
}
 
void CMyWizardDlg::ShowPrePage(int iStep)
{
    //所有按钮初始化状态可用
    GetDlgItem(IDB_PREV)->EnableWindow(TRUE);
    GetDlgItem(IDB_NEXT)->EnableWindow(TRUE);
    GetDlgItem(IDB_CANCEL)->EnableWindow(TRUE);
 
    // 页面显示位置
    CRect rect;
    GetDlgItem(IDC_STATIC_PANEL)->GetWindowRect(&rect);
    ScreenToClient(&rect);
 
    if (iStep == 1) //显示第一页
    {
        //判断当前页是否可以进行上一步
        if (m_lpStepTwo->OnWizardPrevious() != 0)
        {
            return;
        }
 
        if (m_lpStepOne == NULL)
        {
            m_lpStepOne = new CStepOneDlg();
            m_lpStepOne->Create(IDD_STEP_ONE, this);
        }
        m_lpStepOne->MoveWindow(rect, TRUE);
        m_lpStepOne->OnWizardActive();
 
        GetDlgItem(IDB_PREV)->EnableWindow(FALSE);
    }
    else if (iStep == 2)    //显示第二页
    {
        //判断当前页是否可以进行上一步
        if (m_lpStepThree->OnWizardPrevious() != 0)
        {
            return;
        }
 
        if (m_lpStepTwo == NULL)
        {
            m_lpStepTwo = new CStepTwoDlg();
            m_lpStepTwo->Create(IDD_STEP_TWO, this);
        }
        m_lpStepTwo->MoveWindow(rect, TRUE);
        m_lpStepTwo->OnWizardActive();
    }
}
实现“上一步”、“下一步”按钮事件响应函数如下:
 
void CMyWizardDlg::OnBnClickedPrev()
{
    m_iStep -= 1;
    ShowPrePage(m_iStep);
}
 
void CMyWizardDlg::OnBnClickedNext()
{
    m_iStep += 1;
    ShowNextPage(m_iStep);
}
 
最后,实现“取消”按钮:
 
void CMyWizardDlg::OnBnClickedCancel()
{
    if (m_iStep == 1 && m_lpStepOne->OnWizardCancel() != 0)
    {
        return;
    }
    else if (m_iStep == 2 && m_lpStepTwo->OnWizardCancel() != 0)
    {
        return;
    }
    else if (m_iStep == 3 && m_lpStepThree->OnWizardCancel() != 0)
    {
        return;
    }
}
        编译运行后,可得到如下图所示的结果:

自行设计MFC向导对话框_向导对话框_06

       以上就是简单的Wizard制作过程,这里我只是简单的介绍了这种实现方式,大部分内容都需要按照实际的应用进行修改。另外,为了方便父窗口与属性页之间的通信,除了属性页继承自IWizardPage接口外,最好设计IWizard接口,CMyWizardDlg继承自该接口,并将CMyWizardDlg的指针传递给属性页,这样属性页就可以回调父窗口了。