Java的Swing編程中使用SwingWorker線程模式及頂層容器
使用SwingWorker線程模式
謹慎地使用并發(fā)機制對Swing開發(fā)人員來說非常重要。一個好的Swing程序使用并發(fā)機制來創(chuàng)建不會失去響應的用戶接口-不管是什么樣的用戶交互,程序總能夠?qū)ζ浣o出響應。創(chuàng)建一個有響應的程序,開發(fā)人員必須學會如何在Swing框架中使用多線程。
一個Swing開發(fā)人員將會與下面幾類線程打交道:
(1)Initial threads(初始線程),此類線程將執(zhí)行初始化應用代碼。
(2)The event dispatch thread(事件派發(fā)線程),所有的事件處理代碼在這里執(zhí)行。大多數(shù)與Swing框架交互的代碼也必須執(zhí)行這個線程。
(3)Worker threads(工作線程),也稱作background threads(后臺線程),此類線程將執(zhí)行所有消耗時間的任務。
開發(fā)人員不需要在代碼中顯式的創(chuàng)建這些線程:它們是由runtime或Swing框架提供的。開發(fā)人員的工作就是利用這些線程來創(chuàng)建具有響應的,持久的Swing程序。
如同所有其他在Java平臺上運行的程序,一個Swing程序可以創(chuàng)建額外的線程和線程池,這需要使用本文即將介紹的方法。本文將介紹以上這三種線程。工作線程的討論將涉及到使用javax.swing.SwingWorker類。這個類有許多有用的特性,包括在工作線程任務與其他線程任務之間的通信與協(xié)作。
1.初始線程
每個程序都會在應用邏輯開始時生成一系列的線程。在標準的程序中,只有一個這樣的線程:這個線程將調(diào)用程序主類中的main方法。在applet中初始線程是applet對象的構(gòu)造子,它將調(diào)用init方法;這些actions可能在一個單一的線程中執(zhí)行,或在兩個或三個不同的線程中,這些都依據(jù)Java平臺的具體實現(xiàn)。在本文中,我們稱這類線程為初始線程(initial threads)。
在Swing程序中,初始線程沒有很多事情要做。它們最基本的任務是創(chuàng)建一個Runnable對象,用于初始化GUI以及為那些用于執(zhí)行事件派發(fā)線程中的事件的對象編排順序。一旦GUI被創(chuàng)建,程序?qū)⒅饕蒅UI事件驅(qū)動,其中的每個事件驅(qū)動將引起一個在事件派發(fā)線程中事件的執(zhí)行。程序代碼可以編排額外的任務給事件驅(qū)動線程(前提是它們會被很快的執(zhí)行,這樣才不會干擾事件的處理)或創(chuàng)建工作線程(用于執(zhí)行消耗時間的任務)。
一個初始線程編排GUI創(chuàng)建任務是通過調(diào)用javax.swing.SwingUtilities.invokeLater或javax.swing.SwingUtilities.invokeAndWait。這兩個方法都帶有一個唯一的參數(shù):Runnable用于定義新的任務。它們唯一的區(qū)別是:invokerLater僅僅編排任務并返回;invokeAndWait將等待任務執(zhí)行完畢才返回。
看下面示例:
SwingUtilities.invokeLater(new Runnable()) { public void run() { createAndShowGUI(); } }
在applet中,創(chuàng)建GUI的任務必須被放入init方法中并且使用invokeAndWait;否則,初始過程將有可能在GUI創(chuàng)建完之前完成,這樣將有可能出現(xiàn)問題。在其他的情況下,編排GUI創(chuàng)建任務通常是初始線程中最后一個被執(zhí)行的,所以使用invokeLater或invokeAndWait都可以。
為什么初始線程不直接創(chuàng)建GUI?因為幾乎所有的用于創(chuàng)建和交互Swing組件的代碼必須在事件派發(fā)線程中執(zhí)行。這個約束將在下文中討論。
2.事件派發(fā)線程
Swing事件的處理代碼在一個特殊的線程中執(zhí)行,這個線程被稱為事件派發(fā)線程。大部分調(diào)用Swing方法的代碼都在這個線程中被執(zhí)行。這樣做是必要的,因為大部分Swing對象是“非線程安全的”。
可以將代碼的執(zhí)行想象成在事件派發(fā)線程中執(zhí)行一系列短小的任務。大部分任務被事件處理方法調(diào)用,諸如ActionListener.actionPerformed。其余的任務將被程序代碼編排,使用invokeLater或invokeAndWait。在事件派發(fā)線程中的任務必須能夠被快速執(zhí)行完成,如若不然,未經(jīng)處理的事件被積壓,用戶界面將變得“響應遲鈍”。
如果你需要確定你的代碼是否是在事件派發(fā)線程中執(zhí)行,可調(diào)用javax.swing.SwingUtilities.isEventDispatchThread。
3.工作線程與SwingWorker
當一個Swing程序需要執(zhí)行一個長時間的任務,通常將使用一個工作線程來完成。每個任務在一個工作線程中執(zhí)行,它是一個javax.swing.SwingWorker類的實例。SwingWorker類是抽象類;你必須定義它的子類來創(chuàng)建一個SwingWorker對象;通常使用匿名內(nèi)部類來這做這些。
SwingWorker提供一些通信與控制的特征:
(1)SwingWorker的子類可以定義一個方法,done。當后臺任務完成的時候,它將自動的被事件派發(fā)線程調(diào)用。
(2)SwingWorker類實現(xiàn)java.util.concurrent.Future。這個接口允許后臺任務提供一個返回值給其他線程。該接口中的方法還提供允許撤銷后臺任務以及確定后臺任務是被完成了還是被撤銷的功能。
(3)后臺任務可以通過調(diào)用SwingWorker.publish來提供中間結(jié)果,事件派發(fā)線程將會調(diào)用該方法。
(4)后臺任務可以定義綁定屬性。綁定屬性的變化將觸發(fā)事件,事件派發(fā)線程將調(diào)用事件處理程序來處理這些被觸發(fā)的事件。
4.簡單的后臺任務
下面介紹一個示例,這個任務非常簡單,但它是潛在地消耗時間的任務。TumbleItem applet導入一系列的圖片文件。如果這些圖片文件是通過初始線程導入的,那么將在GUI出現(xiàn)之前有一段延遲。如果這些圖片文件是在事件派發(fā)線程中導入的,那么GUI將有可能出現(xiàn)臨時無法響應的情況。
為了解決這些問題,TumbleItem類在它初始化時創(chuàng)建并執(zhí)行了一個StringWorker類的實例。這個對象的doInBackground方法,在一個工作線程中執(zhí)行,將圖片導入一個ImageIcon數(shù)組,并且返回它的一個引用。接著done方法,在事件派發(fā)線程中執(zhí)行,得到返回的引用,將其放在applet類的成員變量imgs中。這樣做可以允許TumbleItem類立刻創(chuàng)建GUI,而不必等待圖片導入完成。
下面的示例代碼定義和實現(xiàn)了一個SwingWorker對象。
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的子類都必須實現(xiàn)doInBackground;實現(xiàn)done方法是可選的。
注意,SwingWorker是一個范型類,有兩個參數(shù)。第一個類型參數(shù)指定doInBackground的返回類型。同時也是get方法的類型,它可以被其他線程調(diào)用以獲得來自于doInBackground的返回值。第二個類型參數(shù)指定中間結(jié)果的類型,這個例子沒有返回中間結(jié)果,所以設(shè)為void。
使用get方法,可以使對象imgs的引用(在工作線程中創(chuàng)建)在事件派發(fā)線程中得到使用。這樣就可以在線程之間共享對象。
實際上有兩個方法來得到doInBackground類返回的對象。
(1)調(diào)用SwingWorker.get沒有參數(shù)。如果后臺任務沒有完成,get方法將阻塞直到它完成。
(2)調(diào)用SwingWorker.get帶參數(shù)指定timeout。如果后臺任務沒有完成,阻塞直到它完成-除非timeout期滿,在這種情況下,get將拋出java.util.concurrent.TimeoutException。
5.具有中間結(jié)果的任務
讓一個正在工作的后臺任務提供中間結(jié)果是很有用處的。后臺任務可以調(diào)用SwingWorker.publish方法來做到這個。這個方法接受許多參數(shù)。每個參數(shù)必須是由SwingWorker的第二個類型參數(shù)指定的一種。
可以覆蓋(override)SwingWorker.process來保存由publish方法提供的結(jié)果。這個方法是由事件派發(fā)線程調(diào)用的。來自publish方法的結(jié)果集通常是由一個process方法收集的。
我們看一下Filpper.java提供的實例。這個程序通過一個后臺任務產(chǎn)生一系列的隨機布爾值測試java.util.Random。就好比是一個投硬幣試驗。為了報告它的結(jié)果,后臺任務使用了一個對象FlipPair。
private static class FlipPair { private final long heads, total; FlipPair(long heads, long total) { this.heads = heads; this.total = total; } }
heads表示true的結(jié)果;total表示總的投擲次數(shù)。
后臺程序是一個FilpTask的實例:
private class FlipTask extends SwingWorker<Void, FlipPair> {
因為任務沒有返回一個最終結(jié)果,這里不需要指定第一個類型參數(shù)是什么,使用Void。在每次“投擲”后任務調(diào)用publish:
@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時常被調(diào)用,許多的FlipPair值將在process方法被事件派發(fā)線程調(diào)用之前被收集;process僅僅關(guān)注每次返回的最后一組值,使用它來更新GUI:
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.取消后臺任務
調(diào)用SwingWorker.cancel來取消一個正在執(zhí)行的后臺任務。任務必須與它自己的撤銷機制一致。有兩個方法來做到這一點:
(1)當收到一個interrupt時,將被終止。
(2)調(diào)用SwingWorker.isCanceled,如果SwingWorker調(diào)用cancel,該方法將返回true。
7.綁定屬性和狀態(tài)方法
SwingWorker支持bound properties,這個在與其他線程通信時很有作用。提供兩個綁定屬性:progress和state。progress和state可以用于觸發(fā)在事件派發(fā)線程中的事件處理任務。
通過實現(xiàn)一個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的枚舉類型??赡艿闹涤校?br />
(1)PENDING
這個狀態(tài)持續(xù)的時間為從對象的建立知道doInBackground方法被調(diào)用。
(2)STARTED
這個狀態(tài)持續(xù)的時間為doInBackground方法被調(diào)用前一刻直到done方法被調(diào)用前一刻。
(3)DONE
對象存在的剩余時間將保持這個狀態(tài)。
需要返回當前state的值可調(diào)用SwingWorker.getState。
7.3Status Methods
兩個由Future接口提供的方法,同樣可以報告后臺任務的狀態(tài)。如果任務被取消,isCancelled返回true。此外,如果任務完成,即要么正常的完成,要么被取消,isDone返回true。
使用頂層容器
Swing提供3種頂層容器類:JFrame,JDialog,JApplet。當使用這三個類時,你必須注意以下幾點:
(1).為了顯示在屏幕上,每個GUI組件必須是包含層次(containment hierarchy)的一部分。包含層次是組件的一個樹型結(jié)構(gòu),最頂層的容器是它的根。
(2).每個GUI組件只能被包含一次。如果一個組件已經(jīng)在一個容器中,這時試圖將它加入到一個新的容器,則這個組件會從第一個容器移除,并加入到第二個容器中。
(3).每個頂層容器都有一個內(nèi)容面板(content pane),一般情況下,這個內(nèi)容面板會包含(直接或間接地)所有頂層容器GUI的可視組件。
(4).可以在頂層容器中加入一個菜單條(menu bar)。通常這個菜單條被放置在頂層容器中,但在內(nèi)容面板外。
1.頂層容器與包含層次
每個使用Swing組件的程序都至少有一個頂層容器。這個頂層容器是包含層次的根節(jié)點—這個層次會包含所有將在這個頂層容器中出現(xiàn)的Swing組件。
通常情況下,一個單獨的基于Swing GUI的應用程序至少有一個包含層次,且它的根節(jié)點是JFrame。舉例來說,如果一個應用程序擁有一個窗口和兩個對話框,那么這個應用程序?qū)腥齻€包含層次,也即會有三個頂層容器。一個包含層次將JFrame作為它的根節(jié)點,兩外兩個包含層次各有一個JDialog作為它的根節(jié)點。
一個基于Swing組件的小程序(applet)至少含有一個包含層次,并且可以確定其中必有一個是以JApplet作為其根節(jié)點的。例如,一個小程序帶有一個對話框,則它會有兩個包含層次。在瀏覽器窗口中的組件將會置于一個包含層次,它的根節(jié)點是一個JApplet對象。對話框會有一個包含層次,它的根節(jié)點是一個JDialog對象。
2.將組件加入到內(nèi)容面板中
下面的代碼操作是上面的例子中得到frame的內(nèi)容面板并加入黃色標簽:
frame.getContentPane().add(yellowLabel, BorderLayout.CENTER);
如代碼所示,必須先找到頂層容器的內(nèi)容面板,通過方法getContentPane實現(xiàn)。默認的內(nèi)容面板是一個簡單的中間容器,它繼承自JComponent,使用一個BorderLayout作為它的面板管理器。
定制一個內(nèi)容面板很簡單—設(shè)置面板管理器或添加邊框。這里必須注意,getContentPane方法將返回一個Container對象,而不是JComponent對象。這意味著如果需要利用JComponent的部分功能,還必須將返回值進行類型轉(zhuǎn)換或創(chuàng)建你自己的組件來作為內(nèi)容面板。我們的實例通常采用的是第二種方式. 因為第二種方法比較清楚明朗。 另一種我們有時會使用的方法就是簡單地將一個自己定義組件添加進內(nèi)容面板, 完全遮蓋住內(nèi)容面板。
如果你創(chuàng)建你自己的內(nèi)容面板, 那么請注意確認它是不透明的. 一個不透明的JPanel將是一個不錯的選擇. 注意, 默認情況下JPanel的布局管理為FlowLayout, 你或許會想要用其它的布局管理器替換它。
為了使一個組件成為內(nèi)容面板, 你需要使用頂層容器的setContentPane方法, 例如:
//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);
注意: 不要使用透明的容器作為內(nèi)容面板, 如JScrollPane, JSplitPane和JTabbedPane. . 一個透明的內(nèi)容面板將導致組件混亂. 盡管你可以使任何的透明的Swing組件通過setOpaque(true)方法來使其不透明化, 但當一些組件被設(shè)置成完全不透明后看上去會不太對勁. 例如, 一個標簽面板.
3.添加一個菜單欄 (Adding a Menu Bar)
從理論上來講每一個頂層容器都可以有一個菜單欄. 但事實表明菜單欄僅出現(xiàn)于Frame或者Applet中. 為達到添加一個菜單欄到頂層容器, 你需要創(chuàng)建一個JMenuBar對象, 組裝上一些菜單, 然后呼叫setJMenuBar方法. TopLevelDemo實例通過以下代碼添加一個菜單欄到它的Frame中.
frame.setJMenuBar(cyanMenuBar);
4.根容器 (The Root Pane)
每個頂層容器都依賴于一個隱式的稱為根容器的中間容器. 這個根容器管理著內(nèi)容面板和菜單欄, 并且連同兩個或者兩個以上的其它容器(見圖中Layered Pane等). 你通常不需要了解關(guān)于使用Swing組件根容器方面的知識. 然而, 如果你想截獲鼠標的點擊或者在多重組件上進行繪畫動作, 那么你需要知曉根容器.
上文已經(jīng)講述了關(guān)于內(nèi)容面板與可選的菜單欄的內(nèi)容,此處不再復述. 根容器中包含的另外兩個組件, 是布局面板和玻璃面板. 布局面板直接包含菜單欄和內(nèi)容面板, 并且允許你對所添加的其它組件進行Z坐標排序. 玻璃面板通常用來截獲發(fā)生在頂層中的輸入動作, 并且同樣可以用來在多重組件上進行繪畫.
相關(guān)文章
idea2019.1.4 鼠標放到方法上顯示注解的實現(xiàn)操作
這篇文章主要介紹了idea2019.1.4 鼠標放到方法上顯示注解的實現(xiàn)操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-02-02SpringBoot使用classfinal-maven-plugin插件加密Jar包的示例代碼
這篇文章給大家介紹了SpringBoot使用classfinal-maven-plugin插件加密Jar包的實例,文中通過代碼示例和圖文講解的非常詳細,對大家的學習或工作有一定的幫助,需要的朋友可以參考下2024-02-02java輸出1~100之間的全部素數(shù)的5種方式總結(jié)
這篇文章主要介紹了java輸出1~100之間的全部素數(shù)的5種方式總結(jié),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-02-02SpringCloud超詳細講解負載均衡組件Ribbon源碼
在微服務中,對服務進行拆分之后,必然會帶來微服務之間的通信需求,而每個微服務為了保證高可用性,又會去部署集群,那么面對一個集群微服務進行通信的時候,如何進行負載均衡也是必然需要考慮的問題2022-07-07舉例講解Java的Jackson庫中ObjectMapper類的使用
這篇文章主要介紹了舉例講解Java的Jackson庫中ObjectMapper類的使用,Jackson庫通常被用來實現(xiàn)Java的對象和JSON之間的轉(zhuǎn)換功能,需要的朋友可以參考下2016-01-01Java Arrays.sort()如何實現(xiàn)對int類型數(shù)組倒序排序
這篇文章主要介紹了Java Arrays.sort()如何實現(xiàn)對int類型數(shù)組倒序排序問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-08-08