本文呈現一個文本編輯器的三個演進版本,展示 SWT/JFace 應用開發的最佳實踐路徑:從手動控件綁定(v1)→ JFace Action/Manager 解耦(v2)→ 整合 StyledText 專業列印(v3)。涵蓋選單/工具列、檔案 IO、編碼、修改追蹤、字體/顏色/換行設置、列印分頁/頁碼/頁首尾等完整功能。
Menu、MenuItem、ToolBar、ToolItem。Listener 手動綁定事件。ImageRegistry 管理圖示資源。GridLayout 佈局,StyledText 編輯區。// 工具列建立
ToolBar toolBar = new ToolBar(shell, SWT.FLAT | SWT.RIGHT);
ToolItem tiNew = new ToolItem(toolBar, SWT.PUSH);
tiNew.setText("&New"); tiNew.setImage(getImage("new.gif"));
tiNew.addListener(SWT.Selection, e -> { if (handleChangesBeforeDiscard()) { file = null; text.setText(""); } });
// 選單建立
Menu menuBar = new Menu(shell, SWT.BAR);
MenuItem fileMenuItem = new MenuItem(menuBar, SWT.CASCADE);
fileMenuItem.setText("&File");
Menu fileMenu = new Menu(shell, SWT.DROP_DOWN);
MenuItem miNew = new MenuItem(fileMenu, SWT.PUSH);
miNew.setText("&New\tCtrl+N");
miNew.setAccelerator(SWT.CTRL | 'N');
miNew.addListener(SWT.Selection, e -> { ... });
fileMenuItem.setMenu(fileMenu);
// 修改追蹤
text.addModifyListener(e -> hasUnsavedChanges = true);
// 關閉前確認
boolean handleChangesBeforeDiscard() {
if (!hasUnsavedChanges) return true;
MessageBox box = new MessageBox(shell, SWT.ICON_WARNING | SWT.YES | SWT.NO | SWT.CANCEL);
box.setMessage("Save changes to " + (file == null ? "untitled?" : file.getName()));
int ret = box.open();
if (ret == SWT.YES) return saveTextToFile();
return ret == SWT.NO;
}
actionWrap.setChecked() 雙向同步選單/工具列勾選狀態。// Action 定義
Action actionNew = new Action("&New", ImageDescriptor.createFromFile(null, "C:/icons/new.gif")) {
public void run() { if (handleChangesBeforeDiscard()) { file = null; text.setText(""); } }
};
actionNew.setAccelerator(SWT.CTRL | 'N');
Action actionWrap = new Action("&Wrap", IAction.AS_CHECK_BOX) {
public void run() { text.setWordWrap(isChecked()); }
};
actionWrap.setAccelerator(SWT.CTRL | 'W');
// Manager 組裝
ToolBarManager tbm = new ToolBarManager(toolBar);
tbm.add(actionNew); tbm.add(actionOpen); tbm.add(actionSave);
tbm.add(new Separator());
tbm.add(actionCopy); tbm.add(actionCut); tbm.add(actionPaste);
tbm.add(new Separator());
tbm.add(actionWrap);
tbm.update(true);
MenuManager barMenu = new MenuManager();
MenuManager fileMenu = new MenuManager("&File");
MenuManager editMenu = new MenuManager("&Edit");
MenuManager formatMenu = new MenuManager("&Format");
barMenu.add(fileMenu); barMenu.add(editMenu); barMenu.add(formatMenu);
fileMenu.add(actionNew); fileMenu.add(actionOpen); fileMenu.add(actionSave);
fileMenu.add(new Separator()); fileMenu.add(actionExit);
editMenu.add(actionCopy); editMenu.add(actionCut); editMenu.add(actionPaste);
formatMenu.add(actionWrap);
barMenu.updateAll(true);
shell.setMenuBar(barMenu.createMenuBar(shell));
StyledText.print() 內建列印雙模式。// 普通文字列印
void printText(String text) {
PrintDialog dialog = new PrintDialog(shell);
PrinterData data = dialog.open();
if (data == null) return;
Printer printer = new Printer(data);
if (!printer.startJob("text")) return;
GC gc = new GC(printer);
PrintMargin margins = PrintMargin.getPrintMargin(printer, 1.0); // 1 吋邊距
Font font = new Font(printer, "Arial", 12, SWT.NORMAL);
gc.setFont(font);
int lineHeight = gc.getFontMetrics().getHeight();
printer.startPage();
// 繪製頁碼
String page = "- 1 -";
gc.drawString(page, (margins.right - margins.left - gc.textExtent(page).x) / 2 + margins.left, margins.bottom + gc.textExtent(page).y);
// 逐字符處理換行/分頁
StringBuffer buffer = new StringBuffer();
int x = margins.left, y = margins.top;
for (int i = 0; i < text.length(); ) {
char c = text.charAt(i++);
if (c == '\r' || c == '\n') {
if (c == '\r' && i < text.length() && text.charAt(i) == '\n') i++;
printLine(gc, printer, buffer.toString(), x, y, margins, lineHeight);
buffer.setLength(0);
newLine(gc, printer, margins, lineHeight, x, y, pageNumber);
} else if (c == ' ' || c == '\t') {
// 空白處理:看後續單詞決定是否換行
buffer.append(c);
if (i < text.length() && !Character.isWhitespace(text.charAt(i))) {
String word = readWord(text, i);
if (gc.textExtent(buffer + word).x > margins.right - margins.left) {
printLine(gc, printer, buffer.toString(), x, y, margins, lineHeight);
buffer.setLength(0);
}
}
} else {
buffer.append(c);
}
}
if (buffer.length() > 0) printLine(gc, printer, buffer.toString(), x, y, margins, lineHeight);
if (y + lineHeight <= margins.bottom) printer.endPage();
printer.endJob();
// 釋放資源...
}
// StyledText 內建列印(一鍵)
Action actionPrint2 = new Action("Print (StyledText)") {
public void run() {
StyledTextPrintOptions options = new StyledTextPrintOptions();
options.header = "SWT";
options.footer = "Page ";
options.jobName = "Text";
Runnable runnable = text.print(new Printer(), options);
runnable.run(); // 或提交執行緒池
}
};
public class PrintMargin {
public int left, right, top, bottom;
static PrintMargin getPrintMargin(Printer printer, double margin) {
return getPrintMargin(printer, margin, margin, margin, margin);
}
static PrintMargin getPrintMargin(Printer printer, double mL, double mR, double mT, double mB) {
Rectangle clientArea = printer.getClientArea();
Rectangle trim = printer.computeTrim(0, 0, 0, 0);
Point dpi = printer.getDPI();
int leftMargin = (int)(mL * dpi.x) - trim.x;
int rightMargin = clientArea.width + trim.width - (int)(mR * dpi.x) - trim.x;
int topMargin = (int)(mT * dpi.y) - trim.y;
int bottomMargin = clientArea.height + trim.height - (int)(mB * dpi.y) - trim.y;
return new PrintMargin(leftMargin, rightMargin, topMargin, bottomMargin);
}
}
| 特性 | v1 手動綁定 | v2 Action+Manager | v3 +專業列印 |
|---|---|---|---|
| 代碼結構 | 單類、耦合度高 | 命令解耦、Manager 組裝 | 功能模組化、列印獨立 |
| Action 復用 | 不可(選單/工具列各寫) | 同一 Action 多處使用 | 同 v2 |
| 狀態同步 | 手動同步勾選項 | AS_CHECK_BOX 自動同步 | 同 v2 |
| 擴展性 | 差 | 良(新增 Action 即可) | 優(列印模組可插拔) |
| 列印能力 | 無 | 無 | 專業分頁/頁碼/樣式保留 |
| 學習曲線 | 低 | 中(理解 JFace 命令模式) | 中高 |
| 選項 | 類型 | 說明 |
|---|---|---|
| header | String | 頁首,支援 <page>、<date>、<time> |
| footer | String | 頁尾,同支援變數 |
| jobName | String | 列印作業名稱 |
| lineStyle | int | 行樣式常數 |
// 讀取:指定編碼、緩衝讀取
BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
StringBuffer sb = new StringBuffer();
String line;
while ((line = reader.readLine()) != null) { sb.append(line).append("\r\n"); }
text.setText(sb.toString());
// 寫入:原子性寫入(寫臨時檔再 rename)
File temp = File.createTempFile("edit", ".tmp", file.getParentFile());
try (FileWriter fw = new FileWriter(temp, StandardCharsets.UTF_8)) {
fw.write(text.getText());
}
Files.move(temp.toPath(), file.toPath(), StandardCopyOption.REPLACE_EXISTING);
文本編輯器三版本演進展示了 SWT/JFace 應用開發的成長路徑:
Action/Manager 命令模式,實現 UI 與邏輯分離、狀態自動同步、聲明式組裝。StyledText.print() 內建列印與自定義分頁列印,解決文檔輸出的專業需求。核心最佳實踐:
dispose() 或 try-with-resources。DPI、trim、clientArea 三要素。此系列代碼可作為 SWT/JFace 桌面應用的標準起始模板,直接擴展為筆記本、代碼編輯器、日誌查看器等產品級工具。