Swing:LookAndFeel 教程第一篇——手把手教你寫出自己的 LookAndFeel
本文是 LookAndFeel 系列教程的第一篇。
是我在對 Swing 學習摸索中的一些微薄經驗。
我相信,細致看全然系列之後。你就能寫出自己的 LookAndFeel。
你會發現 Swing 原來能夠這樣美。
--------------------------------------------------------------------------------
引言:
我第一次接觸 Java 要追溯到非常多年前做畢業設計的時候。
那天我和同學來到了一個微型軟件公司(三程序猿、一會計、一老總)。
第一次接觸到了面向對象,第一次接觸到了 Java,
非常奇妙的。在公司某技術大牛的幫助下完畢了一個 HelloWorld 之後,我便開始接觸 Swing。
這位技術大牛能夠說是我在 Java 之路上的領路人,我之後的代碼風格非常大程度上就是受他的影響。
不得不說,從那時起,我就和 Swing 結下了不解之緣。
盡管畢設做完之後許久沒有再碰 Java……
直到兩年前,工作轉型。從project實施轉為軟件開發,才又拾起了 Java,
第一個大項目就是 Swing 的項目,不得不說這就是緣分。
盡管闊別 Swing 多年,可是當年領路人的一句話我一直牢記於心:
“Swing 真正的精髓就是感觀(LookAndFeel),假設能開發感觀了,那才是真正的進入到 Swing 的核心領域。”
關於這一點。可能仁者見仁,智者見智。可是,它正是我指引我前進的航標。
所以,當公司的 Swing 項目提出了開發定制皮膚的需求時。我毫不猶豫的應下了這個差事。
這是何其巧妙與奇妙的緣分啊……
隨著項目的展開。隨著對 Swing 源代碼的深入解讀,LookAndFeel 的大門在我的面前漸漸清晰,並已在隱隱開啟。
終於,歷時兩個月,我成功的完畢了人生的第一套 LookAndFeel。
盡管外觀依舊不夠美觀,盡管功能還有不少缺陷,盡管代碼還是略顯幼稚,
可是門已打開,前方的道路一片光明……
所以今天寫這篇文。就是為了告訴大家一些我的經驗,告訴大家一個真正的 Swing。
我在開發這套 LookAndFeel 之前,盡管淺淺的接觸過一些 Swing,可是也談不上多深的基礎。
在不斷摸索中,走了不少歪路,但依舊還是在兩個月內完畢了開發,所以……
假設你有良好的 Swing 基礎。那你理解 LookAndFeel 的速度一定飛快;
假設你沒有哪怕一點點的 Swing 基礎,沒有關系。我帶領你從還有一個角度踏入 Swing 的領域;
假設你是不 Java 愛好者。那也無妨,Swing/LookAndFeel 優美的 MVC 模式會給你一個良好的編程思想。
就像這些年不斷冒出的修真小說上描寫敘述的那樣——
不管是修仙,修禪,還是修魔——都是修真。
道雖不同,但大道相通。
--------------------------------------------------------------------------------
題外話:在看本文之前,你最好已經安裝了一個 JDK。
由於安裝了 JDK 你才幹看到 Java 的源代碼。你才幹更好的理解本文。
JDK 安裝之後。在其安裝文件夾中有個 src.zip 壓縮包。裏面就放著 Java 源代碼。
作為 LookAndFeel 教程的第一篇,本文要說些什麽?
先列個提綱吧:
1、為什麽要用 Swing 而不用 AWT
2、什麽是 LookAndFeel
3、一個 Swing 控件是怎麽由 MVC 結構組成的
4、一個 Swing 控件是怎樣繪制——準確說是怎樣通過 UI 類來繪制的
5、一個 Swing 控件是怎樣獲得相應於自己的那個 UI 類對象的
這是第一篇的主要內容,也是 LookAndFeel 的核心部分,
看完這篇,你至少會知道什麽是 LookAndFeel。
盡管你可能臨時還不能寫出一個自己的 LookAndFeel。
可是。這就是開始。沿著這條路走下去,你肯定會成功。
以下開始正文:
--------------------------------------------------------------------------------
一、為什麽要用 Swing 而不用 AWT
Java 官方的 GUI 有兩套:AWT 以及 Swing。
在這裏。我們稍微探討一下這兩者之間的歷史關系,
可是不討論官方的 GUI 和一些非官方的 GUI (比如 SWT)之間孰優孰劣。
我們今天要說的重點是——Swing。
為什麽是 Swing 而不是 AWT 呢?
由於早在十多年前。Java 官方就發覺了 AWT 控件的缺陷——這種重量級方案想做到平臺一致性。難度太大。
不但界面難以美觀,為了保證平臺一致性。官方開發者不斷的去除 AWT 控件中可能會引起平臺差異性的特性。最後導致 AWT 控件的功能也異常薄弱。
在這個時候,官方大膽的拋棄了 AWT 控件,在 AWT 事件機制的基礎上推出了 Swing。
從此之後。官方僅僅對 Swing 進行更新維護,而停止了對 AWT 控件的更新維護。
也就是說,假設你如今還在學習 AWT 控件。那就等於是在學習一種被官方拋棄。並停止更新維護長達十年之久的技術。
Swing 是什麽?
Swing 是 Java 官方推出的,絕大部分控件都由 Graphics2D 繪制的一種輕量級 GUI 方案。其全部的輕量級控件都繼承自 JComponent 類。
須要註意的是,Swing 中依舊有三個重量級控件:JFrame。JDialog,JWindow。
只是它們都是窗口,他們都繼承自 Window 類。
而不管是 JComponent 還是 Window 它們都繼承自 Container 類,這事實上也就意味著:全部的 Swing 控件。都能夠做控件容器。
--------------------------------------------------------------------------------
二、什麽是 LookAndFeel
如今,我們直接切入重點:Swing 是怎樣通過 Graphics2D 繪制這些控件的呢?
答案就是 LookAndFeel 機制。
那什麽是 LookAndFeel 呢?
通俗的說,這就是皮膚;
從功能上說。這是一種批量管理 Swing 控件外觀的機制;
從根源來說,這是 Swing 的核心。
官方正式推出的 LookAndFeel 在 Java 7 版本號中已經添加到了 4 套。
如今,讓我們簡單的預覽一下它們的效果吧——
1、(官方)MetalLookAndFeel(Swing 默認的 LookAndFeel):
類路徑:javax.swing.plaf.metal.MetalLookAndFeel
跨平臺性:可跨平臺
2、(官方)WindowsLookAndFeel:
類路徑:com.sun.java.swing.plaf.windows.WindowsLookAndFeel
跨平臺性:限於 Windows 平臺
3、(官方)MotifLookAndFeel:
類路徑:com.sun.java.swing.plaf.motif.MotifLookAndFeel
跨平臺性:可跨平臺
4、(官方)NimbusLookAndFeel:
類路徑:javax.swing.plaf.nimbus.NimbusLookAndFeel
跨平臺性:可跨平臺
5、(本人任意之作)UltimaLookAndFeel:
跨平臺性:可跨平臺
6、最後看一下這個……
這是什麽?這就是 AWT……
事實上不是我不想給它加上 Tab 頁,實在是在 AWT 裏找不到 Tab 頁控件……
事實上也不是我不想給它加個標題邊框,實在是找不到設置 AWT 控件邊框的方法……
呵呵,看出為什麽 AWT 會被拋棄了麽?
話說 MetalLookAndFeel 作為 Swing 的默認 LookAndFeel 實在有些寒磣。
而官方在 Java 7 中正式推出的 NimbusLookAndFeel 則要美觀的多。
而我的 UltimaLookAndFeel 是以 MetalLookAndFeel 為基礎的一個優化版。
眼下有藍色、綠色、黑色三種風格。
各位看到這裏是不是有點激動了?原來 Swing 並非僅僅能像默認的 LookAndFeel 那樣寒磣。
別急,放心,看完我的這一系列文章,你就能寫出一個屬於自己的 LookAndFeel。想多美觀就能有多美觀,僅僅要你有好的美工基礎……
順便說一下。由於我個人無美工基礎。所以我的 UltimaLookAndFeel 外觀以簡潔為主。求美工……
你問怎樣換 LookAndFeel?
在啟動你的程序前:
UIManager.setLookAndFeel(…);
就可以假設是在程序已經啟動之後再換 LookAndFeel,那在上面那句之後,建議再加上:
SwingUtilities.updateComponentTreeUI(…);
--------------------------------------------------------------------------------
三、一個 Swing 控件是怎麽由 MVC 結構組成的
說到 LookAndFeel 又不得不提一下 Swing 優美的 MVC 模式。
所謂 MVC 就是:模型(Model)、視圖(View)、控制器(Controller)這種一種結構。
Swing 中,差點兒全部的控件都能夠清晰的分解成這三大部分。
就拿 JButton 來舉例,我們能夠這樣分解:
JButton——控制器;
ButtonModel——模型。其最常見的詳細實現類是:DefaultButtonModel。
ButtonUI——視圖,其最常見的詳細實現類是:MetalButtonUI;
JButton 負責控制,ButtonModel 提供模型,而 ButtonUI 實現展示。
也就是說,基本上全部的 Swing 控件都是由一個 Control 類、一個 Model 類、一個 UI 類組成的。
部分過於簡單和數據無關的控件無 Model 類,比如 JPanel……
全部的 Control 類你都非常熟悉。
大部分 Model 類你也非常熟悉。
而 UI 類。你可能不是非常熟悉。
沒關系。今天之後,你將熟悉它們!
--------------------------------------------------------------------------------
四、一個 Swing 控件是怎樣繪制——準確說是怎樣通過 UI 類來繪制的
那 Swing 是怎樣通過 UI 類來繪制控件的呢?
要說清晰這個問題。我們先來說一下 Swing 控件在 UI 線程中的繪制過程:
不管是 repaint 還是什麽,在繪制控件時。終於都會產生一個 PaintEvent 然後排入 UI 線程的事件處理序列。
而 UI 線程在處理 Swing 控件的 PaintEvent 時。終於都會調用到控件的 paint 方法。
所以我們如今看一下 JComponent 的 paint 方法是怎麽寫的:
public void paint(Graphics g) { //…… if(!rectangleIsObscured(clipX,clipY,clipW,clipH)) { if (!printing) { paintComponent(co); paintBorder(co); } else { printComponent(co); printBorder(co); } } if (!printing) { paintChildren(co); } else { printChildren(co); } //…… }
略去了非常多,但那不是重點,重點是在 paint 方法中調用了 paintComponent 方法。
好,再看看 paintComponent 方法:
protected void paintComponent(Graphics g) { if (ui != null) { Graphics scratchGraphics = (g == null) ? null : g.create(); try { ui.update(scratchGraphics, this); } finally { scratchGraphics.dispose(); } } }
我們看到了什麽?抓重點,那就是:
ui.update(scratchGraphics, this);
這個 ui 是什麽?看 JComponent 中的聲明:
protected transient ComponentUI ui;
原來是個 ComponentUI,順便說一下,這個 ComponentUI 就是全部 UI 類的祖先類。
看這些是為了幹什麽?
OK,我們來總結一下:
一個控件要繪制。就必定調用到它的 paint 方法;
而默認的 paint 方法中會調用到 paintComponent 方法;
paintComponent 方法中又會調用 UI 類的 update 方法;
也就是說。一個控件的繪制,和其 UI 類中的 update 方法是息息相關的。
看,UI 類成功的和控件繪制關聯上了。
好的。這個問題攻克了,我們看下一個問題吧。
--------------------------------------------------------------------------------
五、一個 Swing 控件是怎樣獲得相應於自己的那個 UI 類對象的
各個控件的 UI 類又是怎麽被設置到 Control 類中的呢?
為了說明這個問題。我們再來看個樣例,這次拿 JPanel 來開刀:
我們要看的是它的構造方法,不管我們怎麽構造一個 JPanel,在其內部終於都是調用的這種方法:
public JPanel(LayoutManager layout, boolean isDoubleBuffered){ setLayout(layout); setDoubleBuffered(isDoubleBuffered); setUIProperty("opaque", Boolean.TRUE); updateUI(); }
重點是最後一句:
updateUI();
事實上假設你去細致研究各個 Swing 控件的構造方法的源代碼。會發現其終於都會調用到 updateUI 這種方法
所以如今重點轉移了,來看 updateUI 方法:
public void updateUI() { setUI((PanelUI)UIManager.getUI(this)); }
setUI 的運行過程我就不再累述,有興趣的能夠自己看源代碼,總之最後,它會對 ui 對象賦值。
這裏出現了一個非常重要的類——UIManager。這是什麽呢?
在 LookAndFeel 機制中,會有大量的鍵值對存放在一個 UIDefaults(事實上就是個 HashTable)中。
這些鍵值對記錄了控件的邊框、各種部分的顏色、字體等等,當中也包含了這個控件相應的 UI 類的類名。
而 UIManager 就是方便我們調用或替換這些鍵值對的一個管理工具類。
UIManager.getUI(this) 又是怎樣返回一個 UI 類的對象呢?
我們來看看:
public static ComponentUI getUI(JComponent target) { maybeInitialize(); ComponentUI ui = null; LookAndFeel multiLAF = getLAFState().multiLookAndFeel; if (multiLAF != null) { ui =multiLAF.getDefaults().getUI(target); } if (ui == null) { ui = getDefaults().getUI(target); } return ui; }
如今大家應該都學會抓重點了吧?
重點是:
getDefaults().getUI(target);
它又是怎麽運行的呢?
public ComponentUI getUI(JComponent target) { Object cl = get("ClassLoader"); ClassLoader uiClassLoader = (cl != null) ?(ClassLoader)cl :target.getClass().getClassLoader(); Class<? extends ComponentUI> uiClass = getUIClass(target.getUIClassID(), uiClassLoader); Object uiObject = null; //……略去反射部分的源代碼 return (ComponentUI)uiObject; }
看到這句代碼沒:
getUIClass(target.getUIClassID(), uiClassLoader);
原來是通過控件類中的 getUIClassID 返回的“鍵”。來獲得 UI 類的類名在 UIDefaults 中的“值”。然後反射生成 UI 類的對象。
看一下 JPanel 中的 getUIClassID 方法:
private static final String uiClassID = "PanelUI"; public String getUIClassID() { return uiClassID; }
又到一段總結時……
在控件構造時。都會去調用 updateUI 方法。
在控件的 updateUI 方法中。會通過 UIManager 去獲取 ui 對象。
而 UIManager 去獲取 ui 對象時,是通過控件的 uiClassID 這個“鍵”去獲得 UIDefaults 中的相應的“值”。
而最後依據返回的類名,反射生成一個 UI 類的對象。返回給 updateUI 方法。
再通過 setUI 方法賦值給 ui 成員變量。
--------------------------------------------------------------------------------
好的。第一篇 LookAndFeel 教程就快結束了。
我們最後來看一下 LookAndFeel 類中的一個方法,你會明確非常多事。
MetalLookAndFeel 類,initClassDefaults 方法:
protected void initClassDefaults(UIDefaults table) { super.initClassDefaults(table); final String metalPackageName = "javax.swing.plaf.metal."; Object[] uiDefaults = { "ButtonUI", metalPackageName+ "MetalButtonUI", "CheckBoxUI", metalPackageName+ "MetalCheckBoxUI", "ComboBoxUI", metalPackageName + "MetalComboBoxUI", "DesktopIconUI", metalPackageName + "MetalDesktopIconUI", "FileChooserUI", metalPackageName + "MetalFileChooserUI", "InternalFrameUI", metalPackageName + "MetalInternalFrameUI", "LabelUI", metalPackageName + "MetalLabelUI", "PopupMenuSeparatorUI", metalPackageName + "MetalPopupMenuSeparatorUI", "ProgressBarUI", metalPackageName + "MetalProgressBarUI", "RadioButtonUI", metalPackageName + "MetalRadioButtonUI", "ScrollBarUI", metalPackageName + "MetalScrollBarUI", "ScrollPaneUI", metalPackageName + "MetalScrollPaneUI", "SeparatorUI", metalPackageName + "MetalSeparatorUI", "SliderUI", metalPackageName + "MetalSliderUI", "SplitPaneUI", metalPackageName + "MetalSplitPaneUI", "TabbedPaneUI", metalPackageName + "MetalTabbedPaneUI", "TextFieldUI", metalPackageName + "MetalTextFieldUI", "ToggleButtonUI", metalPackageName + "MetalToggleButtonUI", "ToolBarUI", metalPackageName + "MetalToolBarUI", "ToolTipUI", metalPackageName + "MetalToolTipUI", "TreeUI", metalPackageName + "MetalTreeUI", "RootPaneUI", metalPackageName + "MetalRootPaneUI", }; table.putDefaults(uiDefaults); }
如今你明確了麽?
"ButtonUI" 就是那個“鍵”;
"javax.swing.plaf.metal.MetalButtonUI" 就是那個“值”;
原來在 updateUI 中,獲得 ui 對象時,用到的那個鍵值對關系,就是在這裏相應上的。
所以,假設你打算自己寫一套 LookAndFeel,當你寫了一個 UI 類之後應該怎麽和控件相應上呢?
答案就是改寫 LookAndFeel 類中的 initClassDefaults 方法。
第一篇。就到這裏了~
to be continue...
Swing:LookAndFeel 教程第一篇——手把手教你寫出自己的 LookAndFeel