本文展示如何使用 Java Swing 的 JTree 元件,結合 FileSystemView 實現一個類似 Windows 檔案總管的樹狀目錄瀏覽器。完整代碼包含自定義樹模型、節點渲染器、展開/摺疊監聽等核心功能。
JTree,作為主視圖元件TreeModel,提供樹狀資料模型I_fileSystem,表示資料夾節點,支援延遲加載子節點DefaultTreeCellRenderer,自定義節點圖示顯示package tl.exercise.swing;
import java.awt.Component;
import java.io.File;
import java.util.Vector;
import javax.swing.Icon;
import javax.swing.JTree;
import javax.swing.event.*;
import javax.swing.filechooser.FileSystemView;
import javax.swing.tree.*;
public class FileTree extends JTree {
private static final long serialVersionUID = 0;
private FileList theList;
public FileTree(FileList list) {
this.theList = list;
// 設定樹模型,根節點為桌面
setModel(new FileSystemModel(new FolderNode()));
// 自定義渲染器顯示系統圖示
this.setCellRenderer(new FolderRenderer());
// 選擇監聽
addTreeSelectionListener(new TreeSelectionListener() {
@Override
public void valueChanged(TreeSelectionEvent tse) {
// 可在此處理選擇事件
}
});
this.setSelectionRow(0); // 預設選中根節點
}
// 轉發選擇事件給 FileList
public void fireValueChanged(TreeSelectionEvent tse) {
TreePath tp = tse.getNewLeadSelectionPath();
Object o = tp.getLastPathComponent();
theList.fireTreeSelectionChanged((FolderNode) o);
}
@Override
public void fireTreeCollapsed(TreePath path) {
super.fireTreeCollapsed(path);
TreePath curpath = getSelectionPath();
if (path.isDescendant(curpath)) {
setSelectionPath(path);
}
}
@Override
public void fireTreeWillExpand(TreePath path) {
System.out.println("Path will expand: " + path);
}
@Override
public void fireTreeWillCollapse(TreePath path) {
System.out.println("Path will collapse: " + path);
}
// 內部類:樹模型
class FileSystemModel implements TreeModel {
private I_fileSystem theRoot;
private char fileType = I_fileSystem.DIRECTORY;
public FileSystemModel(I_fileSystem fs) {
this.theRoot = fs;
}
@Override public Object getRoot() { return theRoot; }
@Override public Object getChild(Object parent, int index) {
return ((I_fileSystem) parent).getChild(fileType, index);
}
@Override public int getChildCount(Object parent) {
return ((I_fileSystem) parent).getChildCount(fileType);
}
@Override public boolean isLeaf(Object node) {
return ((I_fileSystem) node).isLeaf(fileType);
}
@Override public int getIndexOfChild(Object parent, Object child) {
return ((I_fileSystem) parent).getIndexOfChild(fileType, child);
}
@Override public void valueForPathChanged(TreePath path, Object newValue) {}
@Override public void addTreeModelListener(TreeModelListener l) {}
@Override public void removeTreeModelListener(TreeModelListener l) {}
}
// 介面:檔案系統節點契約
interface I_fileSystem {
char DIRECTORY = 'D';
char FILE = 'F';
char ALL = 'A';
Icon getIcon();
I_fileSystem getChild(char fileType, int index);
int getChildCount(char fileType);
boolean isLeaf(char fileType);
int getIndexOfChild(char fileType, Object child);
}
// 實作:資料夾節點(核心邏輯)
class FolderNode implements I_fileSystem {
private static FileSystemView fsView = FileSystemView.getFileSystemView();
private static boolean showHidden = true;
private File theFile;
private Vector<File> all = new Vector<>();
private Vector<File> folder = new Vector<>();
public void setShowHidden(boolean ifshow) { showHidden = ifshow; }
@Override public Icon getIcon() { return fsView.getSystemIcon(theFile); }
@Override public String toString() {
return fsView.getSystemDisplayName(theFile);
}
// 根節點建構子:預設為桌面
public FolderNode() {
theFile = fsView.getHomeDirectory();
prepareChildren();
}
// 子節點建構子
private FolderNode(File file) {
this.theFile = file;
prepareChildren();
}
// 掃描子目錄/檔案
private void prepareChildren() {
File[] files = fsView.getFiles(theFile, showHidden);
for (File f : files) {
all.add(f);
// 只加入資料夾(排除捷徑 .lnk)
if (f.isDirectory() && !f.toString().toLowerCase().endsWith(".lnk")) {
folder.add(f);
}
}
}
@Override public I_fileSystem getChild(char fileType, int index) {
switch (fileType) {
case DIRECTORY: return new FolderNode(folder.get(index));
case ALL: return new FolderNode(all.get(index));
default: return null;
}
}
@Override public int getChildCount(char fileType) {
switch (fileType) {
case DIRECTORY: return folder.size();
case ALL: return all.size();
default: return -1;
}
}
@Override public boolean isLeaf(char fileType) {
switch (fileType) {
case DIRECTORY: return folder.isEmpty();
case ALL: return all.isEmpty();
case FILE: return true;
default: return true;
}
}
@Override public int getIndexOfChild(char fileType, Object child) {
if (!(child instanceof FolderNode)) return -1;
File childFile = ((FolderNode) child).theFile;
switch (fileType) {
case DIRECTORY: return folder.indexOf(childFile);
case ALL: return all.indexOf(childFile);
default: return -1;
}
}
}
// 節點渲染器:使用系統原生圖示
class FolderRenderer extends DefaultTreeCellRenderer {
private static final long serialVersionUID = 1L;
@Override
public Component getTreeCellRendererComponent(JTree tree, Object value,
boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) {
I_fileSystem node = (I_fileSystem) value;
Icon icon = node.getIcon();
setLeafIcon(icon);
setOpenIcon(icon);
setClosedIcon(icon);
return super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
}
}
}
FileSystemView fsView = FileSystemView.getFileSystemView();
// 獲取桌面目錄(根節點)
File desktop = fsView.getHomeDirectory();
// 獲取系統圖示
Icon icon = fsView.getSystemIcon(file);
// 獲取系統顯示名稱(含本地化)
String displayName = fsView.getSystemDisplayName(file);
// 列舉子項目(含隱藏檔選項)
File[] children = fsView.getFiles(parent, showHidden);
代碼中 FolderNode 在建構時即掃描所有子項目並存入 Vector。作者在註解中提到:
「為了速度,每個模型實例都包含子目錄的檔案物件,目前沒有釋放方法,直到程式結束。如果檔案系統夠大且 JVM 記憶體設定太小,可能會導致記憶體不足。」
改進方向:實作真正的延遲加載,僅在節點首次展開時掃描子目錄,並在摺疊時釋放子節點引用。
繼承 DefaultTreeModel 並使用 DefaultMutableTreeNode 是更簡單的做法,但自定義 TreeModel 介面可:
DefaultMutableTreeNode 中的額外開銷需搭配 FileList 類(未在代碼中提供,需自行實作)使用。最小啟動範例:
import javax.swing.*;
public class FileTreeDemo {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("檔案總管樹狀視圖");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new JScrollPane(new FileTree(null))); // 傳入 null 或實作 FileList
frame.setSize(400, 600);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
| 需求 | 解決方案 |
|---|---|
| 顯示檔案大小、修改時間 | 擴展 I_fileSystem 介面,在 FolderNode 中快取 File 物件屬性 |
| 右鍵選單(複製、刪除、重命名) | 為 JTree 添加 MouseListener,彈出 JPopupMenu |
| 拖放操作 | 實作 TransferHandler,支援 DnD |
| 過濾特定副檔名 | 在 prepareChildren() 中添加過濾邏輯 |
| 多根節點(我的電腦、網路位置) | 修改 FileSystemModel.getRoot() 返回虛擬根節點,子節點掛載各邏輯磁碟 |