首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 程式設計入門

Java Swing 文件瀏覽器實作:JTree 遞歸建立目錄樹與系統外觀整合

     編輯:Java 程式設計入門

構建一個桌面級的文件瀏覽器,是掌握 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;
    }
}

主瀏覽器元件:整合 JTree、異步載入、右鍵選單

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);
        });
    }
}

關鍵優化點總結

  1. 懶加載 (Lazy Loading): `FileTreeNode.children()` 首次呼叫時才掃描目錄,**啟動極快**,不會卡死 EDT 掃描整個硬碟。
  2. 系統原生圖標與名稱: `FileSystemView.getSystemIcon()` / `getSystemDisplayName()`,**零成本獲得原生體驗** (Windows 黃色資料夾、macOS 藍色資料夾、Linux 主題圖標)。
  3. 系統外觀 (LookAndFeel): `UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())` 一行代碼讓應用融入 OS。
  4. NIO.2 `Files.newDirectoryStream` + Lambda 過濾: 替代舊 `File.listFiles()`,支援流式處理、目錄流關閉、類型安全過濾。
  5. 異步操作建議: 對於網路磁碟/大目錄,`loadChildren()` 應移至 `SwingWorker` 背景執行緒,完成後 `SwingUtilities.invokeLater` 更新模型。
  6. 權限與異常處理: `SecurityException`、`IOException` 全捕獲,視為空目錄並記錄日誌,不讓單個目錄拖垮整棵樹。

從 `java.io.File` 到 `java.nio.file.Path` 的遷移對照

操作 舊 `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 桌面開發奠定堅實基礎。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved