Swing 不是執行緒安全的,**所有 UI 操作必須在事件分發執行緒 (Event Dispatch Thread, EDT) 執行**。當在 EDT 中執行耗時任務 (資料庫批次操作、複雜運算、遠程呼叫) 時,EDT 被阻塞,導致介面無法重繪、無法響應點擊、**視覺上呈現「凍結/假死」**。本文展示如何利用 SwingWorker (Java 6+) 正確實現「後台執行緒執行任務 + 前台顯示等待對話框 + 完成自動關閉」模式,並對比舊版手工執行緒管理的缺陷。
// ❌ 錯誤:在 EDT 中直接執行耗時操作
btnStart.addActionListener(e -> {
// 這裡會阻塞 EDT,介面凍結,進度條不轉、無法移動視窗
for (int i = 0; i < 100000; i++) System.out.println(i);
JOptionPane.showMessageDialog(frame, "完成");
});
javax.swing.SwingWorker 專為 Swing 異步任務設計:
doInBackground():在**工作者執行緒**執行耗時邏輯,返回結果類型 T。process(List) :在 **EDT** 處理 publish(V...) 發布的中間結果 (更新進度條、列表)。done():任務完成 (正常/異常/取消) 後在 **EDT** 自動調用,安全更新 UI。get():阻塞等待結果 (僅在非 EDT 呼叫)。PropertyChangeListener 支援進度屬性 (progress 0-100)。
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List;
public class WaitDialogDemo extends JFrame {
private JButton btnStart;
private JProgressBar progressBar;
private SwingWorker worker;
public WaitDialogDemo() {
setTitle("SwingWorker 等待對話框演示");
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 200);
setLocationRelativeTo(null);
setLayout(new BorderLayout(10, 10));
btnStart = new JButton("開始耗時任務 (模擬 5 秒)");
progressBar = new JProgressBar(0, 100);
progressBar.setStringPainted(true);
progressBar.setVisible(false);
JPanel panel = new JPanel(new BorderLayout(5, 5));
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
panel.add(btnStart, BorderLayout.NORTH);
panel.add(progressBar, BorderLayout.CENTER);
add(panel);
btnStart.addActionListener(this::startTask);
}
private void startTask(ActionEvent e) {
btnStart.setEnabled(false);
progressBar.setValue(0);
progressBar.setVisible(true);
// 1. 建立 SwingWorker
worker = new SwingWorker() {
@Override
protected Void doInBackground() throws Exception {
// 模擬耗時任務:分 100 步,每步 50ms
for (int i = 1; i <= 100; i++) {
if (isCancelled()) break; // 支援取消
Thread.sleep(50); // 耗時操作
publish(i); // 發布進度 (觸發 process)
}
return null;
}
@Override
protected void process(List chunks) {
// 在 EDT 執行,安全更新 UI
int latest = chunks.get(chunks.size() - 1);
progressBar.setValue(latest);
}
@Override
protected void done() {
// 在 EDT 執行,任務結束統一處理
btnStart.setEnabled(true);
progressBar.setVisible(false);
try {
get(); // 拋出執行期異常 (如有)
if (!isCancelled()) {
JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務完成!");
}
} catch (CancellationException ex) {
JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務已取消");
} catch (Exception ex) {
JOptionPane.showMessageDialog(WaitDialogDemo.this, "任務失敗: " + ex.getMessage(), "錯誤", JOptionPane.ERROR_MESSAGE);
ex.printStackTrace();
}
}
};
// 2. 執行
worker.execute();
// 3. 可選:顯示模態對話框阻塞用戶操作,提供取消按鈕
showWaitDialog();
}
private void showWaitDialog() {
JDialog dialog = new JDialog(this, "處理中...", true); // 模態
dialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
dialog.setLayout(new BorderLayout(10, 10));
dialog.add(new JLabel("正在執行任務,請稍候..."), BorderLayout.NORTH);
JProgressBar dialogBar = new JProgressBar(0, 100);
dialogBar.setStringPainted(true);
dialog.add(dialogBar, BorderLayout.CENTER);
JButton btnCancel = new JButton("取消");
btnCancel.addActionListener(ev -> {
worker.cancel(true); // 中斷 doInBackground
dialog.dispose();
});
dialog.add(btnCancel, BorderLayout.SOUTH);
dialog.pack();
dialog.setLocationRelativeTo(this);
dialog.setVisible(true); // 阻塞至此,直到 dispose
}
public static void main(String[] args) {
// 確保 UI 建立在 EDT
SwingUtilities.invokeLater(() -> new WaitDialogDemo().setVisible(true));
}
}
| 模式 | 適用場景 | 實現 |
|---|---|---|
| 非模態 (進度條嵌入主視窗) | 用戶可繼續操作其他功能、任務較長 | 主視窗放置 JProgressBar,SwingWorker.process 更新 |
| 模態對話框 (阻塞用戶輸入) | 關鍵操作必須序列化、防止重複提交、任務較短 | JDialog(..., true) + setVisible(true) 阻塞,done() 中 dispose() |