使用SwingWorker線程模式
謹慎地使用并發機制對Swing開發人員來說非常重要。一個好的Swing程序使用并發機制來創建不會失去響應的用戶接口-不管是什么樣的用戶交互,程序總能夠對其給出響應。創建一個有響應的程序,開發人員必須學會如何在Swing框架中使用多線程。
一個Swing開發人員將會與下面幾類線程打交道:
(1)Initial threads(初始線程),此類線程將執行初始化應用代碼。
(2)The event dispatch thread(事件派發線程),所有的事件處理代碼在這里執行。大多數與Swing框架交互的代碼也必須執行這個線程。
(3)Worker threads(工作線程),也稱作background threads(后臺線程),此類線程將執行所有消耗時間的任務。
開發人員不需要在代碼中顯式的創建這些線程:它們是由runtime或Swing框架提供的。開發人員的工作就是利用這些線程來創建具有響應的,持久的Swing程序。
如同所有其他在Java平臺上運行的程序,一個Swing程序可以創建額外的線程和線程池,這需要使用本文即將介紹的方法。本文將介紹以上這三種線程。工作線程的討論將涉及到使用javax.swing.SwingWorker類。這個類有許多有用的特性,包括在工作線程任務與其他線程任務之間的通信與協作。
1.初始線程
每個程序都會在應用邏輯開始時生成一系列的線程。在標準的程序中,只有一個這樣的線程:這個線程將調用程序主類中的main方法。在applet中初始線程是applet對象的構造子,它將調用init方法;這些actions可能在一個單一的線程中執行,或在兩個或三個不同的線程中,這些都依據Java平臺的具體實現。在本文中,我們稱這類線程為初始線程(initial threads)。
在Swing程序中,初始線程沒有很多事情要做。它們最基本的任務是創建一個Runnable對象,用于初始化GUI以及為那些用于執行事件派發線程中的事件的對象編排順序。一旦GUI被創建,程序將主要由GUI事件驅動,其中的每個事件驅動將引起一個在事件派發線程中事件的執行。程序代碼可以編排額外的任務給事件驅動線程(前提是它們會被很快的執行,這樣才不會干擾事件的處理)或創建工作線程(用于執行消耗時間的任務)。
一個初始線程編排GUI創建任務是通過調用javax.swing.SwingUtilities.invokeLater或javax.swing.SwingUtilities.invokeAndWait。這兩個方法都帶有一個唯一的參數:Runnable用于定義新的任務。它們唯一的區別是:invokerLater僅僅編排任務并返回;invokeAndWait將等待任務執行完畢才返回。
看下面示例:
1
2
3
4
5
|
SwingUtilities.invokeLater( new Runnable()) { public void run() { createAndShowGUI(); } } |
在applet中,創建GUI的任務必須被放入init方法中并且使用invokeAndWait;否則,初始過程將有可能在GUI創建完之前完成,這樣將有可能出現問題。在其他的情況下,編排GUI創建任務通常是初始線程中最后一個被執行的,所以使用invokeLater或invokeAndWait都可以。
為什么初始線程不直接創建GUI?因為幾乎所有的用于創建和交互Swing組件的代碼必須在事件派發線程中執行。這個約束將在下文中討論。
2.事件派發線程
Swing事件的處理代碼在一個特殊的線程中執行,這個線程被稱為事件派發線程。大部分調用Swing方法的代碼都在這個線程中被執行。這樣做是必要的,因為大部分Swing對象是“非線程安全的”。
可以將代碼的執行想象成在事件派發線程中執行一系列短小的任務。大部分任務被事件處理方法調用,諸如ActionListener.actionPerformed。其余的任務將被程序代碼編排,使用invokeLater或invokeAndWait。在事件派發線程中的任務必須能夠被快速執行完成,如若不然,未經處理的事件被積壓,用戶界面將變得“響應遲鈍”。
如果你需要確定你的代碼是否是在事件派發線程中執行,可調用javax.swing.SwingUtilities.isEventDispatchThread。
3.工作線程與SwingWorker
當一個Swing程序需要執行一個長時間的任務,通常將使用一個工作線程來完成。每個任務在一個工作線程中執行,它是一個javax.swing.SwingWorker類的實例。SwingWorker類是抽象類;你必須定義它的子類來創建一個SwingWorker對象;通常使用匿名內部類來這做這些。
SwingWorker提供一些通信與控制的特征:
(1)SwingWorker的子類可以定義一個方法,done。當后臺任務完成的時候,它將自動的被事件派發線程調用。
(2)SwingWorker類實現java.util.concurrent.Future。這個接口允許后臺任務提供一個返回值給其他線程。該接口中的方法還提供允許撤銷后臺任務以及確定后臺任務是被完成了還是被撤銷的功能。
(3)后臺任務可以通過調用SwingWorker.publish來提供中間結果,事件派發線程將會調用該方法。
(4)后臺任務可以定義綁定屬性。綁定屬性的變化將觸發事件,事件派發線程將調用事件處理程序來處理這些被觸發的事件。
4.簡單的后臺任務
下面介紹一個示例,這個任務非常簡單,但它是潛在地消耗時間的任務。TumbleItem applet導入一系列的圖片文件。如果這些圖片文件是通過初始線程導入的,那么將在GUI出現之前有一段延遲。如果這些圖片文件是在事件派發線程中導入的,那么GUI將有可能出現臨時無法響應的情況。
為了解決這些問題,TumbleItem類在它初始化時創建并執行了一個StringWorker類的實例。這個對象的doInBackground方法,在一個工作線程中執行,將圖片導入一個ImageIcon數組,并且返回它的一個引用。接著done方法,在事件派發線程中執行,得到返回的引用,將其放在applet類的成員變量imgs中。這樣做可以允許TumbleItem類立刻創建GUI,而不必等待圖片導入完成。
下面的示例代碼定義和實現了一個SwingWorker對象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
SwingWorker worker = new SwingWorker<ImageIcon[], Void>() { @Override public ImageIcon[] doInBackground() { final ImageIcon[] innerImgs = new ImageIcon[nimgs]; for ( int i = 0 ; i < nimgs; i++) { innerImgs[i] = loadImage(i+ 1 ); } return innerImgs; } @Override public void done() { //Remove the "Loading images" label. animator.removeAll(); loopslot = - 1 ; try { imgs = get(); } catch (InterruptedException ignore) {} catch (java.util.concurrent.ExecutionException e) { String why = null ; Throwable cause = e.getCause(); if (cause != null ) { why = cause.getMessage(); } else { why = e.getMessage(); } System.err.println( "Error retrieving file: " + why); } } }; |
所有的繼承自SwingWorker的子類都必須實現doInBackground;實現done方法是可選的。
注意,SwingWorker是一個范型類,有兩個參數。第一個類型參數指定doInBackground的返回類型。同時也是get方法的類型,它可以被其他線程調用以獲得來自于doInBackground的返回值。第二個類型參數指定中間結果的類型,這個例子沒有返回中間結果,所以設為void。
使用get方法,可以使對象imgs的引用(在工作線程中創建)在事件派發線程中得到使用。這樣就可以在線程之間共享對象。
實際上有兩個方法來得到doInBackground類返回的對象。
(1)調用SwingWorker.get沒有參數。如果后臺任務沒有完成,get方法將阻塞直到它完成。
(2)調用SwingWorker.get帶參數指定timeout。如果后臺任務沒有完成,阻塞直到它完成-除非timeout期滿,在這種情況下,get將拋出java.util.concurrent.TimeoutException。
5.具有中間結果的任務
讓一個正在工作的后臺任務提供中間結果是很有用處的。后臺任務可以調用SwingWorker.publish方法來做到這個。這個方法接受許多參數。每個參數必須是由SwingWorker的第二個類型參數指定的一種。
可以覆蓋(override)SwingWorker.process來保存由publish方法提供的結果。這個方法是由事件派發線程調用的。來自publish方法的結果集通常是由一個process方法收集的。
我們看一下Filpper.java提供的實例。這個程序通過一個后臺任務產生一系列的隨機布爾值測試java.util.Random。就好比是一個投硬幣試驗。為了報告它的結果,后臺任務使用了一個對象FlipPair。
1
2
3
4
5
6
7
|
private static class FlipPair { private final long heads, total; FlipPair( long heads, long total) { this .heads = heads; this .total = total; } } |
heads表示true的結果;total表示總的投擲次數。
后臺程序是一個FilpTask的實例:
private class FlipTask extends SwingWorker<Void, FlipPair> {
因為任務沒有返回一個最終結果,這里不需要指定第一個類型參數是什么,使用Void。在每次“投擲”后任務調用publish:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
@Override protected Void doInBackground() { long heads = 0 ; long total = 0 ; Random random = new Random(); while (!isCancelled()) { total++; if (random.nextBoolean()) { heads++; } publish( new FlipPair(heads, total)); } return null ; } |
由于publish時常被調用,許多的FlipPair值將在process方法被事件派發線程調用之前被收集;process僅僅關注每次返回的最后一組值,使用它來更新GUI:
1
2
3
4
5
6
7
|
protected void process(List pairs) { FlipPair pair = pairs.get(pairs.size() - 1 ); headsText.setText(String.format( "%d" , pair.heads)); totalText.setText(String.format( "%d" , pair.total)); devText.setText(String.format( "%.10g" , (( double ) pair.heads)/(( double ) pair.total) - 0.5 )); } |
6.取消后臺任務
調用SwingWorker.cancel來取消一個正在執行的后臺任務。任務必須與它自己的撤銷機制一致。有兩個方法來做到這一點:
(1)當收到一個interrupt時,將被終止。
(2)調用SwingWorker.isCanceled,如果SwingWorker調用cancel,該方法將返回true。
7.綁定屬性和狀態方法
SwingWorker支持bound properties,這個在與其他線程通信時很有作用。提供兩個綁定屬性:progress和state。progress和state可以用于觸發在事件派發線程中的事件處理任務。
通過實現一個property change listener,程序可以捕捉到progress,state或其他綁定屬性的變化。
7.1The progress Bound Variable
Progress綁定變量是一個整型變量,變化范圍由0到100。它預定義了setter (the protected SwingWorker.setProgress)和getter (the public SwingWorker.getProgress)方法。
7.2The state Bound Variable
State綁定變量的變化反映了SwingWorker對象在它的生命周期中的變化過程。該變量中包含一個SwingWorker.StateValue的枚舉類型。可能的值有:
(1)PENDING
這個狀態持續的時間為從對象的建立知道doInBackground方法被調用。
(2)STARTED
這個狀態持續的時間為doInBackground方法被調用前一刻直到done方法被調用前一刻。
(3)DONE
對象存在的剩余時間將保持這個狀態。
需要返回當前state的值可調用SwingWorker.getState。
7.3Status Methods
兩個由Future接口提供的方法,同樣可以報告后臺任務的狀態。如果任務被取消,isCancelled返回true。此外,如果任務完成,即要么正常的完成,要么被取消,isDone返回true。
使用頂層容器
Swing提供3種頂層容器類:JFrame,JDialog,JApplet。當使用這三個類時,你必須注意以下幾點:
(1).為了顯示在屏幕上,每個GUI組件必須是包含層次(containment hierarchy)的一部分。包含層次是組件的一個樹型結構,最頂層的容器是它的根。
(2).每個GUI組件只能被包含一次。如果一個組件已經在一個容器中,這時試圖將它加入到一個新的容器,則這個組件會從第一個容器移除,并加入到第二個容器中。
(3).每個頂層容器都有一個內容面板(content pane),一般情況下,這個內容面板會包含(直接或間接地)所有頂層容器GUI的可視組件。
(4).可以在頂層容器中加入一個菜單條(menu bar)。通常這個菜單條被放置在頂層容器中,但在內容面板外。
1.頂層容器與包含層次
每個使用Swing組件的程序都至少有一個頂層容器。這個頂層容器是包含層次的根節點—這個層次會包含所有將在這個頂層容器中出現的Swing組件。
通常情況下,一個單獨的基于Swing GUI的應用程序至少有一個包含層次,且它的根節點是JFrame。舉例來說,如果一個應用程序擁有一個窗口和兩個對話框,那么這個應用程序將會有三個包含層次,也即會有三個頂層容器。一個包含層次將JFrame作為它的根節點,兩外兩個包含層次各有一個JDialog作為它的根節點。
一個基于Swing組件的小程序(applet)至少含有一個包含層次,并且可以確定其中必有一個是以JApplet作為其根節點的。例如,一個小程序帶有一個對話框,則它會有兩個包含層次。在瀏覽器窗口中的組件將會置于一個包含層次,它的根節點是一個JApplet對象。對話框會有一個包含層次,它的根節點是一個JDialog對象。
2.將組件加入到內容面板中
下面的代碼操作是上面的例子中得到frame的內容面板并加入黃色標簽:
frame.getContentPane().add(yellowLabel, BorderLayout.CENTER);
如代碼所示,必須先找到頂層容器的內容面板,通過方法getContentPane實現。默認的內容面板是一個簡單的中間容器,它繼承自JComponent,使用一個BorderLayout作為它的面板管理器。
定制一個內容面板很簡單—設置面板管理器或添加邊框。這里必須注意,getContentPane方法將返回一個Container對象,而不是JComponent對象。這意味著如果需要利用JComponent的部分功能,還必須將返回值進行類型轉換或創建你自己的組件來作為內容面板。我們的實例通常采用的是第二種方式. 因為第二種方法比較清楚明朗。 另一種我們有時會使用的方法就是簡單地將一個自己定義組件添加進內容面板, 完全遮蓋住內容面板。
如果你創建你自己的內容面板, 那么請注意確認它是不透明的. 一個不透明的JPanel將是一個不錯的選擇. 注意, 默認情況下JPanel的布局管理為FlowLayout, 你或許會想要用其它的布局管理器替換它。
為了使一個組件成為內容面板, 你需要使用頂層容器的setContentPane方法, 例如:
1
2
3
4
5
6
7
8
|
//Create a panel and add components to it. JPanel contentPane = new JPanel( new BorderLayout()); contentPane.setBorder(someBorder); contentPane.add(someComponent, BorderLayout.CENTER); contentPane.add(anotherComponent, BorderLayout.PAGE_END); //Make it the content pane. //contentPane.setOpaque(true); topLevelContainer.setContentPane(contentPane); |
注意: 不要使用透明的容器作為內容面板, 如JScrollPane, JSplitPane和JTabbedPane. . 一個透明的內容面板將導致組件混亂. 盡管你可以使任何的透明的Swing組件通過setOpaque(true)方法來使其不透明化, 但當一些組件被設置成完全不透明后看上去會不太對勁. 例如, 一個標簽面板.
3.添加一個菜單欄 (Adding a Menu Bar)
從理論上來講每一個頂層容器都可以有一個菜單欄. 但事實表明菜單欄僅出現于Frame或者Applet中. 為達到添加一個菜單欄到頂層容器, 你需要創建一個JMenuBar對象, 組裝上一些菜單, 然后呼叫setJMenuBar方法. TopLevelDemo實例通過以下代碼添加一個菜單欄到它的Frame中.
1
|
frame.setJMenuBar(cyanMenuBar); |
4.根容器 (The Root Pane)
每個頂層容器都依賴于一個隱式的稱為根容器的中間容器. 這個根容器管理著內容面板和菜單欄, 并且連同兩個或者兩個以上的其它容器(見圖中Layered Pane等). 你通常不需要了解關于使用Swing組件根容器方面的知識. 然而, 如果你想截獲鼠標的點擊或者在多重組件上進行繪畫動作, 那么你需要知曉根容器.
上文已經講述了關于內容面板與可選的菜單欄的內容,此處不再復述. 根容器中包含的另外兩個組件, 是布局面板和玻璃面板. 布局面板直接包含菜單欄和內容面板, 并且允許你對所添加的其它組件進行Z坐標排序. 玻璃面板通常用來截獲發生在頂層中的輸入動作, 并且同樣可以用來在多重組件上進行繪畫.