文字編輯器是桌面應用的經典範例。本文展示三個版本的演進:
Action、MenuManager、ToolBarManager 解耦邏輯與 UIPrinter、StyledTextPrintOptions)與自訂分頁列印邏輯StyledText:多行、自動換行、捲動、等寬字型ToolBar + ToolItem:新增、開啟、儲存、複製、剪下、貼上、自動換行切換MenuBar + MenuItem:對應工具列功能,含快捷鍵(Accelerator)MessageBox:未儲存變更確認對話框FileDialog:開啟/儲存檔案ImageRegistry:圖示快取管理boolean handleChangesBeforeDiscard() {
if (!hasUnsavedChanges) return true;
MessageBox box = new MessageBox(shell, SWT.ICON_WARNING | SWT.YES | SWT.NO | SWT.CANCEL);
box.setMessage("Do you want to save the changes to " + (file == null ? "a file?" : file.getName()));
box.setText(APP_NAME);
int ret = box.open();
if (ret == SWT.YES) return saveTextToFile();
else if (ret == SWT.NO) return true;
else return false; // CANCEL
}
所有「新增、開啟、結束」操作前均呼叫此方法,確保資料不遺失。
boolean loadTextFromFile() {
FileDialog dialog = new FileDialog(shell, SWT.OPEN);
if (lastOpenDirectory != null) dialog.setFilterPath(lastOpenDirectory);
String selected = dialog.open();
if (selected == null) return false;
file = new File(selected);
lastOpenDirectory = file.getParent();
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) { sb.append(line).append("\r\n"); }
text.setText(sb.toString());
hasUnsavedChanges = false;
return true;
} catch (IOException e) { log(e); return false; }
}
Action actionNew = new Action("&New", ImageDescriptor.createFromFile(null, "icons/new.gif")) {
public void run() { if (handleChangesBeforeDiscard()) { file = null; text.setText(""); } }
};
actionNew.setAccelerator(SWT.CTRL | 'N');
// 核取方塊型 Action(自動換行)
Action actionWrap = new Action("&Wrap", IAction.AS_CHECK_BOX) {
public void run() { text.setWordWrap(isChecked()); }
};
actionWrap.setAccelerator(SWT.CTRL | 'W');
IAction.AS_CHECK_BOX:工具列顯示為核取按鈕,選單顯示為勾選項目setAccelerator:註冊全域快捷鍵ImageDescriptor:延遲載入圖示,支援插件化部署ToolBar toolBar = new ToolBar(shell, SWT.FLAT | SWT.RIGHT);
ToolBarManager toolBarManager = new ToolBarManager(toolBar);
toolBarManager.add(actionNew); toolBarManager.add(actionOpen); toolBarManager.add(actionSave);
toolBarManager.add(new Separator());
toolBarManager.add(actionCopy); toolBarManager.add(actionCut); toolBarManager.add(actionPaste);
toolBarManager.add(new Separator()); toolBarManager.add(actionWrap);
toolBarManager.update(true);
MenuManager barMenuManager = new MenuManager();
MenuManager fileMenu = new MenuManager("&File");
MenuManager editMenu = new MenuManager("&Edit");
MenuManager formatMenu = new MenuManager("&Format");
barMenuManager.add(fileMenu); barMenuManager.add(editMenu); barMenuManager.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);
barMenuManager.updateAll(true);
shell.setMenuBar(barMenuManager.createMenuBar(shell));
優勢: 同一個 Action 同時貢獻至工具列與選單,狀態(啟用/勾選)自動同步,程式碼大幅減少且易維護。
Action actionPrint = new Action("&Print@Ctrl+P") {
public void run() { printText(text.getText()); }
};
Action actionPrint2 = new Action("Print (StyledText)") {
public void run() {
StyledTextPrintOptions options = new StyledTextPrintOptions();
options.header = "SWT"; options.footer = "Page <page>"; options.jobName = "Text";
Runnable runnable = text.print(new Printer(), options);
runnable.run(); // 同步列印
}
};
actionPrint:自訂分頁邏輯(支援換行、分頁、頁碼)actionPrint2:使用 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 吋邊距
x = margins.left; y = margins.top;
Font font = new Font(printer, "Arial", 12, SWT.NORMAL);
gc.setFont(font);
lineHeight = gc.getFontMetrics().getHeight();
printer.startPage();
drawPageNumber(gc, margins, pageNumber);
StringBuffer buffer = new StringBuffer();
for (int i = 0; i < text.length(); ) {
char c = text.charAt(i);
switch (c) {
case '\r': // CRLF 處理
if (i+1 < text.length() && text.charAt(i+1) == '\n') {
printNewLine(printer, gc, buffer.toString()); buffer.setLength(0); i += 2;
} break;
case '\t': case ' ':
// 寬度檢查:超過右邊距則換行
if (gc.textExtent(buffer.toString() + ' ').x > margins.right - margins.left) {
printNewLine(printer, gc, buffer.toString()); buffer.setLength(0);
}
buffer.append(c);
// 預讀下一個單字,決定是否在此換行
if (i+1 < text.length() && !Character.isWhitespace(text.charAt(i+1))) {
String word = readWord(text, i+1);
if (gc.textExtent(buffer.toString() + word).x > margins.right - margins.left) {
printNewLine(printer, gc, buffer.toString()); buffer.setLength(0);
}
}
i++; break;
default: buffer.append(c); i++;
}
}
if (buffer.length() > 0) printNewLine(printer, gc, buffer.toString());
if (y + lineHeight <= margins.bottom) printer.endPage();
printer.endJob(); gc.dispose(); font.dispose(); printer.dispose();
}
void printNewLine(Printer printer, GC gc, String line) {
if (y + lineHeight > margins.bottom) { // 頁底換頁
printer.endPage();
x = margins.left; y = margins.top; pageNumber++; lineNumber = 1;
printer.startPage(); drawPageNumber(gc, margins, pageNumber);
}
gc.drawString(line, x, y); y += lineHeight;
}
PrintMargin 類別根據印表機 DPI 與指定邊距(吋)計算像素邊界,支援非對稱邊距。
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 client = printer.getClientArea();
Rectangle trim = printer.computeTrim(0,0,0,0);
Point dpi = printer.getDPI();
int left = (int)(ml * dpi.x) - trim.x;
int right = client.width + trim.width - (int)(mr * dpi.x) - trim.x;
int top = (int)(mt * dpi.y) - trim.y;
int bottom = client.height + trim.height - (int)(mb * dpi.y) - trim.y;
return new PrintMargin(left, right, top, bottom);
}
}
| 特性 | BasicEditor | BasicEditor2 | BasicEditor3 |
|---|---|---|---|
| UI 建構 | 手動 SWT API | JFace Action + Manager | 繼承 v2 + 列印 |
| 命令封裝 | 匿名 Listener | Action 子類別 | 同上 |
| 狀態同步 | 手動 (toolBar/item 對應) | 自動 (單一 Action 多重貢獻) | 同上 |
| 圖示管理 | ImageRegistry | ImageDescriptor + Registry | 同上 |
| 列印支援 | 無 | 無 | 自訂 + StyledText 內建 |
| 程式碼行數 | ~350 | ~300 | ~450 |
StyledText.print();複雜排版(表格、圖片、多欄)需自訂 GC 繪製。Font、Image、GC、Printer 皆需在 finally 或 try-with-resources 中釋放。StandardCharsets.UTF_8,避免跨平台亂碼。從 BasicEditor 到 BasicEditor3,展示了 SWT/JFace 應用從「手工組裝」走向「架構化開發」的典型演進路徑。掌握 Action 架構與列印子系統,能讓開發者快速構建具備專業級文件處理能力的桌面應用。