不靈敏的圖形化使用者介面會降低應用程式的可用性。當以下現象出現的時候,我們通常說這個使用者介面反應不靈敏:
不響應事件的現象;
沒有更新的現象;
這些現象在很大程度上與事件的處理方法相關,而在編寫Swing應用程式的時候,我們幾乎必然要編寫方法去響應滑鼠點擊按鈕,鍵盤迴車等事件。在這些方法中我們要編寫一些代碼,在運行時去觸發一些動作。常見動作包括尋找,更新資料庫等。在這篇文章中通過對一個執行個體的分析,介紹了一些基本概念,常見的錯誤以及提出了一個解決方案。
event-dispatching thread
我們一定要記住,事件回應程式法的代碼都是在event-dispatching thread中執行的,除非你啟用另一個線程。
那麼,什麼是event-dispatching thread呢?單一線程規則:一旦一個Swing組件被實現(realized),所有的有可能影響或依賴於這個組件的狀態的代碼都應該在event-dispatching thread中被執行。而實現一個組件有兩種方式:
對頂層組件調用show(), pack(), 或者setVisible(true);
將一個組件加到一個已經被實現的容器中。
單一線程規則的根源是由於Swing組件庫的大部分方法是對多線程不安全的。
為了支援單一執行緒模式,Swing組件庫提供了一個專門來完成這些與Swing組件相關的操作的線程,而這一線程就是event-dispatching thread。我們的事件回應程式法通常都是由這一線程調用的,除非你自己編寫代碼來調用這些事件回應程式法。在這裡初學者經常犯的一個錯誤就是在事件回應程式法中完成過多的與修改組件沒有直接聯絡的代碼。其最有可能的效果就是導致組件反應緩慢。比如以下響應按鈕事件的代碼:
String str = null; this.textArea.setText("Please wait..."); try { //do something that is really time consuming str = "Hello, world!"; Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } this.textArea.setText(str); |
執行之後的效果就是按鈕似乎定住了一段時間,直到Done.出現之後才彈起來。原因就是Swing組件的更新和事件的響應都是在event-dispatching thread中完成的,而事件響應的時候,event-dispatching thread被事件回應程式法佔據,所以組件不會被更新。而直到事件回應程式法退出時才有可能去更新Swing組件。
為瞭解決這個問題,有人也許會試圖通過調用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方法才能達到目的。
final String[] str = new String[1]; 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(); |
在這個程式中,要花費大量時間的操作被放到另一個線程當中,從而使事件回應程式法能快速返回,event-dispatching thread就可以更新UI和響應其它事件了。注意到這個程式使用了invokeLater()方法。invokeLater()方法的作用是讓event-dispatching thread去運行制定的代碼。當然也可以不使用invokeLater()方法,但是這樣就違背了單一線程原則,同時帶來了一定程度的相對多線程的不安全性。到現在,解決方案似乎是完美的了,但是我們看一看在原來的程式添加下面的代碼,儘管我們通常不這樣做。
public void paint(java.awt.Graphics g) { super.paint(g); g.drawRect(1, 1, 100, 100); } |
我們會發現以前畫的矩形被覆蓋了一部分,原因是由於我們沒用重畫這一個矩形,因此在結尾加上對repaint()方法的調用。
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]); repaint(); } }); } }.start(); |
如果你認為這段代碼過於缺乏可讀性,可以通過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、不要過多地佔用event-dispatching thread;
2、與更新群組件相關的代碼要使用event-dispatching thread去執行;
3、要更新群組件。
編寫反應靈敏的圖形化使用者介面還需要考慮很多問題,以上只是最基本的一部分。歡迎有興趣的讀者來信進行討論。