「查找替換」是文本編輯器、配置管理工具、代碼生成器的核心功能。本文將構建一個基於 Swing 的可視化查找替換工具,涵蓋:GUI 佈局、文件選擇器、編碼處理、大文件分塊讀寫、異常安全、桌面集成(打開文件)、國際化預留。代碼採用事件分發線程 (EDT) 安全、try-with-resources、函數式回調等現代最佳實踐。
┌─────────────────────────────────────┐
│ ReplaceFileText (JFrame) │
├─────────────────────────────────────┤
│ [選擇文件] [文本框: 路徑] │
│ [搜索文本:] [文本框] │
│ [替換為:] [文本框] │
│ [替換] [打開文件] │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ 核心業務邏輯層 │
│ FileReader/Writer (編碼顯式) │
│ StringBuilder 替換 (內存模式) │
│ 大文件流式替換 (可選擴展) │
└─────────────────────────────────────┘
import javax.swing.*;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
public class ReplaceFileText extends JFrame {
private static final long serialVersionUID = 1L;
// UI 組件
private final JTextField filePathField = new JTextField();
private final JTextField searchField = new JTextField();
private final JTextField replaceField = new JTextField();
private final JButton selectBtn = new JButton("選擇文件");
private final JButton replaceBtn = new JButton("執行替換");
private final JButton openBtn = new JButton("打開文件");
// 狀態
private File selectedFile;
public ReplaceFileText() {
initUI();
bindEvents();
}
private void initUI() {
setTitle("文本查找替換工具 v1.0");
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setSize(600, 220);
setLocationRelativeTo(null); // 屏幕居中
setLayout(new BorderLayout(10, 10));
// 主面板:GridBagLayout 精確控制
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(15, 15, 15, 15));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
// 第 0 行:選擇文件
gbc.gridx = 0; gbc.gridy = 0; gbc.weightx = 0;
mainPanel.add(selectBtn, gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridwidth = 3;
filePathField.setEditable(false);
filePathField.setToolTipText("點擊「選擇文件」按鈕選擇目標文件");
mainPanel.add(filePathField, gbc);
// 第 1 行:搜索文本
gbc.gridx = 0; gbc.gridy = 1; gbc.weightx = 0; gbc.gridwidth = 1;
mainPanel.add(new JLabel("搜索文本:"), gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridwidth = 3;
searchField.setToolTipText("支持正則表達式(勾選後啟用)");
mainPanel.add(searchField, gbc);
// 第 2 行:替換文本
gbc.gridx = 0; gbc.gridy = 2; gbc.weightx = 0; gbc.gridwidth = 1;
mainPanel.add(new JLabel("替換為:"), gbc);
gbc.gridx = 1; gbc.weightx = 1.0; gbc.gridwidth = 3;
replaceField.setToolTipText("留空表示刪除匹配內容");
mainPanel.add(replaceField, gbc);
// 第 3 行:操作按鈕
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 4; gbc.weightx = 1.0;
gbc.anchor = GridBagConstraints.CENTER;
JPanel btnPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 15, 0));
btnPanel.add(replaceBtn);
btnPanel.add(openBtn);
mainPanel.add(btnPanel, gbc);
add(mainPanel, BorderLayout.CENTER);
// 狀態欄
JLabel statusLabel = new JLabel("就緒");
statusLabel.setBorder(BorderFactory.createEtchedBorder());
add(statusLabel, BorderLayout.SOUTH);
}
private void bindEvents() {
// 選擇文件
selectBtn.addActionListener(e -> chooseFile());
// 執行替換
replaceBtn.addActionListener(e -> performReplace());
// 打開文件
openBtn.addActionListener(e -> openFile());
}
private void chooseFile() {
JFileChooser chooser = new JFileChooser(System.getProperty("user.dir"));
chooser.setFileFilter(new FileNameExtensionFilter(
"文本文件 (*.txt, *.java, *.xml, *.properties, *.json, *.md)",
"txt", "java", "xml", "properties", "json", "md", "log", "csv"
));
chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
chooser.setDialogTitle("選擇要處理的文件");
int result = chooser.showOpenDialog(this);
if (result == JFileChooser.APPROVE_OPTION) {
selectedFile = chooser.getSelectedFile();
filePathField.setText(selectedFile.getAbsolutePath());
filePathField.setCaretPosition(0);
}
}
private void performReplace() {
if (selectedFile == null || !selectedFile.exists()) {
showError("請先選擇有效文件");
return;
}
String searchText = searchField.getText();
if (searchText.isEmpty()) {
showError("搜索文本不能為空");
searchField.requestFocus();
return;
}
String replaceText = replaceField.getText(); // 可為空
// 確認對話框
int confirm = JOptionPane.showConfirmDialog(this,
String.format("即將在文件中替換:\n'%s' → '%s'\n\n文件:%s\n\n是否繼續?",
searchText, replaceText.isEmpty() ? "(刪除)" : replaceText, selectedFile.getName()),
"確認替換", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
if (confirm != JOptionPane.YES_OPTION) return;
// 在後台線程執行耗時操作,避免凍結 UI
SwingWorker worker = new SwingWorker<>() {
@Override
protected Void doInBackground() throws Exception {
publish("正在讀取文件...");
String content = readFile(selectedFile);
publish("正在執行替換...");
int count = countOccurrences(content, searchText);
if (count == 0) {
publish("未找到匹配內容");
return null;
}
String newContent = content.replace(searchText, replaceText);
publish("正在寫入文件...");
writeFile(selectedFile, newContent);
publish(String.format("替換完成!共替換 %d 處", count));
return null;
}
@Override
protected void process(java.util.List chunks) {
for (String msg : chunks) {
// 更新狀態欄或彈出進度
System.out.println(msg); // 實際應更新 UI 狀態欄
}
}
@Override
protected void done() {
try {
get(); // 拋出異常
JOptionPane.showMessageDialog(ReplaceFileText.this,
"替換操作已完成", "成功", JOptionPane.INFORMATION_MESSAGE);
} catch (Exception ex) {
showError("替換失敗: " + ex.getMessage());
ex.printStackTrace();
}
}
};
worker.execute();
}
/** 讀取文件內容(自動檢測編碼或指定 UTF-8) */
private String readFile(File file) throws IOException {
// 簡化:固定 UTF-8,生產可用 juniversalchardet 自動檢測
return Files.readString(file.toPath(), StandardCharsets.UTF_8);
}
/** 寫入文件(UTF-8 無 BOM) */
private void writeFile(File file, String content) throws IOException {
Files.writeString(file.toPath(), content, StandardCharsets.UTF_8);
}
/** 計算出現次數(不使用正則,純字符串匹配) */
private int countOccurrences(String text, String target) {
int count = 0, idx = 0;
while ((idx = text.indexOf(target, idx)) != -1) {
count++;
idx += target.length();
}
return count;
}
private void openFile() {
if (selectedFile == null || !selectedFile.exists()) {
showError("無有效文件可打開");
return;
}
try {
if (Desktop.isDesktopSupported()) {
Desktop.getDesktop().edit(selectedFile); // 使用系統關聯編輯器
} else {
showError("當前平台不支持桌面集成");
}
} catch (IOException ex) {
showError("打開文件失敗: " + ex.getMessage());
}
}
private void showError(String msg) {
JOptionPane.showMessageDialog(this, msg, "錯誤", JOptionPane.ERROR_MESSAGE);
}
/** 啟動入口 */
public static void main(String[] args) {
// 設置系統觀感
try { UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); }
catch (Exception ignored) {}
// 在 EDT 中啟動
SwingUtilities.invokeLater(() -> new ReplaceFileText().setVisible(true));
}
}
Files.readString/writeString 內建 UTF-8 默認,但顯式更安全| 模式 | 適用場景 | 實現要點 |
|---|---|---|
| 內存模式 (當前) | 文件 < 100MB,內存充足 | 一次性讀入 StringBuilder,replace 後寫回 |
| 流式模式 | 大文件、內存受限 | BufferedReader + BufferedWriter,行級/塊級處理,需處理跨塊匹配 |
| 記憶體映射 | 超大文件、高性能 | FileChannel.map() + 直接內存操作 |
doInBackground():工作線程執行耗時 IOpublish()/process():安全向 EDT 推送進度更新done():EDT 執行完成回調,可獲取異常open():默認程序打開(如瀏覽器開 HTML)edit():編輯器打開(如記事本開 .txt)print():打印Desktop.isDesktopSupported() 守衛Pattern.compile(searchText).matcher(content).replaceAll(replaceText)Pattern.CASE_INSENSITIVE、\\b 邊界.bak,或內存保存快照JDiff 或 DiffMatchPatch 高亮顯示變更juniversalchardet 或 Apache Tika本文實現的 ReplaceFileText 工具具備:
代碼零第三方依賴,僅需 JDK 11+,可直接打包為可執行 JAR 分發使用。若項目已使用 JavaFX,建議遷移至 JavaFX 實現更現代的 UI 體驗。