JFace Wizard 框架 (org.eclipse.jface.wizard) 將複雜的多步驟任務封裝為標準化流程:
createControl)、資料驗證 (setPageComplete)、錯誤訊息顯示 (setErrorMessage)。包含三個頁面:FrontPage (房型/日期)、CustomerInfoPage (客戶資料驗證)、PaymentInfoPage (信用卡資訊)。資料模型 ReservationData 集中管理,支援對話框設定持久化 (DialogSettings) 與完成時進度監控 (IRunnableWithProgress)。
package swt_jface.demo12;
import org.eclipse.jface.dialogs.Dialog;
import org.eclipse.jface.window.ApplicationWindow;
import org.eclipse.jface.wizard.WizardDialog;
import org.eclipse.swt.SWT;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
public class HotelReservation extends ApplicationWindow {
@Override
protected Control createContents(Composite parent) {
Button button = new Button(parent, SWT.PUSH);
button.setText("Make a reservation");
button.addListener(SWT.Selection, e -> {
ReservationWizard wizard = new ReservationWizard();
WizardDialog dialog = new WizardDialog(getShell(), wizard);
dialog.setBlockOnOpen(true);
int returnCode = dialog.open();
if (returnCode == Dialog.OK) System.out.println(wizard.data);
else System.out.println("Cancelled");
});
return button;
}
public HotelReservation(Shell parentShell) { super(parentShell); }
public static void main(String[] args) {
HotelReservation reservation = new HotelReservation(null);
reservation.setBlockOnOpen(true);
reservation.open();
}
}
class ReservationData {
Date arrivalDate, departureDate;
int roomType; // 0: Standard, 1: Deluxe
String customerName, customerPhone, customerEmail, customerAddress;
int creditCardType; // 0: Amex, 1: Master, 2: Visa
String creditCardNumber, creditCardExpiration;
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
sb.append("* HOTEL ROOM RESERVATION DETAILS *\n");
sb.append("Arrival date:\t" + arrivalDate + "\n");
sb.append("Departure date:\t" + departureDate + "\n");
sb.append("Room type:\t" + (roomType == 0 ? "Standard ($198)" : "Deluxe ($298)") + "\n");
sb.append("Customer name:\t" + customerName + "\n");
sb.append("Customer email:\t" + customerEmail + "\n");
sb.append("Credit card no.:\t" + creditCardNumber + "\n");
return sb.toString();
}
}
package swt_jface.demo12;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.Date;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.dialogs.DialogSettings;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.operation.IRunnableWithProgress;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.wizard.Wizard;
public class ReservationWizard extends Wizard {
static final String SETTING_FILE = "C:/userInfo.xml";
static final String KEY_NAME = "customer-name";
static final String KEY_EMAIL = "customer-email";
static final String KEY_PHONE = "customer-phone";
static final String KEY_ADDRESS = "customer-address";
ReservationData data = new ReservationData();
public ReservationWizard() {
setWindowTitle("Hotel room reservation wizard");
setNeedsProgressMonitor(true); // 啟用完成時進度監控
setDefaultPageImageDescriptor(ImageDescriptor.createFromFile(null, "C:/icons/hotel.gif"));
// 載入持久化設定
DialogSettings settings = new DialogSettings("userInfo");
try { settings.load(SETTING_FILE); } catch (IOException e) { e.printStackTrace(); }
setDialogSettings(settings);
}
@Override
public void addPages() {
addPage(new FrontPage());
addPage(new CustomerInfoPage());
addPage(new PaymentInfoPage());
}
@Override
public boolean performFinish() {
// 1. 儲存設定
if (getDialogSettings() != null) {
getDialogSettings().put(KEY_NAME, data.customerName);
getDialogSettings().put(KEY_PHONE, data.customerPhone);
getDialogSettings().put(KEY_EMAIL, data.customerEmail);
getDialogSettings().put(KEY_ADDRESS, data.customerAddress);
try { getDialogSettings().save(SETTING_FILE); } catch (IOException e) { e.printStackTrace(); }
}
// 2. 執行耗時操作 (模擬儲存至資料庫/遠端)
try {
getContainer().run(true, true, new IRunnableWithProgress() {
@Override
public void run(IProgressMonitor monitor) throws InvocationTargetException, InterruptedException {
monitor.beginTask("Store data", 100);
monitor.worked(40);
System.out.println(data); // 實際業務邏輯
Thread.sleep(2000); // 模擬耗時
monitor.done();
}
});
} catch (Exception e) { e.printStackTrace(); }
return true;
}
@Override
public boolean performCancel() {
return MessageDialog.openConfirm(getShell(), "Confirmation", "Are you sure to cancel the task?");
}
}
使用 Combo 選擇年/月/日,驗證入住日期早於退房日期。監聽所有 Combo 選擇事件統一驗證。
package swt_jface.demo12;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
public class FrontPage extends WizardPage {
Combo comboRoomTypes, comboArrivalYear, comboArrivalMonth, comboArrivalDay;
Combo comboDepartureYear, comboDepartureMonth, comboDepartureDay;
public FrontPage() {
super("FrontPage");
setTitle("Your reservation information");
setDescription("Select the type of room and your arrival date & departure date");
}
@Override
public void createControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setLayout(new GridLayout(2, false));
// 到達日期
new Label(composite, SWT.NULL).setText("Arrival date: ");
Composite compArr = new Composite(composite, SWT.NULL);
compArr.setLayout(new RowLayout());
String[] months = {"Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"};
Calendar cal = new GregorianCalendar();
data.arrivalDate = cal.getTime(); // 初始化模型
comboArrivalMonth = new Combo(compArr, SWT.BORDER | SWT.READ_ONLY);
for(String m: months) comboArrivalMonth.add(m);
comboArrivalMonth.select(cal.get(Calendar.MONTH));
comboArrivalDay = new Combo(compArr, SWT.BORDER | SWT.READ_ONLY);
for(int i=0;i<31;i++) comboArrivalDay.add(""+(i+1));
comboArrivalDay.select(cal.get(Calendar.DAY_OF_MONTH)-1);
comboArrivalYear = new Combo(compArr, SWT.BORDER | SWT.READ_ONLY);
for(int i=2004;i<2010;i++) comboArrivalYear.add(""+i);
comboArrivalYear.select(cal.get(Calendar.YEAR)-2004);
cal.add(Calendar.DATE, 1);
data.departureDate = cal.getTime();
// 離開日期
new Label(composite, SWT.NULL).setText("Departure date: ");
Composite compDep = new Composite(composite, SWT.NULL);
compDep.setLayout(new RowLayout());
comboDepartureMonth = new Combo(compDep, SWT.BORDER | SWT.READ_ONLY);
for(String m: months) comboDepartureMonth.add(m);
comboDepartureMonth.select(cal.get(Calendar.MONTH));
comboDepartureDay = new Combo(compDep, SWT.BORDER | SWT.READ_ONLY);
for(int i=0;i<31;i++) comboDepartureDay.add(""+(i+1));
comboDepartureDay.select(cal.get(Calendar.DAY_OF_MONTH)-1);
comboDepartureYear = new Combo(compDep, SWT.BORDER | SWT.READ_ONLY);
for(int i=2004;i<2010;i++) comboDepartureYear.add(""+i);
comboDepartureYear.select(cal.get(Calendar.YEAR)-2004);
// 分隔線
Label line = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
line.setLayoutData(new GridData(GridData.FILL_HORIZONTAL | GridData.HORIZONTAL_ALIGN_FILL));
((GridData)line.getLayoutData()).horizontalSpan = 2;
// 房型
new Label(composite, SWT.NULL).setText("Room type: ");
comboRoomTypes = new Combo(composite, SWT.BORDER | SWT.READ_ONLY);
comboRoomTypes.add("Standard room (rate: $198)");
comboRoomTypes.add("Deluxe room (rate: $298)");
comboRoomTypes.select(0);
// 統一日期驗證監聽器
Listener dateListener = e -> {
int aDay = comboArrivalDay.getSelectionIndex() + 1;
int aMon = comboArrivalMonth.getSelectionIndex();
int aYear = comboArrivalYear.getSelectionIndex() + 2004;
int dDay = comboDepartureDay.getSelectionIndex() + 1;
int dMon = comboDepartureMonth.getSelectionIndex();
int dYear = comboDepartureYear.getSelectionIndex() + 2004;
setDates(aDay, aMon, aYear, dDay, dMon, dYear);
};
comboArrivalDay.addListener(SWT.Selection, dateListener);
comboArrivalMonth.addListener(SWT.Selection, dateListener);
comboArrivalYear.addListener(SWT.Selection, dateListener);
comboDepartureDay.addListener(SWT.Selection, dateListener);
comboDepartureMonth.addListener(SWT.Selection, dateListener);
comboDepartureYear.addListener(SWT.Selection, dateListener);
comboRoomTypes.addListener(SWT.Selection, e -> data.roomType = comboRoomTypes.getSelectionIndex());
setControl(composite);
}
private void setDates(int aDay, int aMon, int aYear, int dDay, int dMon, int dYear) {
Calendar cal = new GregorianCalendar();
cal.set(aYear, aMon, aDay);
Date arrival = cal.getTime();
cal.set(dYear, dMon, dDay);
Date departure = cal.getTime();
if (!arrival.before(departure)) {
setErrorMessage("The arrival date is not before the departure date");
setPageComplete(false);
} else {
setErrorMessage(null);
setPageComplete(true);
data.arrivalDate = arrival;
data.departureDate = departure;
}
}
}
使用 ModifyListener 即時驗證 Email 格式 (@ 必須存在),所有欄位填寫完畢才啟用「下一步」(setPageComplete(true))。載入/儲存 DialogSettings。
package swt_jface.demo12;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
public class CustomerInfoPage extends WizardPage {
Text textName, textPhone, textEmail, textAddress;
public CustomerInfoPage() {
super("CustomerInfo");
setTitle("Customer Information");
setPageComplete(false);
}
@Override
public void createControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setLayout(new GridLayout(2, false));
new Label(composite, SWT.NULL).setText("Full name: ");
textName = new Text(composite, SWT.SINGLE | SWT.BORDER);
textName.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
new Label(composite, SWT.NULL).setText("Phone Number: ");
textPhone = new Text(composite, SWT.SINGLE | SWT.BORDER);
textPhone.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
new Label(composite, SWT.NULL).setText("Email address: ");
textEmail = new Text(composite, SWT.SINGLE | SWT.BORDER);
textEmail.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
new Label(composite, SWT.NULL).setText("Address: ");
textAddress = new Text(composite, SWT.MULTI | SWT.BORDER);
textAddress.setText("\r\n\r\n\r\n"); // 預留高度
textAddress.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
Listener listener = e -> {
if (!(e.widget instanceof Text)) return;
String s = ((Text)e.widget).getText();
if (e.widget == textName) data.customerName = s;
else if (e.widget == textPhone) data.customerPhone = s;
else if (e.widget == textEmail) {
if (s.indexOf('@') < 0) { setErrorMessage("Invalid email: " + s); data.customerEmail = null; }
else { setErrorMessage(null); data.customerEmail = s; }
} else if (e.widget == textAddress) data.customerAddress = s;
// 檢查是否全部填寫
boolean complete = data.customerName != null && data.customerPhone != null
&& data.customerEmail != null && data.customerAddress != null;
setPageComplete(complete);
};
textName.addListener(SWT.Modify, listener);
textPhone.addListener(SWT.Modify, listener);
textEmail.addListener(SWT.Modify, listener);
textAddress.addListener(SWT.Modify, listener);
// 載入歷史設定
if (getDialogSettings() != null && validDialogSettings()) {
textName.setText(getDialogSettings().get(ReservationWizard.KEY_NAME));
textPhone.setText(getDialogSettings().get(ReservationWizard.KEY_PHONE));
textEmail.setText(getDialogSettings().get(ReservationWizard.KEY_EMAIL));
textAddress.setText(getDialogSettings().get(ReservationWizard.KEY_ADDRESS));
}
setControl(composite);
}
private boolean validDialogSettings() {
return getDialogSettings().get(ReservationWizard.KEY_NAME) != null
&& getDialogSettings().get(ReservationWizard.KEY_ADDRESS) != null
&& getDialogSettings().get(ReservationWizard.KEY_EMAIL) != null
&& getDialogSettings().get(ReservationWizard.KEY_PHONE) != null;
}
}
驗證有效期格式 (MM/YY),欄位皆填寫正確才啟用完成。
package swt_jface.demo12;
import org.eclipse.jface.wizard.WizardPage;
import org.eclipse.swt.SWT;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
public class PaymentInfoPage extends WizardPage {
Combo comboCreditCardTypes;
Text textCreditCardNumber, textCreditCardExpiration;
public PaymentInfoPage() {
super("PaymentInfo");
setTitle("Payment information");
setDescription("Please enter your credit card details");
setPageComplete(false);
}
@Override
public void createControl(Composite parent) {
Composite composite = new Composite(parent, SWT.NULL);
composite.setLayout(new GridLayout(2, false));
new Label(composite, SWT.NULL).setText("Credit card type: ");
comboCreditCardTypes = new Combo(composite, SWT.READ_ONLY | SWT.BORDER);
comboCreditCardTypes.add("American Express");
comboCreditCardTypes.add("Master Card");
comboCreditCardTypes.add("Visa");
comboCreditCardTypes.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
new Label(composite, SWT.NULL).setText("Credit card number: ");
textCreditCardNumber = new Text(composite, SWT.SINGLE | SWT.BORDER);
textCreditCardNumber.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
new Label(composite, SWT.NULL).setText("Expiration (MM/YY)");
textCreditCardExpiration = new Text(composite, SWT.SINGLE | SWT.BORDER);
textCreditCardExpiration.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
comboCreditCardTypes.addListener(SWT.Selection, e -> {
data.creditCardType = comboCreditCardTypes.getSelectionIndex();
checkComplete();
});
textCreditCardNumber.addListener(SWT.Modify, e -> {
data.creditCardNumber = textCreditCardNumber.getText();
checkComplete();
});
textCreditCardExpiration.addListener(SWT.Modify, e -> {
String t = textCreditCardExpiration.getText().trim();
if (t.length() == 5 && t.charAt(2) == '/') {
data.creditCardExpiration = t; setErrorMessage(null);
} else {
data.creditCardExpiration = null; setErrorMessage("Invalid expiration: " + t);
}
checkComplete();
});
setControl(composite);
}
private void checkComplete() {
setPageComplete(data.creditCardNumber != null && data.creditCardExpiration != null);
}
}
ReservationData 作為單一真實來源,所有 Page 直接操作 wizard.data,避免頁面間傳值耦合。ModifyListener/SelectionListener 即時更新模型並呼叫 setPageComplete(boolean),Wizard 會自動啟用/禁用「下一步」/「完成」按鈕。setErrorMessage(String) 會在頁面頂部顯示紅字提示,setMessage(String) 顯示一般資訊。DialogSettings + XML 檔案實現使用者偏好記憶 (姓名、Email 等),Wizard 建構子載入,performFinish 儲存。setNeedsProgressMonitor(true) + performFinish 中使用 getContainer().run(IRunnableWithProgress),自動顯示進度對話框支援取消/暫停。