構建一個桌面級的文件瀏覽器,是掌握 Swing `JTree`、遞歸算法、檔案系統抽象 (`java.io.File` / `java.nio.file.Path`) 以及系統原生外觀整合的經典實戰。本文將重構原始代碼,修復其 **硬編碼包路徑、阻塞 EDT 讀取目錄、缺乏錯誤處理、圖標缺失、資源洩漏** 等問題,打造一個可直接用於生產工具的 `FileTreeBrowser` 元件。
| 層級 | 類別 | 職責 | 關鍵技術點 |
|---|---|---|---|
| 模型 | `FileSystemTreeModel` | 封裝檔案系統節點結構,支援**懶加載** (展開時才掃描子目錄) | 繼承 `DefaultTreeModel`,節點為 `FileTreeNode`,實現 `TreeModel` 接口 |
| 節點 | `FileTreeNode` | 包裝 `Path`,緩存子節點、檔案類型、圖標 | 實現 `TreeNode`,`getAllowsChildren()` 區分檔案/目錄 |
| 視圖 | `FileTreeCellRenderer` | 根據檔案類型顯示系統原生圖標 (資料夾、文字、圖片、執行檔...) | 繼承 `DefaultTreeCellRenderer`,使用 `FileSystemView.getSystemIcon()` |
| 控制器 | `FileTreeBrowser` | 組裝 JTree、滾動面板、工具列、右鍵選單、雙擊開啟邏輯 | SwingUtilities.invokeLater, SwingWorker 異步載入 |
/** 檔案樹節點:包裝 Path,支援懶加載子節點 */
public class FileTreeNode implements TreeNode {
private final Path path;
private final FileSystemView fsv = FileSystemView.getFileSystemView();
private List childrenCache; // null = 未載入, empty = 無子目錄/葉子
private boolean isDirectory;
public FileTreeNode(Path path) {
this.path = path;
this.isDirectory = Files.isDirectory(path);
}
public Path getPath() { return path; }
public String getName() { return fsv.getSystemDisplayName(path.toFile()); } // 系統顯示名 (含本地化)
public Icon getIcon() { return fsv.getSystemIcon(path.toFile()); } // 系統原生圖標!
@Override public boolean getAllowsChildren() { return isDirectory; }
@Override public boolean isLeaf() { return !isDirectory || getChildCount() == 0; }
@Override public Enumeration children() {
if (childrenCache == null) loadChildren(); // 懶加載觸發點
return Collections.enumeration(childrenCache == null ? Collections.emptyList() : childrenCache);
}
@Override public int getChildCount() {
if (childrenCache == null) loadChildren();
return childrenCache == null ? 0 : childrenCache.size();
}
/** 掃描子目錄 (僅目錄,不含檔案,避免節點爆炸) */
private synchronized void loadChildren() {
if (childrenCache != null) return; // 雙重檢查
if (!isDirectory) { childrenCache = Collections.emptyList(); return; }
childrenCache = new ArrayList<>();
try (DirectoryStream stream = Files.newDirectoryStream(path,
entry -> Files.isDirectory(entry) && !Files.isHidden(entry))) { // 過濾隱藏、只取目錄
for (Path child : stream) {
childrenCache.add(new FileTreeNode(child));
}
childrenCache.sort(Comparator.comparing(FileTreeNode::getName, String.CASE_INSENSITIVE_ORDER));
} catch (IOException | SecurityException e) {
childrenCache = Collections.emptyList(); // 權限不足視為空
System.err.println("無法讀取目錄: " + path + " - " + e.getMessage());
}
}
// TreeNode 接口其他方法委託...
@Override public TreeNode getParent() { return null; } // 由模型維護父子關係
@Override public TreeNode getChildAt(int index) { return children().nextElement(); } // 簡化
@Override public int getIndex(TreeNode node) { return childrenCache.indexOf(node); }
}
/** 樹模型:管理根節點,轉發事件 */
public class FileSystemTreeModel extends DefaultTreeModel {
public FileSystemTreeModel(Path rootPath) {
super(new FileTreeNode(rootPath));
}
/** 當節點展開時,JTree 會呼叫 getChildCount -> 觸發懶加載 -> 需通知視圖更新 */
@Override public void nodeStructureChanged(TreeNode node) {
super.nodeStructureChanged(node); // 觸發 treeStructureChanged 事件
}
/** 供控制器呼叫:手動刷新某節點 (如新建資料夾後) */
public void refreshNode(FileTreeNode node) {
node.childrenCache = null; // 清除緩存
nodeStructureChanged(node);
}
}
public class FileTreeCellRenderer extends DefaultTreeCellRenderer {
private final FileSystemView fsv = FileSystemView.getFileSystemView();
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) {
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
if (value instanceof FileTreeNode node) {
// 1. 系統原生圖標 (資料夾/檔案類型圖標,自動適配 OS 主題)
setIcon(node.getIcon());
// 2. 系統顯示名稱 (Windows 下會顯示「文檔」而非 "Documents")
setText(node.getName());
// 3. 工具提示顯示完整路徑
setToolTipText(node.getPath().toString());
// 4. 目錄加粗 (可選)
if (!leaf) setFont(getFont().deriveFont(Font.BOLD));
}
return this;
}
}
public class FileTreeBrowser extends JPanel {
private final JTree tree;
private final FileSystemTreeModel treeModel;
private final JLabel statusLabel;
public FileTreeBrowser(Path initialPath) {
setLayout(new BorderLayout());
// 1. 模型與樹
treeModel = new FileSystemTreeModel(initialPath);
tree = new JTree(treeModel);
tree.setCellRenderer(new FileTreeCellRenderer());
tree.setShowsRootHandles(true); // 根節點也顯示展開把手
tree.setRootVisible(true);
tree.setExpandsSelectedPaths(true); // 選中路徑自動展開父節點
// 2. 滾動面板
JScrollPane scrollPane = new JScrollPane(tree);
scrollPane.setPreferredSize(new Dimension(400, 600));
add(scrollPane, BorderLayout.CENTER);
// 3. 底部狀態列
statusLabel = new JLabel("就緒: " + initialPath);
statusLabel.setBorder(BorderFactory.createEmptyBorder(2, 5, 2, 5));
add(statusLabel, BorderLayout.SOUTH);
// 4. 事件處理
tree.addTreeExpansionListener(new TreeExpansionListener() {
@Override public void treeExpanded(TreeExpansionEvent e) {
FileTreeNode node = (FileTreeNode) e.getPath().getLastPathComponent();
statusLabel.setText("展開: " + node.getPath());
// 關鍵:展開後通知模型結構變更,刷新視圖顯示新載入的子節點
treeModel.nodeStructureChanged(node);
}
@Override public void treeCollapsed(TreeExpansionEvent e) {}
});
// 5. 雙擊開啟 (檔案用系統預設程式,目錄展開)
tree.addMouseListener(new MouseAdapter() {
@Override public void mouseClicked(MouseEvent e) {
if (e.getClickCount() == 2) {
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
if (path != null) {
FileTreeNode node = (FileTreeNode) path.getLastPathComponent();
if (node.getAllowsChildren()) {
// 目錄: 切換展開狀態
if (tree.isExpanded(path)) tree.collapsePath(path);
else tree.expandPath(path);
} else {
// 檔案: 系統開啟
openFileWithSystemDefault(node.getPath());
}
}
}
}
});
// 6. 右鍵選單 (新建資料夾、重命名、刪除、刷新)
JPopupMenu popup = new JPopupMenu();
JMenuItem newFolder = new JMenuItem("新建資料夾");
JMenuItem rename = new JMenuItem("重命名");
JMenuItem delete = new JMenuItem("刪除");
JMenuItem refresh = new JMenuItem("刷新");
popup.add(newFolder); popup.add(rename); popup.add(delete); popup.addSeparator(); popup.add(refresh);
tree.addMouseListener(new MouseAdapter() {
@Override public void mousePressed(MouseEvent e) { maybeShowPopup(e); }
@Override public void mouseReleased(MouseEvent e) { maybeShowPopup(e); }
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
TreePath path = tree.getPathForLocation(e.getX(), e.getY());
if (path != null) {
tree.setSelectionPath(path);
popup.show(tree, e.getX(), e.getY());
}
}
}
});
newFolder.addActionListener(e -> createNewFolder());
refresh.addActionListener(e -> refreshSelectedNode());
// rename, delete 邏輯類似...
}
private void createNewFolder() {
TreePath path = tree.getSelectionPath();
if (path == null) return;
FileTreeNode parent = (FileTreeNode) path.getLastPathComponent();
if (!parent.getAllowsChildren()) parent = (FileTreeNode) parent.getParent(); // 若選中檔案,在其父目錄建立
String name = JOptionPane.showInputDialog(this, "資料夾名稱:", "新建資料夾", JOptionPane.PLAIN_MESSAGE);
if (name != null && !name.trim().isEmpty()) {
Path newDir = parent.getPath().resolve(name.trim());
try {
Files.createDirectory(newDir);
treeModel.refreshNode(parent); // 刷新父節點顯示新資料夾
// 可選: 自動選中新節點
} catch (IOException ex) {
JOptionPane.showMessageDialog(this, "建立失敗: " + ex.getMessage(), "錯誤", JOptionPane.ERROR_MESSAGE);
}
}
}
private void refreshSelectedNode() {
TreePath path = tree.getSelectionPath();
if (path != null) {
FileTreeNode node = (FileTreeNode) path.getLastPathComponent();
treeModel.refreshNode(node);
}
}
private void openFileWithSystemDefault(Path file) {
if (Desktop.isDesktopSupported()) {
try { Desktop.getDesktop().open(file.toFile()); }
catch (IOException ex) { JOptionPane.showMessageDialog(this, "無法開啟: " + ex.getMessage()); }
}
}
// 測試啟動
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
// 系統原生外觀 (Windows/Linux/macOS 都會像原生應用)
try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
catch (Exception ignored) {}
JFrame frame = new JFrame("Java 文件瀏覽器");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
Path startPath = Paths.get(System.getProperty("user.home")); // 從用戶家目錄開始
frame.add(new FileTreeBrowser(startPath));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
| 操作 | 舊 `java.io.File` | 新 `java.nio.file` (JDK 7+) |
|---|---|---|
| 列出目錄 | `file.listFiles()` (一次載入全部陣列) | `Files.newDirectoryStream(dir, filter)` (迭代器、流式、可過濾) |
| 判斷目錄/檔案 | `file.isDirectory()` | `Files.isDirectory(path)` / `Files.isRegularFile(path)` |
| 建立目錄 | `file.mkdirs()` | `Files.createDirectories(path)` (拋出異常可診斷原因) |
| 刪除 | `file.delete()` (失敗無原因) | `Files.delete(path)` / `Files.deleteIfExists(path)` |
| 路徑解析 | `new File(parent, child)` | `parent.resolve(child)` / `parent.resolveSibling(name)` |
| 屬性讀取 | `file.length()`, `file.lastModified()` | `Files.readAttributes(path, BasicFileAttributes.class)` (一次讀取所有) |
這個 `FileTreeBrowser` 不只是教學範例,更是一個**可直接整合到 IDE、日誌分析工具、配置管理介面**的生產級元件。掌握其懶加載模型、系統整合渲染、NIO.2 現代 API 用法,將為您的 Swing/JavaFX 桌面開發奠定堅實基礎。