用Swing編寫靈敏的圖形用戶界面
不靈敏的圖形用戶界面會降低應用程序的可用性。當以下現象出現的時候,我們通常說這個用戶界面反應不靈敏。
不響應事件的現象;
沒有更新的現象;
這些現象在很大程度上與事件的處理方法相關,而在編寫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。我們的事件響應方法通常都是由這一線程調用的,除非你自己編寫代碼來調用這些事件響應方法。在這里初學者經常犯的一個錯誤就是在事件響應方法中完成過多的與修改組件沒有直接聯系的代碼。其最有可能的效果就是導致組件反應緩慢。比如以下響應按鈕事件的代碼:
執行之后的效果就是按鈕似乎定住了一段時間,直到Done.出現之后才彈起來。原因就是Swing組件的更新和事件的響應都是在event-dispatching thread中完成的,而事件響應的時候,event-dispatching thread被事件響應方法占據,所以組件不會被更新。而直到事件響應方法退出時才有可能去更新Swing組件。
為了解決這個問題,有人也許會試圖通過調用repaint()方法來更新組件:
但是這一個方法沒有起到預期的作用,按鈕仍然定住一段時間,在察看了repaint()方法的源代碼之后就知道原因了。
repaint()方法實際上是在事件隊列里加了一個UPDATE的事件,而沒有直接去重畫組件,而且這一個事件只能等待當前的事件響應方法結束之后才能被分配。因此只有繞過分配機制直接調用paint方法才能達到目的。
這樣卻是實現了更新,但是還存在著以下的問題。雖然從感覺上,按鈕已經彈起來了,但是在Done.出現之前,我們卻無法按下這個按鈕。可以說按鈕還是定住了,只不過定在了彈起的狀態。調用重繪方法無法從根本上解決問題,因此我們需要尋求其他的方法。
不響應事件的現象;
沒有更新的現象;
這些現象在很大程度上與事件的處理方法相關,而在編寫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.出現之前,我們卻無法按下這個按鈕。可以說按鈕還是定住了,只不過定在了彈起的狀態。調用重繪方法無法從根本上解決問題,因此我們需要尋求其他的方法。