首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

撰寫高回應性 Swing 圖形使用者介面:事件派發執行緒與多執行緒實戰

     編輯:Java 相關知識

問題現象

不靈敏的 GUI 會降低應用程式可用性。當出現以下現象時,通常說介面反應不靈敏:

  • 不回應事件
  • 介面無更新

這些現象很大程度上與事件處理方法相關。編寫 Swing 應用程式時,幾乎必然要編寫方法回應滑鼠點擊、鍵盤輸入等事件,在方法中觸發查詢、更新資料庫等動作。本文透過實例分析介紹基本概念、常見錯誤與解決方案。

Event-Dispatching Thread (EDT)

務必記住:事件回應方法的程式碼都在 EDT 中執行,除非你啟用另一個執行緒。

什麼是 EDT?單一執行緒規則:一旦 Swing 元件被實現,所有可能影響或依賴該元件狀態的程式碼都應在 EDT 執行。實現元件有兩種方式:

  1. 對頂層元件呼叫 show()、pack() 或 setVisible(true)
  2. 將元件加入已實現的容器中

單一執行緒規則根源在於 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 被佔據,元件無法更新,直到方法退出才可能更新。

錯誤嘗試:呼叫 repaint()

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 簡化編程

若覺得上述程式碼可讀性差,可透過 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 元件程式時請記住:

  1. 不要過度佔用 EDT
  2. 與更新元件相關的程式碼必須使用 EDT 執行
  3. 要更新元件

編寫高回應性 GUI 還需考慮更多問題,以上僅最基本部分。歡迎有興趣讀者來信討論。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved