在Swing中的繪畫
Swing起步於AWT基本繪畫模式,並且作了進一步的擴展以獲得最大化的性能以及改善可擴展性能。象AWT一樣,Swing支持回調繪畫以及使用repaint()
促使部件更新。另外,Swing提供了內置的雙緩衝(double-buffering)並且作了改變以支持Swing的其它結構(象邊框(border)和UI代理)。最後,Swing爲那些想更進一步定製繪畫機制的程序提供了RepaintManager
API。
對雙緩衝的支持
Swing的最引人注目的特性之一就是把對雙緩衝的支持整個兒的內置到工具包。通過設置javax.swing.JComponent
的"doubleBuffered"屬性就可以使用雙緩衝:
public boolean isDoubleBuffered()
public void setDoubleBuffered(boolean o)
當緩衝激活的時候,Swing的雙緩衝機制爲每個包容層次(通常是每個最高層的窗體)準備一個單獨的屏外緩衝。並且,儘管這個屬性可以基於部件而設置,對一個特定的容器上設置這個屬性,將會影響到這個容器下面的所有輕量級部件把自己的繪畫提交給屏外緩衝,而不管它們各自的"雙緩衝"屬性值
默認地,所有Swing部件的該屬性值爲true
。不過對於JRootPane
這種設置確實有些問題,因爲這樣就使所有位於這個上層Swing部件下面的所有部件都使用了雙緩衝。對於大多數的Swing程序,不需要作任何特別的事情就可以使用雙緩衝,除非你要決定這個屬性是開還是關(並且爲了使GUI能夠平滑呈現,你需要打開這個屬性)。Swing保證會有適宜的Graphics
對象(或者是爲雙緩衝使用的屏外映像的Graphics
,或者是正規的Graphics
)傳遞給部件的繪畫回調函數,所以,部件需要做的所有事情僅僅就是使用這個Graphics
畫圖。本文的後面,在繪製的處理過程這一章會詳細解釋這個機制。
其他的繪畫屬性
爲了改善內部的繪畫算法性能,Swing另外引進了幾個JComponent
的相互有關聯的屬性。引入這些屬性爲的是處理下面兩個問題,這兩個問題有可能導致輕量級部件的繪畫成本過高:
- 透明(Transparency): 當一個輕量級部件的繪畫結束時,如果該部件的一部分或者全部透明,那麼它就可能不會把所有與其相關的像素位都塗上顏色;這就意味着不管它什麼時候重畫,它底層的部件必須首先重畫。這個技術需要系統沿着部件的包容層次去找到最底層的重量級祖先,然後從它開始、從後向前地執行繪畫。
- 重疊的部件(Overlapping components): 當一個輕量級部件的繪畫結束是,如果有一些其他的輕量級部件部分地疊加在它的上方;就是說,不管最初的輕量級部件什麼時候畫完,只要有疊加在它上面的其它部件(裁剪區與疊加區相交),這些疊加的部件必須也要部分地重畫。這需要系統在每次繪畫時要遍歷大量的包容層次,以檢查與之重疊的部件。
遮光性
在一般情況下部件是不透明的,爲了提高改善性能,Swing增加了讀寫javax.swing.JComponent
的遮光(opaque)
屬性的操作:
public boolean isOpaque()
public void setOpaque(boolean o)
這些設置是:
true
:部件同意在它的矩形範圍包含的裏所有像素位上繪畫。false
:部件不保證其矩形範圍內所有像素位上繪畫。
遮光(opaque)
屬性允許Swing的繪圖系統去檢測是否一個對指定部件的重畫請求會導致額外的對其底層祖先的重畫。每個標準Swing部件的默認(遮光)opaque
屬性值由當前的視-感UI對象設定。而對於大多數部件,該值爲true
。
部件實現中的一個最常見的錯誤是它們允許遮光(opaque)
屬性保持其默認值true
,卻又不完全地呈現它們所轄的區域,其結果就是沒有呈現的部分有時會造成屏幕垃圾。當一個部件設計完畢,應該仔細的考慮所控制的遮光(opaque)
屬性,既要確保透的使用是明智的,因爲它會花費更多的繪畫時間,又要確保與繪畫系統之間的協約履行。
遮光(opaque)
屬性的意義經常被誤解。有時候被用來表示“使部件的背景透明”。然而這不是Swing對遮光的精確解釋。一些部件,比如按鈕,爲了給部件一個非矩形的外形可能會把“遮光”設置爲false,或者爲了短時間的視覺效果使用一個矩形框圍住部件,例如焦點指示框。在這些情況下,部件不遮光,但是其背景的主要部分仍然需要填充。
如先前的定義,遮光屬性的本質是一個與負責重畫的系統之間訂立的契約。如果一個部件使用遮光屬性去定義怎樣使部件的外觀透明,那麼該屬性的這種使用就應該備有證明文件。(一些部件可能更合適於定義額外的屬性控制外觀怎樣怎樣增加透明度。例如,javax.swing.AbstractButton
提供ContentAreaFilled
屬性就是爲了達到這個目的。)
另一個毫無價值的問題是遮光屬性與Swing部件的邊框(border)
屬性有多少聯繫。在一個部件上,由Border
對象呈現的區域從幾何意義上講仍是部件的一部分。就是說如果部件遮光,它就有責任去填充邊框所佔用的空間。(然後只需要把邊框放到該不透明的部件之上就可以了)。
如果你想使一個部件允許其底層部件能透過它的邊框範圍而顯示出來 -- 即,通過isBorderOpaque()
判斷border是否支持透明而返回值爲false
-- 那麼部件必須定義自身的遮光屬性爲false並且確保它不在邊框的範圍內繪圖。
"最佳的"繪畫方案
部件重疊的問題有些棘手。即使沒有直接的兄弟部件疊加在該部件之上,也總是可能有非直系繼承關係(比如"堂兄妹"或者"姑嬸")的部件會與它交疊。這樣的情況下,處於一個複雜層次中的每個部件的重畫工作都需要一大堆的樹遍歷來確保'正確地'繪畫。爲了減少不必要的遍歷,Swing爲javax.swing.JComponent
增加一個只讀的isOptimizedDrawingEnabled
屬性:
public boolean isOptimizedDrawingEnabled()
這些設置是:
true
:部件指示沒有直接的子孫與其重疊。false
: 部件不保證有沒有直接的子孫與之交疊。
通過檢查isOptimizedDrawingEnabled
屬性,Swing在重畫時可以快速減少對交疊部件的搜索。
因爲isOptimizedDrawingEnabled
屬性是隻讀的,於是部件改變默認值的唯一方法是在其子類覆蓋(override)這個方法來返回所期望的值。除了JLayeredPane,JDesktopPane
,和JViewPort
外,所有標準Swing部件對這個屬性返回true
。
繪畫方法
適應於AWT的輕量級部件的規則同樣也適用於Swing部件 -- 舉一個例子,在部件需要呈現的時候就會調用paint()
-- 只是Swing更進一步地把paint()
的調用分解爲3個分立的方法,以下列順序依次執行:
protected void paintComponent(Graphics g)
protected void paintBorder(Graphics g)
protected void paintChildren(Graphics g)
Swing程序應該覆蓋paintComponent()
而不是覆蓋paint()
。雖然API允許這樣做,但通常沒有理由去覆蓋paintBorder()
或者paintComponents()
(如果你這麼做了,請確認你知道你到底在做什麼!)。這個分解使得編程變得更容易,程序可以只覆蓋它們需要擴展的一部分繪畫。例如,這樣就解決先前在AWT中提到的問題,因爲調用super.paint()
失敗而使得所有輕量級子孫都不能顯示。
SwingPaintDemo例子程序舉例說明了Swing的paintComponent()
回調方法的簡單應用。
繪畫與UI代理
大多數標準Swing部件擁有它們自己的、由分離的觀-感(look-and-feel)對象(叫做"UI代理")實現的觀-感。這意味着標準部件把大多數或者所有的繪畫委派給UI代理,並且出現在下面的途徑:
paint()
觸發paintComponent()
方法。- 如果
ui
屬性爲non-null,paintComponent()
觸發ui.update()。
- 如果部件的
遮光
屬性爲true,ui.udpate()
方法使用背景顏色填充部件的背景並且觸發ui.paint()
。 ui.paint()
呈現部件的內容。
這意味着Swing部件的擁有UI代理的子類(相對於JComponent
的直系子類),應該在它們所覆蓋的paintComponent
方法中觸發super.paintComponent()
。
|
如果因爲某些原因部件的擴展類不允許UI代理去執行繪畫(是如果,例如,完全更換了部件的外觀),它可以忽略對super.paintComponent()
的調用,但是它必須負責填充自己的背景,如果遮光(opaque)
屬性爲true
的話,如前面在遮光(opaque)
屬性一章講述的。
繪畫的處理過程
Swing處理"repaint"請求的方式與AWT有稍微地不同,雖然對於應用開發人員來講其本質是相同的 -- 同樣是觸發paint()
。Swing這麼做是爲了支持它的RepaintManager
API (後面介紹),就象改善繪畫性能一樣。在Swing裏的繪畫可以走兩條路,如下所述:
(A) 繪畫需求產生於第一個重量級祖先(通常是JFrame、JDialog、JWindow
或者JApplet
):
- 事件分派線程調用其祖先的
paint().
Container.paint()
的默認實現會遞歸地調用任何輕量級子孫的paint()
方法。- 當到達第一個Swing部件時,
JComponent.paint()
的默認執行做下面的步驟:- 如果部件的
雙緩衝
屬性爲true
並且部件的RepaintManager
上的雙緩衝已經激活,將把Graphics
對象轉換爲一個合適的屏外Graphics
。 - 調用
paintComponent()
(如果使用雙緩衝就把屏外Graphics傳遞進去)。 - 調用
paintChildren()
(如果使用雙緩衝就把屏外Graphics傳遞進去),該方法使用裁剪並且遮光
和optimizedDrawingEnabled
等屬性來嚴密地判定要遞歸地調用哪些子孫的paint()
。 - 如果部件的
雙緩衝
屬性爲true
並且在部件的RepaintManager
上的雙緩衝已經激活,使用最初的屏幕Graphics
對象把屏外映像拷貝到部件上。
注意:JComponent.paint()
步驟#1和#5在對paint()
的遞歸調用中被忽略了(由於paintChildren()
,在步驟#4中介紹了),因爲所有在swing窗體層次中的輕量級部件將共享同一個用於雙緩衝的屏外映像。
- 如果部件的
(B) 繪畫需求從一個javax.swing.JComponent
擴展類的repaint()
調用上產生:
JComponent.repaint()
註冊一個針對部件的RepaintManager
的異步的重畫需求,該操作使用invokeLater()
把一個Runnable
加入事件隊列以便稍後執行在事件分派線程上的需求。- 該Runnable在事件分派線程上執行並且導致部件的
RepaintManager
調用該部件上paintImmediately()
,該方法執行下列步驟:- 使用裁剪框以及
遮光
和optimizedDrawingEnabled
屬性確定“根”部件,繪畫一定從這個部件開始(處理透明以及潛在的重疊部件)。 - 如果根部件的
雙緩衝
屬性爲true
,並且根部件的RepaintManager
上的雙緩衝已激活,將轉換Graphics
對象到適當的屏外Graphics
。 - 調用根部件(該部件執行上述(A)中的
JComponent.paint()
步驟#2-4)上的paint()
,導致根部件之下的、與裁剪框相交的所有部件被繪製。 - 如果根部件的
doubleBuffered
屬性爲true
並且根部件的RepaintManager
上的雙緩衝已經激活,使用原始的Graphics
把屏外映像拷貝到部件。
注意:如果在重畫沒有完成之前,又有發生多起對部件或者任何一個其祖先的
repaint()
調用,所有這些調用會被摺疊到一個單一的調用 回到paintImmediately()
on topmostSwing部件 on which 其repaint()
被調用。例如,如果一個JTabbedPane
包含了一個JTable
並且在其包容層次中的現有的重畫需求完成之前兩次發佈對repaint()
的調用,其結果將變成對該JTabbedPane
部件的paintImmediately()
方法的單一調用,會觸發兩個部件的paint()
的執行。 - 使用裁剪框以及
這意味着對於Swing部件來說,update()
不再被調用。
雖然repaint()
方法導致了對paintImmediately()
的調用,它不考慮"回調"繪圖,並且客戶端的繪畫代碼也不會放置到paintImmediately()
方法裏面。實際上,除非有特殊的原因,根本不需要超載paintImmediately()
方法。
同步繪圖
象我們在前面章節所講述的,paintImmediately()
表現爲一個入口,用來通知Swing部件繪製自身,確認所有需要的繪畫都能適當地產生。這個方法也可能用來安排同步的繪圖需求,就象它的名字所暗示的,即一些部件有時候需要保證它們的外觀實時地與其內部狀態保持一致(例如,在JScrollPane
執行滾定操作的時候確實需要這樣並且也做到了)。
程序不應該直接調用這個方法,除非有合理實時繪畫需要。這是因爲異步的repaint()
可以使多個重複的需求得到有效的精簡,反之直接調用paintImmediately()
則做不到這點。另外,調用這個方法的規則是它必須由事件分派線程中的進程調用;它也不是爲能以多線程運行你的繪畫代碼而設計的。關於Swing單線程模式的更多信息,參考一起歸檔的文章"Threads and Swing."
RepaintManager
Swing的RepaintManager
類的目的是最大化地提高Swing包容層次上的重畫執行效率,同時也實現了Swing的'重新生效'機制(作爲一個題目,將在其它文章裏介紹)。它通過截取所有Swing部件的重畫需求(於是它們不再需要經由AWT處理)實現了重畫機制,並且在需要更新的情況下維護其自身的狀態(我們已經知道的"dirty regions")。最後,它使用invokeLater()
去處理事件分派線程中的未決需求,如同在"Repaint Processing"一節中描述的那樣(B選項).
對於大多數程序來講,RepaintManager
可以看做是Swing的內部系統的一部分,並且甚至可以被忽略。然而,它的API爲程序能更出色地控制繪畫中的幾個要素提供了選擇。
"當前的"RepaintManager
RepaintManager
設計 is designed to be dynamically plugged, 雖然 有一個單獨的接口。下面的靜態方法允許程序得到並且設置"當前的"RepaintManager
:
public static RepaintManager currentManager(Component c) public static RepaintManager currentManager(JComponent c) public static void setCurrentManager(RepaintManager aRepaintManager)
更換"當前的"RepaintManager
總的說來,程序通過下面的步驟可能會擴展並且更換RepaintManager
:
RepaintManager.setCurrentManager(new MyRepaintManager());
你也可以參考RepaintManagerDemo ,這是個簡單的舉例說明RepaintManager
加載的例子,該例子將把有關正在執行重畫的部件的信息打印出來。
擴展和替換RepaintManager
的一個更有趣的動機是可以改變對重畫的處理方式。當前,默認的重畫實現所使用的來跟蹤dirty regions的內部狀態值是包內私有的並且因此不能被繼承類訪問。然而,程序可以實現它們自己的跟蹤dirty regions的機制並且通過超載下面的方法對重畫需求的縮減:
public synchronized void
addDirtyRegion(JComponent c, int x, int y, int w, int h) public Rectangle getDirtyRegion(JComponent aComponent) public void markCompletelyDirty(JComponent aComponent) public void markCompletelyClean(JComponent aComponent) {
addDirtyRegion()
方法是在調用Swing部件的repaint()
的之後被調用的,因此可以用作鉤子來捕獲所有的重畫需求。如果程序超載了這個方法(並且不調用super.addDirtyRegion()
),那麼它改變了它的職責,而使用invokeLater()
把Runnable
放置到EventQueue
,該隊列將在合適的部件上調用paintImmediately()
(translation: not for the faint of heart).
從全局控制雙緩衝
RepaintManager
提供了從全局中激活或者禁止雙緩衝的API:
public void setDoubleBufferingEnabled(boolean aFlag)
public boolean isDoubleBufferingEnabled()
這個屬性在繪畫處理的時候,在JComponent
的內部檢查過以確定是否使用屏外緩衝顯示部件。這個屬性默認爲true
,但是如果程序希望在全局範圍爲所有Swing部件關閉雙緩衝的使用,可以按照下面的步驟做:
RepaintManager.currentManager(mycomponent). setDoubleBufferingEnabled(false);
注意:因爲Swing的默認實現要初始化一個單獨的RepaintManager
實例,mycomponent
參數與此不相關。
Swing繪畫準則
Swing開發人員在寫繪畫代碼時應該理解下面的準則:
- 對於Swing部件,不管是系統-觸發還是程序-觸發的請求,總會調用
paint()
方法;而update()
不再被Swing部件調用。 - 程序可以通過
repaint()
觸發一個異步的paint()
調用,但是不能直接調用paint()
。 - 對於複雜的界面,應該調用帶參數的
repaint()
,這樣可以僅僅更新由該參數定義的區域;而不要調用無參數的repaint()
,導致整個部件重畫。 - Swing中實現
paint()
的3個要素是調用3個分離的回調方法:paintComponent()
paintBorder()
paintChildren()
paintComponent()
方法的範圍之內。(不要放在paint()
裏面)。 - Swing引進了兩個屬性來最大化的改善繪畫的性能:
opaque
: 部件是否要重畫它所佔據範圍中的所有像素位?optimizedDrawingEnabled
: 是否有這個部件的子孫與之交疊?
- 如故Swing部件的
(遮光)opaque
屬性設置爲true
,那就表示它要負責繪製它所佔據的範圍內的所有像素位(包括在paintComponent()
中清除它自己的背景),否則會造成屏幕垃圾。 -
把一個部件設置爲
遮光(opaque)
同時又把它的optimizedDrawingEnabled
屬性設置爲false
,將導致在每個繪畫操作中要執行更多的處理,因此我們推薦的明智的方法是同時使用透明並且交疊部件。 -
使用UI代理(包括
JPanel
)的Swing部件的擴展類的典型作法是在它們自己的paintComponent()
的實現中調用super.paintComponent()
。因爲UI代理可以負責清除一個遮光部件的背景,這將照顧到#5. -
Swing通過
JComponent
的doubleBuffered
屬性支持內置的雙緩衝,所有的Swing部件該屬性默認值是true
,然而把Swing容器的遮光設置爲true
有一個整體的構思,把該容器上的所有輕量級子孫的屬性打開,不管它們各自的設定。 -
強烈建議爲所有的Swing部件使用雙緩衝。
-
界面複雜的部件應該靈活地運用剪切框來,只對那些與剪切框相交的區域進行繪畫操作,從而減少工作量。
總結
不管AWT還是Swing都提供了方便的編程手段使得部件內容能夠正確地顯示到屏幕上。雖然對於大多數的GUI需要我們推薦使用Swing,但是理解AWT的繪畫機制也會給我們帶來幫助,因爲Swing建立在它的基礎上。
關於AWT和Sing的特點就介紹到這裏,應用開發人員應該盡力按照本文中介紹的準則來撰寫代碼,充分發揮這些API功能,使自己的程序獲得最佳性能。