BugTracker 是展示 SWT/JFace 表格元件進階用法的經典範例。本文完整呈現兩個版本的演進:版本一 使用純 SWT Table + TableEditor 實現內嵌編輯、排序、檔案持久化;版本二 引入 JFace TableViewer 架構,導入領域模型、ContentProvider/LabelProvider/CellEditor/ICellModifier/ViewerFilter/ViewerSorter,實現關注點分離與高擴充性。兩版本功能對等,架構差異顯著,適合作為技術選型參考。
bugs.dat)
GridLayout layout = new GridLayout(); shell.setLayout(layout);
ToolBar toolBar = new ToolBar(shell, SWT.RIGHT | SWT.FLAT);
ToolBarManager mgr = new ToolBarManager(toolBar);
mgr.add(new Action("New bug") { public void run() { TableItem item=new TableItem(table,SWT.NULL); item.setImage(bugIcon); table.select(table.getItemCount()-1); }});
mgr.add(new Action("Delete selected") { public void run() { /* 確認對話框後 table.remove(index) */ }});
mgr.add(new Separator()); mgr.add(new Action("Save") { public void run() { saveBugs(); }});
mgr.update(true);
table = new Table(shell, SWT.BORDER | SWT.FULL_SELECTION);
table.setLayoutData(new GridData(GridData.FILL_BOTH));
table.setLinesVisible(true); table.setHeaderVisible(true);
// 四欄 TableColumn 設定...
table.pack();
final TableEditor editor = new TableEditor(table);
table.addListener(SWT.MouseDown, e -> {
Point pt = new Point(e.x, e.y);
TableItem item = table.getItem(pt); if (item==null) return;
int col = -1; for(int i=0;i{ item.setText(3,btn.getSelection()?"YES":"NO"); btn.dispose(); });
btn.addListener(SWT.Traverse, ev->{ if(ev.detail==SWT.TRAVERSE_RETURN||ev.detail==SWT.TRAVERSE_ESCAPE){btn.dispose();ev.doit=false;} });
btn.setFocus();
} else if (col>=0 && col<3) { // Text columns
Text txt = new Text(table, SWT.NONE);
txt.setText(item.getText(col));
editor.horizontalAlignment=SWT.LEFT; editor.grabHorizontal=true;
editor.setEditor(txt, item, col);
txt.addListener(SWT.FocusOut, ev->{ item.setText(col,txt.getText()); txt.dispose(); });
txt.addListener(SWT.Traverse, ev->{ if(ev.detail==SWT.TRAVERSE_RETURN||ev.detail==SWT.TRAVERSE_ESCAPE){txt.dispose();ev.doit=false;} });
txt.setFocus();
}
});
Listener sortListener = e -> { if(e.widget instanceof TableColumn) sortTable(table, table.indexOf((TableColumn)e.widget)); };
for(int i=0;i i.getText(col)));
for(TableItem old: items) {
TableItem nu = new TableItem(table, SWT.NULL);
for(int j=0;j
private void saveBugs() {
try(DataOutputStream out=new DataOutputStream(new FileOutputStream("bugs.dat"))) {
for(int i=0;i
public static class Bug {
public String id, summary, assignedTo; public boolean isSolved;
public Bug(String id,String sum,String ass,boolean sol){this.id=id;this.summary=sum;this.assignedTo=ass;this.isSolved=sol;}
public static Vector loadBugs(File file){ /* 同版本一讀取邏輯,回傳 Vector */ }
}
tableViewer = new TableViewer(table);
tableViewer.setContentProvider(new IStructuredContentProvider() {
public Object[] getElements(Object in) { return ((Vector)in).toArray(); }
public void dispose(){} public void inputChanged(Viewer v,Object o,Object n){}
});
tableViewer.setLabelProvider(new ITableLabelProvider() {
public Image getColumnImage(Object el,int col){ return col==0?bugIcon:null; }
public String getColumnText(Object el,int col){
Bug b=(Bug)el; return switch(col){case 0->b.id;case 1->b.summary;case 2->b.assignedTo;case 3->b.isSolved?"YES":"NO";default->null;};
}
// 其餘介面方法空實作
});
tableViewer.setColumnProperties(new String[]{"ID","Summary","Assigned to","Solved"});
CellEditor[] editors = { new TextCellEditor(table), new TextCellEditor(table), new TextCellEditor(table), new CheckboxCellEditor(table) };
tableViewer.setCellEditors(editors);
tableViewer.setCellModifier(new ICellModifier() {
public boolean canModify(Object el,String prop){ return true; }
public Object getValue(Object el,String prop){
Bug b=(Bug)el; return switch(prop){case "ID"->b.id;case "Summary"->b.summary;case "Assigned to"->b.assignedTo;case "Solved"->b.isSolved;default->null;};
}
public void modify(Object el,String prop,Object val){
Bug b = (el instanceof TableItem) ? (Bug)((TableItem)el).getData() : (Bug)el;
switch(prop){case "ID"->b.id=(String)val;case "Summary"->b.summary=(String)val;case "Assigned to"->b.assignedTo=(String)val;case "Solved"->b.isSolved=(Boolean)val;}
tableViewer.update(b,null);
}
});
ViewerFilter unsolvedFilter = new ViewerFilter() {
public boolean select(Viewer v,Object p,Object el){ return !((Bug)el).isSolved; }
};
Action showUnsolved = new Action("Show unsolved only", IAction.AS_CHECK_BOX) {
public void run(){ if(isChecked()) tableViewer.addFilter(unsolvedFilter); else tableViewer.removeFilter(unsolvedFilter); }
};
class BugSorter extends ViewerSorter {
private int idx; public BugSorter(String prop){ for(int i=0;ib1.id.compareTo(b2.id);case 1->b1.summary.compareTo(b2.summary);case 2->b1.assignedTo.compareTo(b2.assignedTo);case 3->Boolean.compare(b1.isSolved,b2.isSolved);default->0;};
}
}
tcID.addListener(SWT.Selection, e->tableViewer.setSorter(new BugSorter(colNames[0])));
// ... 其他欄位同理
bugs = Bug.loadBugs(new File("bugs.dat"));
tableViewer.setInput(bugs);
| 層面 | 版本一 (SWT) | 版本二 (JFace) | 影響 |
|---|---|---|---|
| 資料儲存 | TableItem 文字/圖示 | 獨立 Bug 物件 (Vector) | 資料與 UI 解耦,易單元測試、序列化、匯出 |
| 編輯模型 | TableEditor + 手動 Listener | CellEditor + ICellModifier | 編輯邏輯集中、可複用、支援驗證 |
| 排序實作 | Comparator + 重建 TableItem | ViewerSorter + setSorter | 聲明式、自動處理視圖更新 |
| 過濾實作 | 無 (需手動隱藏) | ViewerFilter + add/removeFilter | 動態切換、多重過濾疊加 |
| 資料同步 | 開發者手動維護 | update(element) 自動重繪 | 減少 Bug、支援多視圖同步 |
| 程式碼結構 | 單類別 400+ 行 | 內部類別分層、職責清晰 | 可維護性、可讀性顯著提升 |
ILazyContentProvider 或分頁載入。tableViewer.update(element, null) 僅重繪該列;refresh() 會重新查詢 ContentProvider,成本較高。ArrayList 或資料庫游標。Bug)與載入/儲存服務。Table 為 TableViewer,設定 ContentProvider/LabelProvider。CellEditor/ICellModifier。ViewerSorter/ViewerFilter 替換手動排序/過濾。TableItem 的程式碼,改操作領域物件並呼叫 update()。BugTracker 雙版本完美展示了從「UI 中心」到「資料中心」的架構演進。JFace Viewer 框架雖增加初期學習成本,但其標準化的 Provider/Editor/Filter/Sorter 介面,能讓複雜表格應用的開發效率與維護性大幅提升。建議團隊在專案早期評估複雜度,果斷選擇適合的架構起步。