不靈敏的 GUI 會降低應用程式可用性。當出現以下現象時,通常說介面反應不靈敏:
這些現象很大程度上與事件處理方法相關。編寫 Swing 應用程式時,幾乎必然要編寫方法回應滑鼠點擊、鍵盤輸入等事件,在方法中觸發查詢、更新資料庫等動作。本文透過實例分析介紹基本概念、常見錯誤與解決方案。
務必記住:事件回應方法的程式碼都在 EDT 中執行,除非你啟用另一個執行緒。
什麼是 EDT?單一執行緒規則:一旦 Swing 元件被實現,所有可能影響或依賴該元件狀態的程式碼都應在 EDT 執行。實現元件有兩種方式:
show()、pack() 或 setVisible(true)單一執行緒規則根源在於 Swing 元件庫大部分方法非執行緒安全。為支援單一執行緒模型,Swing 提供專門完成元件相關操作的執行緒——EDT。事件回應方法通常由 EDT 呼叫,除非自己編碼呼叫。初學者常犯錯誤:在事件回應方法中完成過多與修改元件無直接關聯的程式碼,導致元件反應緩慢。例如:
String str = null;
this.textArea.setText("Please wait...");
try {
// do something really time consuming
str = "Hello, world!";
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
this.textArea.setText(str);
執行效果:按鈕似乎凍結一段時間,直到 Done. 出現才彈起。原因:Swing 元件更新與事件回應都在 EDT 完成,事件回應時 EDT 被佔據,元件無法更新,直到方法退出才可能更新。
final String[] str = new String[1];
this.jTextArea1.setText("Please wait...");
this.repaint();
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
str[0] = "Done.";
jTextArea1.setText(str[0]);
但無預期效果,按鈕仍凍結。查看 repaint() 原始碼:
PaintEvent e = new PaintEvent(this, PaintEvent.UPDATE,
new Rectangle(x, y, width, height));
Toolkit.getEventQueue().postEvent(e);
repaint() 實際上只是在事件佇列加入 UPDATE 事件,並未直接重繪元件,且該事件只能等待當前事件回應方法結束後才能被分派。因此只有繞過分派機制直接呼叫 paint 方法才能達到目的:
this.jTextArea1.setText("Please wait...");
this.paint(this.getGraphics());
try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); }
str[0] = "Done.";
jTextArea1.setText(str[0]);
雖按鈕彈起,但在 Done. 出現前無法按下按鈕——按鈕仍凍結在彈起狀態。呼叫重繪方法無法根本解決問題,需尋求其他方法。
參考《Rethinking Swing Threading》的改良方案:
final String[] str = new String[1];
this.jTextArea1.setText("Please wait...");
this.repaint();
new Thread() {
public void run() {
try {
Thread.sleep(1000L);
} catch (InterruptedException e) {
e.printStackTrace();
}
str[0] = "Done.";
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
jTextArea1.setText(str[0]);
}
});
}
}.start();
耗時操作放入另一執行緒,事件回應方法快速返回,EDT 可更新 UI 並回應其他事件。注意使用 invokeLater() 讓 EDT 執行指定程式碼。雖可不使用 invokeLater(),但會違背單一執行緒原則,帶來相對多執行緒不安全性。
若原程式加入 paint 覆寫繪製矩形,會發現原矩形被覆蓋部分,原因是未重繪該矩形,故在結尾加上 repaint() 呼叫:
javax.swing.SwingUtilities.invokeLater(new Runnable() {
public void run() {
jTextArea1.setText(str[0]);
repaint();
}
});
若覺得上述程式碼可讀性差,可透過 SwingWorker 簡化:實作 construct() 執行耗時操作,覆寫 finished() 完成元件更新。
this.jTextArea1.setText("Please wait...");
final SwingWorker worker = new SwingWorker() {
public Object construct() {
try { Thread.sleep(1000L); }
catch (InterruptedException e) { e.printStackTrace(); }
return "Done.";
}
public void finished() {
jTextArea1.setText(getValue().toString());
repaint();
}
};
worker.start();
此編程方式稱為同步方式。作者另提出透過訊息機制實現相同功能的「非同步」方法,較清晰但需編寫更多程式碼。
編寫使用 Swing 元件程式時請記住:
編寫高回應性 GUI 還需考慮更多問題,以上僅最基本部分。歡迎有興趣讀者來信討論。