12.3 wxWizard

使用向导是将一堆复杂的选项变成一系列相对简单的对话框的一个好方法.它通常用来帮助应用程序的使用新手们开始使用某个特定的功能,比如,搜集建立新工程需要的信息,导出数据等等.向导中的选项通常都在应用程序别的用户界面上有体现,但是提供一个向导以便用户可以专注于那些为了完成某个任务必须要设置的选项.

向导控件通常在一个窗口内提供一系列的类似对话框的窗口,这些窗口的左边都拥有一副图片(可以是一样的也可以是不一样的),而底部则通常用一系列导航按钮,随着用户的选择进入预先设置好的下一页面,下一页面的索引是随用户的选择而变化的,因此不是所有的页面都会在一次向导执行过程中全部显示.

当标准的向导导航按钮被按下时,将会产生对应的事件,向导类或者其派生类可以捕获相应的事件.

要显示一个向导,你需要先创建一个向导(或者其派生类),然后创建子页面(作为向导的子窗口).你可以使用 wxWizardPageSimple类(或其派生类)来创建向导页面,然后使用wxWizardPageSimple::Chain函数将其和一个向导绑定.或者,如果你需要动态调整页面的顺序,你可以使用wxWizardPage的派生类,重载其GetPrev和GetNext函数. 然后将每一个页面增加到GetPageAreaSizer返回的布局控件中,以便向导控件自动将自己的大小调整到最大页面的大小.

向导控件只额外定义了wxWIZARD_EX_HELPBUTTON扩展类型,以便在标准向导导航按钮中增加帮助按钮.注意这是一个扩展类型,需要在Create之前使用SetExtraStyle来进行设置.

wxWizard事件

wxWizard产生wxWizardEvent类型的事件,如下表所示,这些事件将首先被发送到当前页面,如果页面没有定义处理函数, 则发送到向导本身.除了EVT_WIZARD_FINISHED事件以外,别的事件都可以调用wxWizardEvent::GetPage函数返回当前页面.

EVT_WIZARD_PAGE_CHANGED(id, func) 当导航页面已发生变化的时候产生,使用wxWizardEvent::GetDirection函数判断方向(true为向前).
EVT_WIZARD_PAGE_CHANGING(id, func) 当导航页面即将变化的时候产生,这个事件可以被Veto以便取消这个事件.同样可以使用wxWizardEvent::GetDirection判断方向(true为向前).
EVT_WIZARD_CANCEL(id, func) 用户点击取消按钮的时候产生,这个事件可以被Veto(使得事件导致的操作无效).
EVT_WIZARD_HELP(id, func) 用户点击帮助按钮的时候产生.
EVT_WIZARD_FINISHED(id, func) 用户点击完成按钮的时候产生.这个事件产生的时间是在向导对话框刚刚关闭以后.

wxWizard的成员函数

GetPageAreaSizer函数返回用户管理所有页面的布局控件.你需要将所有的页面增加到这个布局控件中,或者将某一个可以通过 GetNext函数访问到其它所有页面的页面增加到布局控件中,以便向导控件可以知道最大的页面的大小.如果你没有这样作,你需要在显示向导时在第一个页面显示之前调用其FitToPage函数,如果wxWizardPage::GetNext不能访问到所有的页面,你需要对每个页面调用 FitToPage函数.

GetCurrentPage函数返回当前活动的页面,如果RunWizard函数还没有被执行则返回NULL.

GetPageSize当前设置的页面大小. SetPageSize则用来设置所有页面使用的页面大小,不过最好还是将页面增加到GetPageAreaSizer布局控件中来决定页面大小比较合适.

调用RunWizard,传递要显示的第一个页面作为参数,以便将向导置于执行状态.如果向导执行成功这个函数返回True,如果用户取消了向导则返回False.

可以用SetBorder函数设置向导边界的大小,默认为0.

wxWizard使用举例

我们来看看wxWidgets自带的向导例子.它包含四个页面,如下图所示(页面索引并没有显示在对话框上,这样说只是为了清晰).

第一个页面非常简单,它不需要实现任何派生类,只是简单的创建了一个wxWizardPageSimple类的实例,然后在其中增加了一个静态文本标签,如下所示:

