作者:梁祺 (eclipsesbs@gmail.com)
来自:http://www.benisoft.net/day9/index.html
WizardDialog称为向导对话框,当用户需要输入大量信息时,向导对话框可以循序渐进地引导用户, 使得用户在每一个向导对话框页面里只需要专注于输入少量信息。并且向导对话框可以根据用户的输入, 判断用户是否完成了该页面,决定下一个向导对话框页面。 由于向导对话框是用于帮助用户输入大量且复杂的信息,它本身就很复杂,所以它非常需要仔细的设计, 经验表明,一个设计不合理的向导对话框是很难通过后期修改来完全满足需求,往往需要重新设计。
在Itinerary的例子里,我们需要一个向导对话框来创建行程活动Itinerary Item。行程活动有三种, 交通(Transportation),住宿(Accommodation),和观光(Activity)。 我们不希望用三个独立的对话框来分别创建它们,而是用一个向导对话框。这个向导对话框有三页, 第一页选择活动类型,交通,住宿,还是观光;根据第一页中选择的内容, 用户在第二页输入交通,住宿或观光的详细内容;第三页输入该行程活动的备注信息。
创建向导对话框,首先创建继承自Wizard的NewItineraryItemWizard类, Wizard主要提供向导对话框所要处理的数据对象,并且控制所有向导对话框页面。 其次就是创建一系列继承自WizardPage的向导对话框页面类。
- NewItineraryItemWizard:保存向导对话框所要处理的数据对象,并且控制向导对话框页面
- NewItineraryItemTypeWizardPage:第一页,选择行程活动类型
- NewItineraryItemTransportationWizardPage:第二页,输入交通信息
- NewItineraryItemAccommodationWizardPage:第二页,输入住宿信息
- NewItineraryItemActivityWizardPage:第二页,输入观光信息
- NewItineraryItemDescriptionWizardPage:第三页,输入备注信息
用户选择主菜单的"Itinerary -> New Itinerary Item",会调用NewItineraryItemAction。 在这个Action类的run()中,先创建一个NewItineraryItemWizard对象,然后将该对象交给WizardDialog的构造函数, 最后调用WizardDialog.open()方法打开对话框。
WizardDialog dialog = new WizardDialog(window.getShell(), wizard);
if (Window.OK == dialog.open()) {
// OK
}
NewItineraryItemWizard负责实例化所有的WizardPage,并调用addPage()把它们加到Wizard维护的页面列表中。 这里的WizardPage成员变量命名比较偷懒,但也一目了然,page2A,page2B,page2C就是第二页的三个可能的页面。
private NewItineraryItemTypeWizardPage page1;
private NewItineraryItemTransportationWizardPage page2A;
private NewItineraryItemAccommodationWizardPage page2B;
private NewItineraryItemActivityWizardPage page2C;
private NewItineraryItemDescriptionWizardPage page3;
public NewItineraryItemWizard() {
page1 = new NewItineraryItemTypeWizardPage( "page.type");
addPage(page1);
page2A = new NewItineraryItemTransportationWizardPage( "page.transportation");
addPage(page2A);
page2B = new NewItineraryItemAccommodationWizardPage( "page.accommodation");
addPage(page2B);
page2C = new NewItineraryItemActivityWizardPage( "page.activity");
addPage(page2C);
page3 = new NewItineraryItemDescriptionWizardPage( "page.description");
addPage(page3);
}
当用户在第一页中选择了交通,住宿或者观光后,Wizard就知道第二页该使用哪个页面了。 getNextPage()和getPreviousPage()类似,都是根据当前页面以及行程活动类型来确定下一页或者上一页。
if (page == page1) {
String type = page1.getType();
if (type.equals( "transportation")) {
return page2A;
} else if (type.equals( "accommodation")) {
return page2B;
} else {
return page2C;
}
} else if (page == page2A || page == page2B || page == page2C) {
return page3;
}
return null;
}
接下来是WizardPage,也是代码量最多的地方。WizardPage主要负责提供界面与用户交互,检查用户的输入, 获取用户输入的数据。这个和普通的对话框没有什么区别。第一个NewItineraryItemTypeWizardPage非常简单, 只有一个Combo控件,用来选择行程活动的类型,也不需要检查输入。
第二页我们以NewItineraryItemTransportationWizardPage为例。这里我们需要检查一些必须输入的域, 其中我们还需要检查航班出发和到达时间的格式,如果没有输入或者输入格式不正确, WizardPage的标题区域会先显示红色的错误提示。
为了实时检查用户输入,我们需要为每个控件添加事件接收器,以便在控件内容改变时能接收到事件,实时检查输入的合法性。 一般文本控件可以使用ModifyListener,Combo控件可以使用SelectionListener。当收到事件时, 我们调用isInputValid()来检查控件里的用户输入,如果输入合法,isInputValid()返回true, 并调用setPageComplete()来告诉WizardPage,当前页已完成。
@Override
public void modifyText(ModifyEvent e) {
setPageComplete(isInputValid());
}};
textNumber.addModifyListener(listener);
textDepartureTime.addModifyListener(listener);
textDepartureStation.addModifyListener(listener);
textArrivalTime.addModifyListener(listener);
textArrivalStation.addModifyListener(listener);
这里简单看一下isInputValid(),每个输入项都有自己的合法性要求,如果不合法,调用setErrorMessage(), 设置错误消息提示用户,并返回false。如果所以输入项都合法,调用setErrorMessage(null)清除错误消息, 返回true。
if (comboType.getText().isEmpty()) {
this.setErrorMessage( "Input transportation type.");
return false;
}
if (textNumber.getText().isEmpty()) {
this.setErrorMessage( "Input " + comboType.getText().toLowerCase() + " number.");
return false;
}
if (! textDepartureTime.getText().isEmpty()) {
Date date = getValidDate(textDepartureTime.getText());
if (date == null) {
this.setErrorMessage( "Input departure time in format yyyy-MM-dd HH:mm.");
return false;
}
}
...
this.setErrorMessage( null);
transportation = new Transportation();
transportation.setTransportationType(getType());
transportation.setNumber(textNumber.getText());
transportation.setDepartureTime(getValidDate(textDepartureTime.getText()));
transportation.setDeparturePlace(textDepartureStation.getText());
transportation.setArrivalTime(getValidDate(textArrivalTime.getText()));
transportation.setArrivalPlace(textArrivalStation.getText());
transportation.setDescription(textDescription.getText());
return true;
只有当调用setPageComplete(true)后,用户才可以点击Next进入下一页,或者点击Finish按钮完成整个向导对话框。 在上面这个例子里,第一页和第二页是必须的,第三页是可选的,所以第一页的Finish按钮非活动状态,不能点, 第二页和第三页Finish按钮处于活动状态。为了定制Finish按钮的行为,我们需要重载Wizard的canFinish()方法, 如果必选页都处于完成状态,向导对话框就可以Finish。
String type = page1.getType();
if (type.equals( "transportation")) {
return page1.isPageComplete() && page2A.isPageComplete();
} else if (type.equals( "accommodation")) {
return page1.isPageComplete() && page2B.isPageComplete();
} else {
return page1.isPageComplete() && page2C.isPageComplete();
}
}
第三页是可选页,比较简单,就不纍述了。
这里小结一下,为了开发一个逻辑清晰并具有良好用户体验的向导对话框:
- 仔细设计每个向导页面,包括页面的个数,每个页面是可选还是必选,每个页面的布局等;
- 在createControl()方法里布局页面控件
- 为需要输入检查的控件添加事件接收器以检查输入合法性,并调用isPageComplete()设置页面完成与否;
- 继承Wizard.canFinish()来定制Finish按钮活动与否,以便用户可以提前结束向导对话框。
应该讲向导对话框的工作量是非常巨大的,测试工作也很耗时,开发过程中遇到很多Bug也很常见。