Swing JFileChooser 深度掌握:文件/目錄選擇、多選、過濾器、預覽、自定義與線程安全
JFileChooser 是 Swing 提供的標準文件選擇對話框,支援跨平台一致體驗。本文系統講解核心 API、文件過濾器、圖片預覽、自定義配件、模態/非模態模式、EDT 線程安全等實戰技巧。
基礎用法速覽
JFileChooser chooser = new JFileChooser();
chooser.setDialogTitle("選擇配置文件");
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY); // 默認
chooser.setAcceptAllFileFilterUsed(false); // 隱藏 "All Files"
chooser.setMultiSelectionEnabled(false); // 單選
int result = chooser.showOpenDialog(parentComponent);
if (result == JFileChooser.APPROVE_OPTION) {
File selectedFile = chooser.getSelectedFile();
// 處理文件...
}
選擇模式對照
| 常量 | 行為 | 獲取結果方法 |
FILES_ONLY | 僅選文件 | getSelectedFile() / getSelectedFiles() |
DIRECTORIES_ONLY | 僅選目錄 | 同左 |
FILES_AND_DIRECTORIES | 文件目錄皆可選 | 同左 |
文件過濾器:FileFilter 實現
// 1. 匿名內部類 (簡單場景)
chooser.setFileFilter(new FileFilter() {
@Override public boolean accept(File f) {
return f.isDirectory() || f.getName().toLowerCase().endsWith(".xml");
}
@Override public String getDescription() {
return "XML 配置文件 (*.xml)";
}
});
// 2. 可複用過濾器類 (推薦)
public class ExtensionFileFilter extends FileFilter {
private final String[] extensions;
private final String description;
public ExtensionFileFilter(String description, String... extensions) {
this.description = description;
this.extensions = Arrays.stream(extensions)
.map(e -> e.toLowerCase().startsWith(".") ? e : "." + e)
.toArray(String[]::new);
}
@Override public boolean accept(File f) {
if (f.isDirectory()) return true;
String name = f.getName().toLowerCase();
return Arrays.stream(extensions).anyMatch(name::endsWith);
}
@Override public String getDescription() { return description; }
}
// 使用
chooser.addChoosableFileFilter(new ExtensionFileFilter("圖片文件", "jpg", "png", "gif", "bmp"));
chooser.addChoosableFileFilter(new ExtensionFileFilter("文檔", "pdf", "doc", "docx"));
chooser.setFileFilter(chooser.getChoosableFileFilters()[0]); // 設置默認
圖片預覽配件:Accessory 實現
chooser.setAccessory(new ImagePreviewAccessor(chooser));
class ImagePreviewAccessor extends JComponent {
private final JLabel previewLabel = new JLabel();
private final JFileChooser chooser;
public ImagePreviewAccessor(JFileChooser chooser) {
this.chooser = chooser;
setLayout(new BorderLayout());
setPreferredSize(new Dimension(200, 200));
add(new JScrollPane(previewLabel), BorderLayout.CENTER);
// 監聽選擇變化
chooser.addPropertyChangeListener(e -> {
if (JFileChooser.SELECTED_FILE_CHANGED_PROPERTY.equals(e.getPropertyName())) {
File file = (File) e.getNewValue();
updatePreview(file);
}
});
}
private void updatePreview(File file) {
if (file == null || !isImage(file)) {
previewLabel.setIcon(null);
previewLabel.setText("無預覽");
return;
}
try {
ImageIcon icon = new ImageIcon(file.getAbsolutePath());
// 縮放適應預覽區
Image img = icon.getImage().getScaledInstance(180, 180, Image.SCALE_SMOOTH);
previewLabel.setIcon(new ImageIcon(img));
previewLabel.setText(null);
} catch (Exception ex) {
previewLabel.setIcon(null);
previewLabel.setText("預覽失敗");
}
}
private boolean isImage(File f) {
String name = f.getName().toLowerCase();
return name.endsWith(".jpg") || name.endsWith(".png") || name.endsWith(".gif") || name.endsWith(".bmp");
}
}
自定義配件:底部按鈕/選項面板
JPanel accessoryPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
JCheckBox overwriteCheck = new JCheckBox("覆蓋現有文件");
JCheckBox backupCheck = new JCheckBox("自動備份");
accessoryPanel.add(overwriteCheck);
accessoryPanel.add(backupCheck);
chooser.setAccessory(accessoryPanel);
// 獲取用戶選項
boolean overwrite = overwriteCheck.isSelected();
boolean backup = backupCheck.isSelected();
模態 vs 非模態對話框
| 方法 | 模式 | 返回時機 | 適用場景 |
showOpenDialog(parent) | 模態 | 用戶關閉後 | 標準工作流、阻塞等待結果 |
showSaveDialog(parent) | 模態 | 用戶關閉後 | 另存為操作 |
showDialog(parent, "按鈕文本") | 模態 | 用戶關閉後 | 自定義操作 (如 "導入") |
createDialog(parent, title) | 非模態 | 立即返回 JDialog | 需與主窗口交互、長時間操作 |
非模態對話框完整示例
JDialog dialog = chooser.createDialog(null, "選擇數據文件");
chooser.addActionListener(e -> {
if (JFileChooser.APPROVE_SELECTION.equals(e.getActionCommand())) {
File[] files = chooser.getSelectedFiles();
// 異步處理文件...
dialog.setVisible(false); // 手動關閉
} else if (JFileChooser.CANCEL_SELECTION.equals(e.getActionCommand())) {
dialog.setVisible(false);
}
});
dialog.setVisible(true); // 非阻塞
EDT 線程安全鐵律
- 所有 Swing 操作必須在 EDT:
SwingUtilities.invokeLater(() -> chooser.showOpenDialog(parent))
- 文件處理移出 EDT:選擇完成後用
CompletableFuture.runAsync() 或線程池處理耗時 IO
- 模態對話框自動處理 EDT:
showOpenDialog 內部已處理事件泵,但回調仍在 EDT
// 正確模式
SwingUtilities.invokeLater(() -> {
int result = chooser.showOpenDialog(frame);
if (result == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
// 異步處理
executor.submit(() -> {
parseLargeFile(file);
SwingUtilities.invokeLater(() -> updateUI(result));
});
}
});
常見問題與解決方案
| 問題 | 原因 | 解決 |
| 對話框不顯示/卡死 | 在非 EDT 調用模態 showXxxDialog | 包裹在 invokeLater 中 |
| 文件過濾器不生效 | 未設置 setFileFilter 或 acceptAllFileFilterUsed=true | setAcceptAllFileFilterUsed(false) |
| Linux 下中文亂碼 | 字體缺失 | 安裝文泉驛/思源字體,或設置 -Dawt.useSystemAAFontSettings=on |
| 網絡路徑 (SMB/NFS) 卡頓 | 文件系統掛載延遲 | setCurrentDirectory 避免網絡路徑,或異步預加載 |
| 大目錄打開慢 | 文件系統枚舉慢 | 使用 FileSystemView.getFileSystemView().getFiles() 自定義模型 |
現代替代方案
- JavaFX FileChooser:現代 UI、原生對話框、更好高 DPI 支援
- JNA 調用原生對話框:Windows
IFileOpenDialog、Linux zenity/kdialog、macOS NSOpenPanel
- Web 應用:HTML
<input type="file"> + JavaScript File API