書籤管理器是展示 SWT/JFace 進階功能的經典範例:樹狀結構(Tree)、拖放(Drag & Drop)、自訂傳輸類型(ByteArrayTransfer)、檔案解析與序列化(Netscape 書籤 HTML 格式)。本文完整解析 BookmarkOrganizer 實作細節。
class Bookmark {
public String name, href, addDate, lastVisited, lastModified;
}
資料夾節點不攜帶 Bookmark 物件,改以 TreeItem.setData(KEY_ADD_DATE, addDate) 標記;書籤節點以 setData(bookmark) 儲存完整資訊。
shell.setLayout(new GridLayout(1, true));
ToolBar toolBar = new ToolBar(shell, SWT.FLAT);
// ... 加入 Load、Save 工具項
tree = new Tree(shell, SWT.BORDER);
tree.setLayoutData(new GridData(GridData.FILL_BOTH));
rootItem = new TreeItem(tree, SWT.NULL);
rootItem.setText("BOOKMARKS");
rootItem.setImage(iconRoot);
label = new Label(shell, SWT.BORDER);
label.setLayoutData(new GridData(GridData.FILL_HORIZONTAL));
public class BookmarkTransfer extends ByteArrayTransfer {
private static final String BOOKMARK_TRANSFER_NAME = "BOOKMARK";
private static final int BOOKMARK_TRANSFER_ID = registerType(BOOKMARK_TRANSFER_NAME);
private static final BookmarkTransfer instance = new BookmarkTransfer();
public static BookmarkTransfer getInstance() { return instance; }
protected int[] getTypeIds() { return new int[] { BOOKMARK_TRANSFER_ID }; }
protected String[] getTypeNames() { return new String[] { BOOKMARK_TRANSFER_NAME }; }
protected void javaToNative(Object object, TransferData transferData) {
if (!(object instanceof Bookmark)) return;
Bookmark b = (Bookmark) object;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(bos)) {
out.writeUTF(b.name);
out.writeUTF(b.href);
out.writeUTF(b.addDate);
out.writeUTF(b.lastVisited);
out.writeUTF(b.lastModified);
out.close();
super.javaToNative(bos.toByteArray(), transferData);
} catch (IOException e) { e.printStackTrace(); }
}
protected Object nativeToJava(TransferData transferData) {
if (!isSupportedType(transferData)) return null;
byte[] raw = (byte[]) super.nativeToJava(transferData);
if (raw == null) return null;
try (ByteArrayInputStream bis = new ByteArrayInputStream(raw);
DataInputStream in = new DataInputStream(bis)) {
Bookmark b = new Bookmark();
b.name = in.readUTF();
b.href = in.readUTF();
b.addDate = in.readUTF();
b.lastVisited = in.readUTF();
b.lastModified = in.readUTF();
return b;
} catch (IOException e) { e.printStackTrace(); return null; }
}
}
繼承 ByteArrayTransfer 以 UTF-8 序列化五個欄位,確保跨平台相容。
DragSource dragSource = new DragSource(tree, DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK);
dragSource.setTransfer(new Transfer[] { BookmarkTransfer.getInstance() });
dragSource.addDragListener(new DragSourceAdapter() {
TreeItem dragSourceItem;
public void dragStart(DragSourceEvent e) {
TreeItem[] sel = tree.getSelection();
if (sel.length > 0 && sel[0].getData() != null) { // 僅書籤節點可拖曳
e.doit = true; dragSourceItem = sel[0];
} else e.doit = false;
}
public void dragSetData(DragSourceEvent e) {
if (BookmarkTransfer.getInstance().isSupportedType(e.dataType))
e.data = dragSourceItem.getData();
}
public void dragFinished(DragSourceEvent e) {
if (e.detail == DND.DROP_MOVE) dragSourceItem.dispose(); // 移動模式刪除來源
dragSourceItem = null;
}
});
DropTarget dropTarget = new DropTarget(tree, DND.DROP_MOVE | DND.DROP_COPY | DND.DROP_LINK);
dropTarget.setTransfer(new Transfer[] { BookmarkTransfer.getInstance() });
dropTarget.addDropListener(new DropTargetAdapter() {
public void dragOver(DropTargetEvent e) {
e.feedback = DND.FEEDBACK_EXPAND | DND.FEEDBACK_SCROLL | DND.FEEDBACK_SELECT;
}
public void dropAccept(DropTargetEvent e) {
// 僅允許放在資料夾節點(無 Bookmark 資料)或根節點
if (e.item == null || ((TreeItem)e.item).getData() != null)
e.detail = DND.DROP_NONE;
}
public void drop(DropTargetEvent e) {
if (e.data == null) { e.detail = DND.DROP_NONE; return; }
TreeItem target = (TreeItem) e.item;
Bookmark b = (Bookmark) e.data;
TreeItem newItem = new TreeItem(target, SWT.NULL);
newItem.setText(b.name);
newItem.setImage(iconURL);
newItem.setData(b);
}
});
關鍵邏輯: dropAccept 限制只能放入資料夾(getData() == null 且非書籤節點),避免書籤嵌套書籤。
<DT><H3 FOLDED ADD_DATE="1234567890">資料夾名稱</H3>
<DL><p>
<DT><A HREF="https://example.com" ADD_DATE="..." LAST_VISIT="..." LAST_MODIFIED="...">書籤名稱</A>
</DL><p>
private void loadBookmark(File file, TreeItem rootItem) {
TreeItem parent = rootItem;
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
if (line.startsWith(folderLinePrefix)) { // <DT><H3 FOLDED
Matcher m = folderPattern.matcher(line); // "\"(\\d+)\">(.*)<"
if (m.find()) {
String addDate = m.group(1), name = m.group(2);
TreeItem item = new TreeItem(parent, SWT.NULL);
item.setText(name);
item.setData(KEY_ADD_DATE, addDate);
item.setImage(iconFolder);
parent = item; // 進入子層
}
} else if (line.startsWith(urlLinePrefix)) { // <DT><A HREF
Matcher m = urlPattern.matcher(line); // "\"(.*)\".*\"(.*)\".*\"(.*)\".*\"(.*)\">(.*)<"
if (m.find()) {
Bookmark b = new Bookmark();
b.href = m.group(1); b.addDate = m.group(2);
b.lastVisited = m.group(3); b.lastModified = m.group(4);
b.name = m.group(5);
TreeItem item = new TreeItem(parent, SWT.NULL);
item.setText(b.name); item.setData(b); item.setImage(iconURL);
}
} else if (line.equals("</DL><p>")) { // 資料夾結束
parent = parent.getParentItem(); // 返回上層
}
}
} catch (IOException e) { e.printStackTrace(); }
}
使用 Pattern 正則解析,透過 parent 變數追蹤當前資料夾層級,遇到 </DL><p> 回溯父節點。
private void saveBookmark(BufferedWriter writer, TreeItem item) throws IOException {
if (item.getData() == null && item.getData(KEY_ADD_DATE) == null) { // 根節點
writer.write("<!DOCTYPE NETSCAPE-Bookmark-file-1>\n<DL><p>\n");
for (TreeItem child : item.getItems()) saveBookmark(writer, child);
writer.write("</DL><p>\n");
} else if (item.getData() == null) { // 資料夾
writer.write("\t<DT><H3 FOLDED ADD_DATE=\"" + item.getData(KEY_ADD_DATE) + "\">" + item.getText() + "</H3>\n");
writer.write("\t<DL><p>\n");
for (TreeItem child : item.getItems()) saveBookmark(writer, child);
writer.write("\t</DL><p>\n");
} else { // 書籤
Bookmark b = (Bookmark) item.getData();
writer.write("\t\t<DT><A HREF=\"" + b.href + "\" ADD_DATE=\"" + b.addDate
+ "\" LAST_VISIT=\"" + b.lastVisited + "\" LAST_MODIFIED=\"" + b.lastModified
+ "\">" + b.name + "</A>\n");
}
}
遞迴遍歷樹結構,依節點類型輸出對應 HTML 片段,保持原始格式相容性。
tree.addSelectionListener(new SelectionAdapter() {
public void widgetSelected(SelectionEvent e) {
TreeItem item = (TreeItem) e.item;
Bookmark b = (Bookmark) item.getData();
if (b != null) setStatus(b.href);
else if (item.getData(KEY_ADD_DATE) != null) setStatus("Folder: " + item.getText());
}
});
Image iconRoot = new Image(display, "C:/icons/icon.gif");
Image iconFolder = new Image(display, "C:/icons/folder.gif");
Image iconURL = new Image(display, "C:/icons/file.gif");
// 關閉時
iconRoot.dispose(); iconFolder.dispose(); iconURL.dispose();
本範例整合了 SWT/JFace 多項核心技術:
Tree 階層資料呈現與圖示區分Transfer 實現型別安全的拖放開發者可在此基礎上擴充:右鍵選單編輯/刪除、搜尋過濾、多書籤檔案管理、同步雲端等功能。