#include "wx/wizard.h"
wxWizard *wizard = new wxWizard(this, wxID_ANY,
                  wxT("Absolutely Useless Wizard"),
                  wxBitmap(wiztest_xpm),
                  wxDefaultPosition,
                  wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
// 第一页
wxWizardPageSimple *page1 = new wxWizardPageSimple(wizard);
wxStaticText *text = new wxStaticText(page1, wxID_ANY,
    wxT("This wizard doesn't help you\nto do anything at all.\n")
    wxT("\n")
    wxT("The next pages will present you\nwith more useless controls."),
         wxPoint(5,5));

第二页则实现了一个wxWizardPage的派生类,重载了其GetPrev和GetNext函数.前者总是返回第一页,而后者则可以根据用户的选择返回下一页或者最后一页.其声明和实现如下所示:

// 演示怎样动态改变页面顺序
// 第二页
class wxCheckboxPage : public wxWizardPage
{
public:
    wxCheckboxPage(wxWizard *parent,
                   wxWizardPage *prev,
                   wxWizardPage *next)
        : wxWizardPage(parent)
    {
        m_prev = prev;
        m_next = next;
        wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
        mainSizer->Add(
            new wxStaticText(this, wxID_ANY, wxT("Try checking the box below and\n")
                                   wxT("then going back and clearing it")),
            0, // 不需要垂直拉伸
            wxALL,
            5 // 边界宽度
        );
        m_checkbox = new wxCheckBox(this, wxID_ANY,
                         wxT("&Skip the next page"));
        mainSizer->Add(
            m_checkbox,
            0, // 不需要垂直拉伸
            wxALL,
            5 // 边界宽度
        );
        SetSizer(mainSizer);
        mainSizer->Fit(this);
    }
    // 重载wxWizardPage函数
    virtual wxWizardPage *GetPrev() const { return m_prev; }
    virtual wxWizardPage *GetNext() const
    {
        return m_checkbox->GetValue() ? m_next->GetNext() : m_next;
    }
private:
    wxWizardPage *m_prev,
                 *m_next;
    wxCheckBox *m_checkbox;
};

第三页实现了一个wxRadioboxPage类,它拦截取消向导和页面改变事件.如果你视图在这个页面取消向导,它会询问你是否真的要取消,如果你选择否,则它将使用事件的Veto函数来取消这个操作.OnWizardPageChanging函数则拦截所有的页面改变事件,并根据当前单选框的选项来确定是否允许页面改变.在实际应用程序中,你可以使用这种技术来确保向导在某一页的时候必须填充某些必须的域,否则不可以前进到下一页或者你可以出于某种原因阻止用户返回以前的页面.代码列举如下:

// 我们演示了另外一个稍微复杂一些的例子,通过拦截相应事件阻止用户向前或者向后翻页
// 或者让用户确认取消操作.
// 第三页
class wxRadioboxPage : public wxWizardPageSimple
{
public:
    // 方向枚举值
    enum
    {
        Forward, Backward, Both, Neither
    };
    wxRadioboxPage(wxWizard *parent) : wxWizardPageSimple(parent)
    {
        // 应该和上面的枚举值对应
        static wxString choices[] = { wxT("forward"), wxT("backward"), wxT("both"), wxT
("neither") };

        m_radio = new wxRadioBox(this, wxID_ANY, wxT("Allow to proceed:"),
                                 wxDefaultPosition, wxDefaultSize,
                                 WXSIZEOF(choices), choices,
                                 1, wxRA_SPECIFY_COLS);
        m_radio->SetSelection(Both);
        wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
        mainSizer->Add(
            m_radio,
            0, // 不伸缩
            wxALL,
            5 // 边界
        );
        SetSizer(mainSizer);
        mainSizer->Fit(this);
    }  
    // 事件处理函数
    void OnWizardCancel(wxWizardEvent& event)
    {
        if ( wxMessageBox(wxT("Do you really want to cancel?"), wxT("Question"),
                          wxICON_QUESTION | wxYES_NO, this) != wxYES )
        {
            // 不确认,取消
            event.Veto();
        }
    }
    void OnWizardPageChanging(wxWizardEvent& event)
    {
        int sel = m_radio->GetSelection();

        if ( sel == Both )
            return;
        if ( event.GetDirection() && sel == Forward )
            return;
        if ( !event.GetDirection() && sel == Backward )
            return;
        wxMessageBox(wxT("You can't go there"), wxT("Not allowed"),
                     wxICON_WARNING | wxOK, this);
        event.Veto();
    }
private:
    wxRadioBox *m_radio;
    DECLARE_EVENT_TABLE()
};

第四页也是最后一页,wxValidationPage,演示了重载transferDataFromWindow函数以便对复选框控件进行数据校验的方法.transferDataFromWindow在无论向前或者向后按钮被点击的时候都会被调用,而且如果这个函数返回失败,将会取消向前或者向后操作.和所有的对话框用法一样,你可以不必重载transferDataFromWindow函数而是给对应的控件设置一个验证器.这个页面还演示了怎样更改作为向导构造函数的一个参数的默认的左图片.下面是相关的代码:

// 第四页
class wxValidationPage : public wxWizardPageSimple
{
public:
    wxValidationPage(wxWizard *parent) : wxWizardPageSimple(parent)
    {
        m_bitmap = wxBitmap(wiztest2_xpm);
        m_checkbox = new wxCheckBox(this, wxID_ANY,
                          wxT("&Check me"));
        wxBoxSizer *mainSizer = new wxBoxSizer(wxVERTICAL);
        mainSizer->Add(
            new wxStaticText(this, wxID_ANY,
                     wxT("You need to check the checkbox\n")
                     wxT("below before going to the next page\n")),
            0,
            wxALL,
            5
        );
        mainSizer->Add(
            m_checkbox,
            0,
            wxALL,
            5
        );
        SetSizer(mainSizer);
        mainSizer->Fit(this);
    }
    virtual bool TransferDataFromWindow()
    {
        if ( !m_checkbox->GetValue() )
        {
            wxMessageBox(wxT("Check the checkbox first!"),
                         wxT("No way"),
                         wxICON_WARNING | wxOK, this);
            return false;
        }
        return true;
    }
private:
    wxCheckBox *m_checkbox;
};

下面的代码用于将所有的页面放在一起并且开始执行这个向导:

void MyFrame::OnRunWizard(wxCommandEvent& event)
{
    wxWizard *wizard = new wxWizard(this, wxID_ANY,
                    wxT("Absolutely Useless Wizard"),
                    wxBitmap(wiztest_xpm),
                    wxDefaultPosition,
                    wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER);
    // 向导页面既可以是一个预定义对象的实例
    wxWizardPageSimple *page1 = new wxWizardPageSimple(wizard);
    wxStaticText *text = new wxStaticText(page1, wxID_ANY,
         wxT("This wizard doesn't help you\nto do anything at all.\n")
         wxT("\n")
         wxT("The next pages will present you\nwith more useless controls."),
         wxPoint(5,5)
        );

    // ... 也可以是一个派生类的实例
    wxRadioboxPage *page3 = new wxRadioboxPage(wizard);
    wxValidationPage *page4 = new wxValidationPage(wizard);
    // 一种方便的设置页面顺序的方法
    wxWizardPageSimple::Chain(page3, page4);
    // 另外一种设置页面顺序的方法
    wxCheckboxPage *page2 = new wxCheckboxPage(wizard, page1, page3);
    page1->SetNext(page2);
    page3->SetPrev(page2);
    // 允许向导设置自适应的大小.
    wizard->GetPageAreaSizer()->Add(page1);
    if ( wizard->RunWizard(page1) )
    {
        wxMessageBox(wxT("The wizard successfully completed"),
         wxT("That's all"), wxICON_INFORMATION | wxOK);
    }
    wizard->Destroy();
}

当向导被完成或者取消的时候,MyFrame拦截了相关的事件,在这个例子中,只是简单的将其结果显示在frame窗口的状态条上.当然你也可以在向导类中拦截相应的事件.

完整版本的代码可以在附录J,"代码列表"或者随书光盘的examples/chap12目录中找到.