今天從Swing的四大基本對(duì)象Component說起。
====================================
Component在Swing的MVC模型中擔(dān)任Controller的角色,同時(shí)它也是Swing API中代表具體組件的對(duì)象。Component在Swing中對(duì)外負(fù)責(zé)提供API接口,對(duì)內(nèi)負(fù)責(zé)協(xié)調(diào)控制Model和UI Delegate(有時(shí)可能還包括Renderer)的操作,可以說是整個(gè)Swing結(jié)構(gòu)的中心角色。為了方便你回憶Swing的MVC模型,特地將上一篇文章中的Swing模型示意圖引了過來:
Component代表Swing對(duì)應(yīng)用程序提供了如下幾類編程接口:
- 用戶界面的組件樹的創(chuàng)建和修改的方法。這包括組件的添加和刪除等操作。
- 組件屬性訪問的方法,比如組件位置、組件前后背景色、組件字體等等。
- 組件狀態(tài)及生命周期的管理的方法,比如隱藏和顯示、創(chuàng)建和銷毀等等。
- 組件位置、大小的管理,包括通過布局管理器的方法。
- 組件事件處理接口的管理,包括添加、刪除等操作。
從開發(fā)者的角度來看,Component是組件樹上的節(jié)點(diǎn)、是控制外觀和行為的入口、是組件事件的發(fā)源地。從Swing組件實(shí)現(xiàn)者的角度來看, Component是協(xié)調(diào)Model和UI Delegate的操作的地方,是低層次事件處理的地方,是高層事件發(fā)生的地方,是同父組件和子組件交互的地方。掌握的這些角度,Swing程序員完全可以實(shí)現(xiàn)自己的自定義簡(jiǎn)單組件,當(dāng)然如需要實(shí)現(xiàn)類似于JTable和JTree等復(fù)雜的矢量組件,還需要進(jìn)一步了解Swing的Model和UI Delegate以及Renderer模型。
對(duì)于復(fù)合型(Composite)組件很簡(jiǎn)單,這兒要講述的是如何實(shí)現(xiàn)自定義的組件,比如表盤,比如溫度計(jì)這些沒有標(biāo)準(zhǔn)組件可以使用的組件,那么如何自己實(shí)現(xiàn)這種自定義組件呢?
不考慮Model分隔和UI Delegate皮膚分離問題,能夠簡(jiǎn)化自定義Component的模型。總的來說自定義組件需要完成兩樣基本任務(wù):第一偵聽并處理低層事件,根據(jù)具體情況改變組件狀態(tài),如需要還要發(fā)出高級(jí)事件;第二,根據(jù)當(dāng)前組件的狀態(tài)畫出當(dāng)前組件的外觀。
偵聽底層的事件是指?jìng)陕狀愃朴趍ouse、keyboard、focus等事件,然后處理此事件,如果發(fā)現(xiàn)此事件帶有特定語義,表達(dá)某種組件行為,則改變當(dāng)前的組件狀態(tài)以記錄,并觸發(fā)某種事件通知應(yīng)用程序進(jìn)行處理。舉例說明,想象你準(zhǔn)備實(shí)現(xiàn)一個(gè)簡(jiǎn)單的按鈕,你可以通過繼承JComponent來完成。你可以在按鈕初始化時(shí),注冊(cè)此按鈕的鼠標(biāo)事件偵聽器,以偵聽發(fā)生自己組件上的鼠標(biāo)事件。當(dāng)按鈕捕獲到鼠標(biāo)按下時(shí),檢查鼠標(biāo)按下的點(diǎn)是否在按鈕有效區(qū)域內(nèi)。如果是,則認(rèn)為當(dāng)前是一個(gè)按鈕按下動(dòng)作,那么改變按鈕的狀態(tài)為按下去,調(diào)用repaint方法通知按鈕重畫成按下去的狀態(tài),然后發(fā)出 ActionPerformed的事件,通知注冊(cè)在此按鈕上的應(yīng)用程序的ActionListener處理這個(gè)動(dòng)作。下面是一個(gè)簡(jiǎn)單示意代碼:
public class MyButton extends Jcomponent implements MouseListener{private String text;
private boolean pressed=false;
private ArrayList<ActionListener> listeners=new ArrayList<ActionListener>();
public MyButton(){
addMouseListener(this);//將自己注冊(cè)為自己的鼠標(biāo)事件偵聽器,監(jiān)聽鼠標(biāo)事件
}
....
public void mousePressed(MouseEvent evt){
Point p=evt.getPoint();
if(getBounds().contains(p)){//判斷鼠標(biāo)落點(diǎn)是否在有效區(qū)域內(nèi)。
pressed=true; //鼠標(biāo)點(diǎn)擊的含義是按鈕被按下!改表按鈕狀態(tài)。
repaint(); //通知按鈕重畫,由原來的抬起狀態(tài)改變成按下狀態(tài)。
fireActionPerformed(new ActionEvent(this)); //這是一個(gè)按鈕動(dòng)作事件,觸發(fā)它。
}
}
public void addActionListener(ActionListener listener){
listeners.add(listener);
}
public void removeActionListener(ActionListener listener){
listeners.remove(listener);
}
protected fireActionPerformed(ActionEvent evt){
for(ActionListener listener:listeners){
listener.actionPerformed(evt);
}
}
...
//這兒你要覆蓋paint方法,實(shí)現(xiàn)按鈕狀態(tài)的重畫
public void paint(Graphics g){
if(pressed){
//畫出按下的樣子
}else{
//畫出抬起的樣子
}
}
}
上面要注意的是你要自己管理自定義組件的事件監(jiān)聽器,包括addListener和removeListener方法,以及如何觸發(fā)。這個(gè)過程很簡(jiǎn)單,基本上就是上面的模板來實(shí)現(xiàn)添加刪除和觸發(fā)。
除了要負(fù)責(zé)事件的處理和新事件的觸發(fā),自定義組件第二個(gè)要完成的任務(wù)就是要根據(jù)組件當(dāng)前的狀態(tài)改變重畫組件的外觀。重畫組件的外觀只需要覆蓋public void paint(Graphics g)方法即可以,在這個(gè)方法里,你只需要根據(jù)當(dāng)前的組件狀態(tài)分別畫出當(dāng)前的組件即可。
當(dāng)然除了上面兩個(gè)基本準(zhǔn)則外,不要忘了添加訪問你的組件屬性的方法,比如,如果上面的按鈕是個(gè)二元按鈕(相當(dāng)于 JCheckbox/JToggleButton的那種按鈕),你可能需要提供isPressed或者setPressed來獲取和設(shè)置當(dāng)前按鈕的狀態(tài)。注意,在設(shè)置狀態(tài)按鈕變化的訪問方法中,比如setPressed,你需要使用repaint方法通知按鈕重新渲染(復(fù)雜的實(shí)現(xiàn)可能包括觸發(fā)propertyChange事件,這兒從簡(jiǎn)):
public void setPressed(boolean p){pressed=p;
repaint();
}
到此為止,你已經(jīng)能根據(jù)上面的兩條準(zhǔn)則簡(jiǎn)單的實(shí)現(xiàn)你想要的組件了。但是你發(fā)現(xiàn)沒有,你的按鈕狀態(tài)和外觀行為都被堆到了Component (MyButton)中實(shí)現(xiàn)了,而且,對(duì)于各個(gè)平臺(tái)都是一個(gè)樣子,不能換皮膚。這對(duì)于比較簡(jiǎn)單、不想要皮膚的組件,可能沒有什么,但是對(duì)于復(fù)雜的組件,比如JTable或者甚至Excel類似的電子表格的那種組件,你把數(shù)據(jù)(組件狀態(tài))和外觀堆在這兒實(shí)現(xiàn)就嚴(yán)重違反了MVC原則。
如何簡(jiǎn)化這種組件的實(shí)現(xiàn)呢?使你實(shí)現(xiàn)的此種組件容易維護(hù)、擴(kuò)展以及重用,皮膚容易換呢?這就需要Swing結(jié)構(gòu)中的另外三個(gè)元素:Model、UI Delegate和Renderer,后面的幾個(gè)文章將講述Model、UI Delegate和Renderer幫你逐步實(shí)現(xiàn)一個(gè)復(fù)雜、靈活、可擴(kuò)展、高效的矢量組件。
=====================================================
今天這樣講,不知道講明白沒有。當(dāng)初剛開始學(xué)習(xí)Swing的時(shí)候,還不了解Swing的這種MVC結(jié)構(gòu),因此當(dāng)時(shí)自己做的自定義組件都是這樣寫的,沒有單獨(dú)的Model和可以定制的UI Delegate,更不用說Renderer了。但是我覺得自己的這個(gè)學(xué)習(xí)過程,恰恰是人們學(xué)習(xí)Swing的最佳途徑,先簡(jiǎn)化模型,然后逐步擴(kuò)展,直到了解Swing模型的全部圖像。
=====================================================================================昨晚回去后還是覺得Component對(duì)象本身說的太簡(jiǎn)單,想來想去,覺得內(nèi)容實(shí)在是太多,有必要補(bǔ)充兩個(gè)續(xù)文說明Component的其它概念。今天介紹Swing組件paint方法的處理流程,這個(gè)流程能使我們理解許多Swing機(jī)制。明天續(xù)文講述Swing事件處理器、雙緩沖和布局管理器等原理。
=====================================
Swing組件的paint方法是內(nèi)部接口方法,一般用戶不要直接調(diào)用這個(gè)方法,它總是在事件調(diào)度線程中調(diào)用。一般說來除了系統(tǒng)刷新事件觸發(fā)這個(gè)方法,Component的repaint也觸發(fā)這個(gè)方法的調(diào)用。repaint方法常用于當(dāng)組件狀態(tài)發(fā)生變化時(shí)刷新界面使用。repaint方法是Swing中少數(shù)幾個(gè)線程安全的方法,可以在任何線程中調(diào)用它。它的原理是往事件隊(duì)列中post一個(gè)PAINT事件。由于事件隊(duì)列的事件是被事件調(diào)度線程同步執(zhí)行的,所以這個(gè)方法總是線程安全的。事件調(diào)度線程從PAINT事件中獲取事件源組件,從系統(tǒng)申請(qǐng)到圖形設(shè)備資源后,調(diào)用該組件的update方法。update是AWT時(shí)代遺留下來的產(chǎn)物,本意是AWT組件畫好組件背景后,再調(diào)用paint方法畫出組件的前景。Swing出現(xiàn)后這個(gè)方法就被棄用了,所有邏輯都轉(zhuǎn)到paint方法里。Update只是簡(jiǎn)單地調(diào)用paint方法來完成組件的渲染。老的Java教材上經(jīng)常可以看到,所謂repaint調(diào)度update方法,update接著調(diào)用paint方法,自定義組件需要重載paint方法等話語,就是因?yàn)檫@個(gè)歷史造成的。
上篇文章中的MyButton的paint方法實(shí)現(xiàn)是一個(gè)非常老式的做法。現(xiàn)在JComponent的實(shí)現(xiàn)已經(jīng)把paint方法改造成可以嵌套多重機(jī)制地方,這些機(jī)制包括層次渲染、邊框、透明背景、雙緩沖以及皮膚等。這些機(jī)制分別實(shí)現(xiàn)不同目的的組件提供了方便。
圖形用戶界面的組件按照其在組件樹上的角色可以分為容器組件和葉組件。Swing模型把葉組件當(dāng)作是特殊、沒有子組件的容器組件,只是JComponent繼承Container類,所有Swing組件繼承JComponent的原因。
JComponent在paint方法中首先根據(jù)組件是否需要使用雙緩沖,封裝好圖形設(shè)備對(duì)象,然后經(jīng)過一番處理后調(diào)用paintComponent方法畫出自身,然后調(diào)用paintBorder畫出邊框,最后調(diào)用paintChildren來完成子組件的渲染。
paintComponent意思是畫出組件自身,不包括子組件。因此前一文章中的MyButton可以通過覆蓋paintComponent方法來完成MyButton的重畫。在JComponent實(shí)現(xiàn)中,JDK 6的paintComponent的代碼為:
protected void paintComponent(Graphics g) {if (ui != null) {
Graphics scratchGraphics = (g == null) ? null : g.create();
try {
ui.update(scratchGraphics, this);
}
finally {
scratchGraphics.dispose();
}
}
}
這個(gè)方法首先檢測(cè)組件是否安裝了UI Delegate,如果安裝了就將渲染過程代理給UI Delegate。這兒是嵌入皮膚的地方。JDK 6中JComponent對(duì)應(yīng)的UI Delegate的update方法缺省的實(shí)現(xiàn)是:
public void update(Graphics g, JComponent c) {if (c.isOpaque()) {
g.setColor(c.getBackground());
g.fillRect(0, 0, c.getWidth(),c.getHeight());
}
paint(g, c);
}
可以看出,背景透明機(jī)制在這兒實(shí)現(xiàn)。首先UI Delegate對(duì)象判斷Component是否背景透明的,如果不是透明的,則使用背景色填充整個(gè)Component區(qū)域,然后調(diào)用paint(g, c)來完成組件在這種LookAndFeel種的渲染。了解了這些后,我們幾乎就明白了Swing如何實(shí)現(xiàn)背景透明和如何切換皮膚。由于后面的文章還會(huì)對(duì)UI Delegate和皮膚機(jī)制詳細(xì)描述,這兒就到此為止。
目前還不要求實(shí)現(xiàn)皮膚,在這種情況下只需要重載paintComponent方法就行了,如果需要背景透明機(jī)制,可以模仿上面代碼,MyButton的paintComponent可以這樣寫:
public void paintComponent(Graphics g) {if (isOpaque()) {
g.setColor(getBackground());
g.fillRect(0, 0, getWidth(), getHeight());
}
if(pressed){//按鈕按下去了
//畫出按下的樣子
}else{
//畫出抬起的樣子
}
}
paintBorder意思是畫出組件的邊框。Swing所有組件都有邊框的概念,就是說可以為任何組件添加各種邊框,包括自定義的邊框。JDK 6中JComponent的paintBorder的實(shí)現(xiàn)是這樣的:
protected void paintBorder(Graphics g) {Border border = getBorder();
if (border != null) {
border.paintBorder(this, g, 0, 0, getWidth(), getHeight());
}
}
非常直接,如果自己有border,就將畫自己邊框的任務(wù)代理給了這個(gè)border,并傳給它圖形設(shè)備和邊界參數(shù)。Swing缺省提供了大量的各種各樣的邊框。同樣可以定義自己的邊框,實(shí)現(xiàn)方法就是繼承Border類,Border類中有三個(gè)方法要實(shí)現(xiàn),它們的含義如下:
public interface Border{
//這兒是畫出組件邊框的地方。
void paintBorder(Component c, Graphics g, int x, int y, int width, int height);
//這兒是定義邊框邊界的地方,組件可以根據(jù)這信息,安排它的內(nèi)容。
Insets getBorderInsets(Component c);
//邊框的背景是不是透明的?不是透明的要負(fù)責(zé)畫出邊框的背景。是透明的使用組件的背景。
boolean isBorderOpaque();
}
這兒實(shí)現(xiàn)一個(gè)簡(jiǎn)單的紅線邊框作為演示:
public class RedLineBorder implements Border{public void paintBorder(Component c, Graphics g, int x, int y, int width, int height){
g.setColor(Color.red);//設(shè)置為紅色
g.drawRect(x,y, width, height);//畫出邊框
}
public Insets getBorderInsets(Component c){
return new Insets(1,1,1,1); //四周都是1
}
public boolean isBorderOpaque(){
return false; //背景透明
}
}
paintChildren完成容器類組件的子組件的渲染。JDK缺省的實(shí)現(xiàn)是調(diào)用各個(gè)自組件的paint方法。一般來說不需要重載這個(gè)方法。如果想改變諸如組件Z-order遮擋順序,可以覆蓋這個(gè)方法,從相反順序調(diào)用組件的paint方法。
到這兒我們對(duì)Swing的結(jié)構(gòu)有了更深化的理解,UI Delegate機(jī)制也已經(jīng)初露倪端。還有幾個(gè)重要Swing Component概念或者機(jī)制沒有講,明天的續(xù)文再對(duì)它們做出說明。
=================================================================================
Swing的事件處理過程為:事件調(diào)度線程(Event Dispatch Thread)從事件隊(duì)列(EventQueue)中獲取底層系統(tǒng)捕獲的原生事件,如鼠標(biāo)、鍵盤、焦點(diǎn)、PAINT事件等。接著調(diào)用該事件源組件的dispachEvent。該方法過濾出特殊事件后,調(diào)用processEvent進(jìn)行處理。processEvent方法根據(jù)事件類型調(diào)用注冊(cè)在這個(gè)組件上的相應(yīng)事件處理器函數(shù)。事件處理器函數(shù)根據(jù)這些事件的特征,判斷出用戶的期望行為,然后根據(jù)期望行為改變組件的狀態(tài),然后根據(jù)需要刷新組件外觀,觸發(fā)帶有特定語義的高級(jí)事件。此事件繼續(xù)傳播下去,直至調(diào)用應(yīng)用程序注冊(cè)在該組件上的處理器函數(shù)。下圖是這個(gè)過程的示意圖:
上圖所示意的過程簡(jiǎn)要說就是:
Pump an Event->Dispatch & Process Event->MouseListener.mousePressed->fireActionPerformed->ActionListener.actionPeformed->Do database query and display result to a table->Return from actionPerformed->Return from fireActionPerformed->Return from MouseListener.mousePressed->Pump another Event.
事件調(diào)度線程在應(yīng)用程序事件處理函數(shù)actionPerformed沒有完成之前是不能處理下一個(gè)事件的,如果應(yīng)用程序處理函數(shù)是一個(gè)時(shí)間復(fù)雜的任務(wù)(比如查詢數(shù)據(jù)庫并將結(jié)果顯示到表格中),后面包括PAINT事件將在長(zhǎng)時(shí)間內(nèi)得不到執(zhí)行。由于PAINT事件負(fù)責(zé)將界面更新,所以這就使用戶界面失去響應(yīng)。
打一個(gè)比方,事件處理線程就像進(jìn)入某城唯一的單行道一樣,事件相當(dāng)于汽車。有種PAINT汽車負(fù)責(zé)為城市運(yùn)輸非常重要的生活物資。但是有一天,PAINT前面有一輛汽車突然壞掉了,司機(jī)下來修車。但是這車太難修,一修就是幾天,結(jié)果后面的PAINT汽車無法前進(jìn),物資無法按時(shí)運(yùn)到城里。市民急了,市長(zhǎng)雖然不停的打電話催PAINT公司,但即使PAINT公司多添加幾輛車也沒用。由于進(jìn)城的唯一條路被那輛車給占著,所以再多的PAINT車也只能堵在路上。
不了解Swing的這種事件處理模型的人往往將時(shí)間復(fù)雜的任務(wù)放在處理函數(shù)中完成,這是造成Swing應(yīng)用程序速度很慢的原因。用戶觸發(fā)這個(gè)動(dòng)作,用戶界面就失去了響應(yīng),于是給用戶的感覺就是Swing太慢了。其實(shí)這個(gè)錯(cuò)誤是程序員造成的,并不是Swing的過失。
說點(diǎn)題外話,所有采用這種事件模型的用戶界面工具都會(huì)產(chǎn)生這種問題,包括SWT、GTK、MFC等流行的用戶界面工具。之所以只有Swing被誤解,主要是和Swing的歷史、市場(chǎng)時(shí)機(jī)、商業(yè)宣傳策略和心理學(xué)相關(guān)的。
首先Swing的歷史和市場(chǎng)時(shí)機(jī)極差。Swing出現(xiàn)早期性能也差、錯(cuò)誤也多,而Java程序員脫身于傳統(tǒng)圖形界面工具,對(duì)于Swing這種新的事件處理模型并不太了解,而此時(shí)正處于Java第一輪狂熱的時(shí)期,大家都滿懷希望做了大量的Swing應(yīng)用程序,而這些程序中大量存在這種錯(cuò)誤方法。于是市場(chǎng)上涌現(xiàn)了大批的這種程序。自從那個(gè)時(shí)代,因?yàn)檫@些程序,Swing被貼上了慢的標(biāo)簽。又由于當(dāng)時(shí)的Swing界面也丑,和一般的Windows程序風(fēng)格炯異,更加深人們的這種印象。這種印象一直持續(xù)到現(xiàn)在,像烙印一樣深深的刻在人們的腦海里。
其次,Swing還有一個(gè)致命的問題,就是沒有涌現(xiàn)出一個(gè)具有標(biāo)識(shí)性的好程序,這是造成它比SWT印象慘的原因。為什么SWT采用相同的事件處理模型,而獲得了速度快的聲譽(yù)呢?這是因?yàn)槿藗儺?dāng)時(shí)對(duì)于Java做桌面應(yīng)用的期望心理達(dá)到了低谷,而SWT的出現(xiàn)恰恰是伴隨Eclipse出現(xiàn)的,早期的Eclipse的確是在速度快、界面漂亮,這一掃當(dāng)時(shí)人們認(rèn)為Java慢,Java界面丑陋,Java無法做桌面應(yīng)用的印象,繼而這個(gè)印象被加在SWT身上,人們認(rèn)為Eclipse速度快、漂亮是因?yàn)镾WT,其實(shí)如果你知道Swing/SWT事件處理模型的話,你就明白功勞是Eclipse開發(fā)者的,Eclipse界面漂亮其實(shí)要?dú)w功于Eclipse界面設(shè)計(jì)專家,他們的高水平造就了這個(gè)好的IDE,從而也抬起了SWT的聲譽(yù)。而Swing的名譽(yù)恰恰就被早期Swing低水平開發(fā)者給毀了。
再次, 這和商業(yè)宣傳策略有關(guān)。IBM和Eclipse很懂得市場(chǎng)宣傳,人們不是認(rèn)為Java慢嗎,就宣傳SWT使用原生組件,人們不是認(rèn)為Swing丑陋、風(fēng)格炯異吧,就宣傳SWT風(fēng)格一致性,人們不是認(rèn)為Java不能做桌面應(yīng)用嗎,就宣傳基于SWT的Eclipse。其實(shí)這一切的背后原因只是“人”的不同,Eclipse的開發(fā)者和Swing應(yīng)用程序的開發(fā)者,Swing和SWT技術(shù)差異并沒有造成那么大的差別,如果是相近能力的人使用他們開發(fā)的話,應(yīng)該能做出相近的產(chǎn)品。這可以從現(xiàn)在Eclipse和NetBeans、Intellij IDEA、JDeveloper和JBuilder看的出來。
最后,人類有一個(gè)心理學(xué)現(xiàn)象,就是一旦形成對(duì)某種事物的印象,很難擺脫舊的認(rèn)識(shí),有時(shí)甚至人們不愿意承認(rèn)擺在眼前的事實(shí)。總而言之,Swing和SWT不同遭遇是因?yàn)闅v史、市場(chǎng)時(shí)機(jī)、商業(yè)宣傳策略、心理學(xué)的種種原因造成的。
那么如何避免這個(gè)問題,編寫響應(yīng)速度快的Swing應(yīng)用程序呢?在SwingWorker的javadoc中有這樣兩條原則:
Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the application becomes unresponsive. 耗時(shí)任務(wù)不要放到事件調(diào)度線程上執(zhí)行,否則程序就會(huì)失去響應(yīng)。
Swing components should be accessed on the Event Dispatch Thread only. Swing組件只能在事件調(diào)度線程上訪問。
因此處理耗時(shí)任務(wù)時(shí),首先要啟動(dòng)一個(gè)專門線程,將當(dāng)前任務(wù)交給這個(gè)線程處理,而當(dāng)前處理函數(shù)立即返回,繼續(xù)處理后面未決的事件。這就像前面塞車的例子似的,那個(gè)司機(jī)只要簡(jiǎn)單的把車開到路邊或者人行道上修理,整個(gè)公路系統(tǒng)就會(huì)恢復(fù)運(yùn)轉(zhuǎn)。
其次,在為耗時(shí)任務(wù)啟動(dòng)的線程訪問Swing組件時(shí),要使用SwingUtilties. invokeLater或者SwingUtilities.invokeAndWait來訪問,invokeLater和invokeAndWait的參數(shù)都是一個(gè)Runnable對(duì)象,這個(gè)Runnable對(duì)象將被像普通事件處理函數(shù)一樣在事件調(diào)度線程上執(zhí)行。這兩個(gè)函數(shù)的區(qū)別是,invokeLater不阻塞當(dāng)前任務(wù)線程,invokeAndWait阻塞當(dāng)前線程,直到Runnable對(duì)象被執(zhí)行返回才繼續(xù)。在前面塞車的例子中,司機(jī)在路邊修車解決了塞車問題,但是他突然想起來要家里辦些事情,這時(shí)他就可以打個(gè)電話讓家里開車來。假如修車不受這件事情的影響,比如叫家人送他朋友一本書,他可以繼續(xù)修車,這時(shí)就相當(dāng)于invokeLater;假如修車受影響,比如缺少某個(gè)汽車零件,叫家人給他送過來,那么在家人來之前,他就沒法繼續(xù)修車,這時(shí)就相當(dāng)于invokeAndWait。
下面舉一個(gè)例子說明這兩點(diǎn),比如按下查詢按鈕,查詢數(shù)據(jù)量很大的數(shù)據(jù)庫,并顯示在一個(gè)表中,這個(gè)過程需要給用戶一個(gè)進(jìn)度提示,并且能動(dòng)態(tài)顯示表格數(shù)據(jù)動(dòng)態(tài)增加的過程。假設(shè)按鈕的處理函數(shù)是myButton_actionPerformed,則:
void myButton_actionPerformed(ActionEvent evt){new MyQueryTask().start();
}
public class MyQueryTask extends Thread{
public void run(){
//查詢數(shù)據(jù)庫
final ResultSet result=...;
/ /顯示記錄
for(;result.next();){
//往表的Model中添加一行數(shù)據(jù),并更新進(jìn)度條,注意這都是訪問組件
SwingUtilities.invokeLater(new Runnable(){
public void run(){
addRecord(result);
}
});
}
....
}
void addRecord(ResultSet result){
//往表格中添加數(shù)據(jù)
jTable.add....
//更新進(jìn)度條
jProgress.setValue(....);
}
}
JDK1.6以后,Swing提供了一個(gè)專門的類SwingWorker能幫你解決這個(gè)編程范式,你所需要做的就是繼承這個(gè)類,重載doInBackground,然后在actionPeformed中調(diào)用它的execute方法,并通過publish/process方法來更新界面。SwingWorker的主要方法和它們的作用在下面的示意圖:
從上面示意圖可以看出,SwingWorker實(shí)際上不過是封裝了前面我所說的例子中的MyQueryTask,并做了更詳盡的考慮。execute方法相當(dāng)于MyQueryTask線程start,它啟動(dòng)這個(gè)后臺(tái)線程并立刻返回。SwingWorker可以注冊(cè)PropertyChangeListener,這些listener都被在事件調(diào)度線程上執(zhí)行,相當(dāng)于MyQueryTask中的那些訪問組件的Runnable對(duì)象。另外,publish、setProgress只不過是特殊的property事件吧,process和done不過是響應(yīng)publish和PropertyChangeEvent.DONE這個(gè)事件的方法罷了。因此我們很容易將上面的例子改成SwingWorker的版本:
void myButton_actionPerformed(ActionEvent evt){new MyQueryTask().execute();
}
public class MyQueryTask extends SwingWorker{
public void doInBackground(){
//查詢數(shù)據(jù)庫
final ResultSet result=...;
//顯示記錄
for(;result.next();){
//往表的Model中添加一行數(shù)據(jù),并更新進(jìn)度條,注意這都是訪問組件
publish(result);
}
....
}
public void process(Object ... result){
//往表格中添加數(shù)據(jù)
jTable.add....
//更新進(jìn)度條
jProgress.setValue(....);
}
}
對(duì)于一般的耗時(shí)任務(wù)這樣做是比較普遍的,但是有一些任務(wù)是一旦觸發(fā)之后,會(huì)周期性的觸發(fā),如何做處理這種任務(wù)呢?JDK中提供了兩個(gè)Timer類幫你完成定時(shí)任務(wù),一個(gè)是javax.swing.Timer,一個(gè)java.util.Timer。使用它們的方法很簡(jiǎn)單,對(duì)于Swing的timer,使用方法如下:
public void myActionPerformed(){//假設(shè)點(diǎn)擊了某個(gè)按鈕開始記時(shí)
Action myAction=new AbstractAction(){
public void actionPerformed(ActionEvent e){
//做周期性的活動(dòng),比如顯示當(dāng)前時(shí)間
Date date=new Date();
jMyDate.setDate(date);//jMyDate是個(gè)假想的組件,能顯示日期時(shí)間
}
};
new Timer(1000, myAction).start();
}
java.util.Timer類似,只不過使用TimerTask完成動(dòng)作封裝。注意這兩個(gè)Timer有一個(gè)關(guān)鍵的區(qū)別:Swing的Timer的事件處理都是在事件調(diào)度線程上進(jìn)行的,因而它里面的操作可以直接訪問Swing組件。而java.util.Timer則可能在其他線程上,因而訪問組件時(shí)要使用SwingUtilities.invokeLater和invokeAndWait來進(jìn)行。這一點(diǎn)要記住。
如果要了解更詳細(xì)的信息,可以查閱SwingWorker、Swing Timer和util Timer這些類javadoc文檔和其他網(wǎng)上資料。最重要的是要記住了那兩條原則。
============================================================================
Swing事件與事件處理器模型
Component在Swing模型中是事件觸發(fā)源。前一篇文章在描述Swing的事件處理模型時(shí)就已經(jīng)提到了這個(gè)事件處理過程。簡(jiǎn)單來說,Swing組件在偵聽到原生事件并處理后,往往產(chǎn)生新的邏輯事件。邏輯事件是某些組件所特有的、具有特定語義的事件,比如JButton按下時(shí)產(chǎn)生ActionEvent、JComboBox一項(xiàng)被選中時(shí)產(chǎn)生ItemEvent,等等。和原生事件不同,它們并不被派發(fā)到系統(tǒng)事件隊(duì)列中,而是由組件直接觸發(fā)。事件處理器作為組件的觀察者添加到組件上并偵聽觸發(fā)的事件。假設(shè)事件名叫XXX,Swing中實(shí)現(xiàn)這個(gè)模式的一般模式是:
1.定義一個(gè)XXXEvent
public class XXXEvent extends Event{...
public void XXXEvent(Object src){
super(src);
...
}
...
}
2.定義一個(gè)事件處理器接口XXXListener,聲明所有和該事件相關(guān)的處理方法:
public interface XXXListener extends EventListener{void action1(XXXEvent evt);
void action2(XXXEvent evt);
...
}
3.在觸發(fā)它的組件中定義一下方法:
public class MyComponent extends Jcomponent{
...
//存放事件處理器的隊(duì)列
private ArrayList<XXXListener>xxxListeners=new ArrayList<XXXListener>();
//定義以下各種方法,訪問符號(hào)用public,以方便添加刪除處理器
public void addXXXListener(XXXListener listener){
xxxListeners.add(listener);
}
public void removeXXXListener(XXXListener listener){
xxxListeners.remove(listener);
}
//定義各種觸發(fā)(fire)action1、action2...的方法,注意一般使用protected,以便繼承和擴(kuò)展
//每一個(gè)action都要定義一個(gè)相應(yīng)觸發(fā)(fire)的方法
protected void fireAction1(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action1(evt);
}
}
protected void fireAction2(XXXEvent evt){
for(XXXListener listener:xxxListeners){
listener.action2(evt);
}
}
...
//在某些地方,比如鼠標(biāo)處理函數(shù)中觸發(fā)相應(yīng)的動(dòng)作
void myMouseReleased(MouseEvent evt){
...
if(應(yīng)該觸發(fā)action1)
fireAction1(new XXXEvent(this));
...
if(應(yīng)該觸發(fā)action2)
fireAction2(new XXXEvent(this));
...
}
}
XXXEvent、XXXListener、addXXXListener、removeXXXListener以及各種fireAction函數(shù)多是重復(fù)性代碼,有些Java IDE如JBuilder中能夠根據(jù)開發(fā)者的指定參數(shù)的自動(dòng)生成這些代碼。
實(shí)際上這個(gè)觀察者模式的編程范式可以推廣到任何JavaBeans,不一定是可視化的Swing組件。以前曾經(jīng)見過JBuilder做的一個(gè)所謂數(shù)據(jù)庫操作的JavaBeans,它沒有界面,但它和Swing組件完全一樣添加刪除處理器。它的功能是異步操作數(shù)據(jù)庫,在數(shù)據(jù)操作完了之后觸發(fā)注冊(cè)在上面的事件處理器,該事件處理器就可以將查詢結(jié)果展現(xiàn)在表格中,或者輸出成報(bào)表等等。
在這個(gè)模型中,JavaBeans本身既可以是事件源(被觀察對(duì)象),也可以是事件處理器(觀察者),JavaBeans也可以偵聽自身的事件并且處理。比如前面文章所提的MyButton在處理鼠標(biāo)事件時(shí)就是自己偵聽自己發(fā)出的鼠標(biāo)事件,自己既是事件源,又是事件處理器,形成自反系統(tǒng)。各種各樣的JavaBeans通過這種機(jī)制聯(lián)系成一張事件網(wǎng),各種JavaBeans就是這個(gè)網(wǎng)上的節(jié)點(diǎn),而它們之間的事件觸發(fā)與事件處理關(guān)系就是這張網(wǎng)絡(luò)上的線。當(dāng)某個(gè)節(jié)點(diǎn)被外界或自身發(fā)出的事件所觸發(fā)時(shí),行成了事件的傳播。這個(gè)過程很像網(wǎng)絡(luò)上節(jié)點(diǎn)的振動(dòng)引起周圍周圍節(jié)點(diǎn)振動(dòng)的模型。下圖示意了這種JavaBeans之間的事件網(wǎng):
例如new JscrollPane(new JtextArea())這個(gè)系統(tǒng),它里面包括兩個(gè)JScrollBar和一個(gè)JTextArea,當(dāng)鼠標(biāo)拖動(dòng)事件觸發(fā)JScrollBar時(shí),JScrollBar處理了這個(gè)鼠標(biāo)拖動(dòng)事件,并發(fā)出滾動(dòng)條拖動(dòng)事件,這個(gè)事件傳播給JTextArea,JTextArea處理這個(gè)拖動(dòng)事件,相應(yīng)的更新自己顯示的內(nèi)容,如果JTextArea之后又根據(jù)更新發(fā)出了一個(gè)新的事件,這個(gè)事件便會(huì)繼續(xù)傳播下去。
Swing布局管理器
現(xiàn)在高級(jí)圖形用戶界面工具一般都包括布局管理器機(jī)制。什么叫做布局管理器?如果所有窗口的大小是不變的,那么我們?cè)谕翱谥刑砑咏M件時(shí),只要將組件的拖放到固定位置、調(diào)整好尺寸就可以了,就像VB的界面工具一樣。可大多數(shù)情況并非如此,用戶經(jīng)常需要調(diào)整窗口的大小,以便和其他程序協(xié)同工作。這種情況下,在傳統(tǒng)界面工具中,比如VB,就需要顯式的偵聽窗口尺寸調(diào)整事件,根據(jù)當(dāng)前窗口的大小重新計(jì)算并調(diào)整各個(gè)組件的大小和位置。AWT/SWT/Swing將這個(gè)過程自動(dòng)化、模塊化了,抽象出一個(gè)布局管理器來負(fù)責(zé)管理界面組件的布局。
它們實(shí)現(xiàn)原理是相似的:容器類組件偵聽初始化、invalide/validate以及容器尺寸調(diào)整等事件,一旦發(fā)生這些事件,容器類組件檢查自己是否配置了布局管理器,如果沒有,則不做任何事情;如果有,則將容器內(nèi)組件的布局代理給布局管理器,讓它來完成容器內(nèi)組件的重新布局。
容器管理器對(duì)象對(duì)實(shí)現(xiàn)兩類接口:LayoutManager和LayoutManager2,LayoutManager2是LayoutManager的一個(gè)擴(kuò)展,允許組件在添加時(shí)指定位置參數(shù)。它們的定義和含義如下:
public interface LayoutManager {//添加組件comp,并和name關(guān)聯(lián)起來,name可以作為位置等特殊含義參數(shù)來使用
void addLayoutComponent(String name, Component comp);
//刪除組件comp
void removeLayoutComponent(Component comp);
//根據(jù)容器內(nèi)的當(dāng)前組件,計(jì)算容器parent的最優(yōu)尺寸。
Dimension preferredLayoutSize(Container parent);
//根據(jù)容器內(nèi)的當(dāng)前組件,計(jì)算容器parent的最小尺寸。
Dimension minimumLayoutSize(Container parent);
//重新布局容器parent,這兒是主要布局邏輯所在。
void layoutContainer(Container parent);
}
public interface LayoutManager2 extends LayoutManager {
//添加組件comp,constraints用作指定如何以及位置的參數(shù),這個(gè)函數(shù)主要是彌補(bǔ)LayoutManager版的addLayoutComponent表達(dá)能力欠缺而添加。
void addLayoutComponent(Component comp, Object constraints);
//根據(jù)容器內(nèi)的當(dāng)前組件,計(jì)算容器parent的最大尺寸。看來除了最優(yōu)、最小,某些情況下還是需要知道最大。
public Dimension maximumLayoutSize(Container target);
//指定水平方向上組件之間的相對(duì)對(duì)齊方式,0表示和源組件對(duì)齊,1表示遠(yuǎn)離源組件。
public float getLayoutAlignmentX(Container target);
//指定垂直方向上組件之間的相對(duì)對(duì)齊方式,0表示和源組件對(duì)齊,1表示遠(yuǎn)離源組件。
public float getLayoutAlignmentY(Container target);
//invalidate這個(gè)布局管理器,有時(shí)布局管理器為了計(jì)算迅速,可能第一次計(jì)算之后就將一些數(shù)據(jù)給緩沖,但是后容器內(nèi)的組件數(shù)目發(fā)生變化,這兒的緩沖值就需要調(diào)用這個(gè)方法通知更新
public void invalidateLayout(Container target);
}
Swing在java.awt和javax.swing中都分別提供大量的布局管理器,這些布局管理器有簡(jiǎn)單的如FlowLayout,有復(fù)雜的如GridBadLayout。用戶還可以自己定義自己的布局管理器,由于篇幅原因,這兒略去例子。
Java 6中在布局管理中引入了BaseLine / Anchor的概念,能協(xié)助Java IDE的用戶界面設(shè)計(jì)工具,方便用戶來設(shè)計(jì)布局組件。NetBeans的Matisse組件首先引入了一個(gè)GroupLayout布局管理器,結(jié)合Matisse使用,提供了非常方便的布局管理和界面設(shè)計(jì)。GroupLayout和BaseLine/Anchor概念以及Matisse可以說是Java界面設(shè)計(jì)工具的一大進(jìn)步,可以說足以成為Java桌面應(yīng)用史上的一個(gè)里程碑。在這之前,缺乏有力的界面設(shè)計(jì)工具是Java在桌面應(yīng)用失敗的一個(gè)重要原因。雖然Anchor概念早就在Delphi界面設(shè)計(jì)工具出現(xiàn)過,但是這個(gè)工具的出現(xiàn)還是Java界面設(shè)計(jì)史上的一大事件。隨著Java 6桌面應(yīng)用支持的增強(qiáng),以及NetBeans Matisse之類界面設(shè)計(jì)工具的出現(xiàn),使得Java桌面應(yīng)用時(shí)代已經(jīng)到來。Seeing is believing,你不妨試一下就知道了。
=====================================
本想再加一節(jié)講述Swing雙緩沖機(jī)制,但是想到雙緩沖并不是Swing模型的核心概念,沒有它并不影響理解Swing的總體模型,因此打算把它作為以后的一篇專門技術(shù)文章來寫。
這樣Swing模型中的Component部分就算是描述完了,從明天開始,講述Swing模型中的另外三個(gè)重要概念:Model、UI Delegate和Renderer。