布局管理器面面觀
本系列文章將系統地介紹在AWT-Swing組件體系下如何使用布局管理器,從概念開始并結合JDK1.6 API源代碼講述布局管理器工作原理,然后介紹如何自定義布局管理器并給出2個自定義的實現——FormLayout、CenterLayout,同時還將介紹如何使用絕對定位解決布局問題,最后以通過xml配置文件聲明及布局組件結束本文。
本文包括如下部分:
一、布局管理器簡介與工作原理
二、如何編寫自定義布局管理器
三、FormLayout實現
四、CenterLayout實現
五、如何使用絕對定位解決布局問題
六、通過xml配置文件定義及布局組件
第一部分:布局管理器簡介與工作原理
布局管理器是一個實現了LayoutManager接口或LayoutManager2接口并且能夠確定一個容器內部所有組件大小和位置的對象。盡管組件能夠提供大小和對齊的提示信息,但是一個容器的布局管理器將最終決定組件的尺寸和位置。
布局管理器的工作原理
基本的布局管理器要實現LayoutManager接口。LayoutManager接口聲明了5個基本方法:
void addLayoutComponent(String name, Component comp)
void layoutContainer(Container parent)
Dimension minimumLayoutSize(Container parent)
Dimension preferredLayoutSize(Container parent)
void removeLayoutComponent(Component comp)
LayoutManager2接口在LayoutManager接口之上添加了4個方法:
void addLayoutComponent(Component comp, Object constraints)
float getLayoutAlignmentX(Container target)
float getLayoutAlignmentY(Container target)
void invalidateLayout(Container target)
Dimension maximumLayoutSize(Container target)
以上方法是構成布局管理器的所有方法,只有當容器添加了布局管理器時,這些方法才可能被調用到。下面一一講述這些方法的調用時機。
“void addLayoutComponent(String name, Component comp)”和“void addLayoutComponent(Component comp, Object constraints)”兩個方法是當向容器內添加組件時候可能被調用。具體調用那個由add方法的參數決定。
Javadoc中是這么說明的:對于前者的注解是“如果布局管理器使用每組件字符串,則將組件 comp 添加到布局,并將它與 name 指定的字符串關聯。”;后者則是“使用指定約束對象,將指定組件添加到布局。”
例如,使用java.awt.Container類的“Component add(String name, Component comp)”方法添加組件comp時候,如果該容器(container)設置了布局管理器,那么該布局管理器的“void addLayoutComponent(String name, Component comp)”方法將被調用;使用java.awt.Container類的
“void add(Component comp, Object constraints)”方法添加組件時,該容器的布局管理器(如果有且實現了LayoutManager2接口)的“void addLayoutComponent(Component comp, Object constraints)”將被調用。例如下面這行代碼:
....add(new JButton(),BorderLayout.CENTER);
就會調用布局管理器的void add(Component comp, Object constraints)。如果你查看java.awt.BorderLayout的源碼,會發現BorderLayout實現的是LayoutManager2接口。
我們看一下JDK源碼是怎樣的調用關系。記住,讀源碼是學習開源技術最徹底的方法。
在java.awt.Container的所有add(...)方法中,都是最終調用“protected void addImpl(Component comp, Object constraints, int index)”這個實現,add方法的參數不同,調用addImpl時候傳入的參數也不同。例如,Component add(String name, Component comp)方法的實現是這樣的:
public Component add(String name, Component comp) {
addImpl(comp, name, -1);
return comp;
}
void add(Component comp, Object constraints)方法的實現是這樣的:
public void add(Component comp, Object constraints) {
addImpl(comp, constraints, -1);
}
“addImpl”方法實現很長,不可能全部給出,但是有一段對分析布局管理器有幫助:
protected void addImpl(Component comp, Object constraints, int index) {
......
/* Notify the layout manager of the added component. */
if (layoutMgr != null) {
if (layoutMgr instanceof LayoutManager2) {
((LayoutManager2)layoutMgr).addLayoutComponent(comp, constraints);
} else if (constraints instanceof String) {
layoutMgr.addLayoutComponent((String)constraints, comp);
}
}
......
}
如果這個容器設置了布局管理器(layoutMgr != null),那么檢查layoutMgr是否實現的是LayoutManager2接口,如果是就調用布局管理器的“void addLayoutComponent(Component comp, Object constraints)”方法,否則(實現的是LayoutManager接口)再判斷constraints是否是String類型,如果是就調用布局管理器的“void addLayoutComponent(String name, Component comp)”方法。
到此為止,布局管理器的“void addLayoutComponent(String name, Component comp)”和“void addLayoutComponent(Component comp, Object constraints)”兩個方法調用時機已經非常明了了,同時我們還了解了一點,那就是如果布局管理器實現的是LayoutManager2接口,那么它的“void addLayoutComponent(String name, Component comp)”永遠不會被awt框架調用到,除非你顯示地調用。
LayoutManager接口的“void removeLayoutComponent(Component comp)”方法,是在容器移除子組件時候被調用。打開JDK源代碼,java.awt.Container的移除組件的方法實現如下:
public void remove(Component comp) {
synchronized (getTreeLock()) {
if (comp.parent == this) {
/* Search backwards, expect that more recent additions are more likely to be removed. */
Component component[] = this.component;
for (int i = ncomponents; --i >= 0; ) {
if (component[i] == comp) {
remove(i);
}
}
}
}
}
可以看出,每個添加到容器的組件都被保存在component[]中,刪除組件時會遍歷數組,發現被刪除的組件調用public void remove(int index)執行刪除。在remove(int index)方法中同樣有我們關注的調用。
public void remove(int index) {
......
if (layoutMgr != null) {
layoutMgr.removeLayoutComponent(comp);
}
......
}
可見,組件從父容器移除過程中會調用布局管理器(如果設置了布局管理器)的“void removeLayoutComponent(Component comp)”方法。
下一步一并介紹“Dimension minimumLayoutSize(Container parent)”、“Dimension preferredLayoutSize(Container parent)”、“Dimension maximumLayoutSize(Container target)”、“float getLayoutAlignmentX(Container target)”、“float getLayoutAlignmentY(Container target)”這5個方法。
有時候,需要自定義一個組件為它的容器布局管理器提供關于大小的提示信息,通過指定組件的最小、首選、最大大小維數可以提供大小的提示信息。可以調用組件的方法來設置大小提示信息——setMinimumSize、setPreferredSize、setMaximumSize,或者重寫其對應的get...Size方法同樣可以實現。注意setSize(Dimension d)與set...Size(Dimension preferredSize)是不一樣的,前者能最終確定組件大小,但是只能用在絕對布局(不設置任何布局管理器)的情況下;后者是給該組件大小提供關于大小的提示信息,是給布局管理器看的。但是提示畢竟是提示,最終決定組件大小還是布局管理器決定,提示信息只能算是參考。但是話說回來,布局管理器應該嚴格按照組件的尺寸提示信息行事,例如不應該把組件的尺寸設置成小于它的提示最小尺寸等。有時候preferredSize屬性會比size更重要,因為組件框架內部通常考慮組件的首選尺寸而不是實際尺寸的值。例如要實現JTree不同結點有不同的高度(QQ上被選中的好友節點會加大尺寸顯示),就可以重寫DefaultTreeCellRenderer的getPreferredSize實現。
除了提供大小提示信息以外,還可以提供對齊提示。例如,兩個組件的上邊界對齊。可以通過調用組件的setAlignmentX和setAlignmentY方法,或重寫對應的get方法來設置對齊提示,但是大多數布局管理器會忽略該提示。為了簡單起見,只給出preferredLayoutSize的調用源代碼,其余方法調用時機相似。java.awt.Container類的getPreferredSize方法定義如下:
public Dimension getPreferredSize() {
return preferredSize();
}
@Deprecated
public Dimension preferredSize() {
/* Avoid grabbing the lock if a reasonable cached size value is available. */
Dimension dim = prefSize;
if (dim == null || !(isPreferredSizeSet() || isValid())) {
synchronized (getTreeLock()) {
prefSize = (layoutMgr != null) ? layoutMgr.preferredLayoutSize(this) : super.preferredSize();
dim = prefSize;
}
}
if (dim != null) {
return new Dimension(dim);
} else{
return dim;
}
}
由此可以看到在preferredSize中調用到了layoutMgr.preferredLayoutSize(this),參數就是當前Container的實例。
LayoutManager2接口的“void invalidateLayout(Container target)”方法,在JavaDoc的注釋為“使布局失效,指示如果布局管理器緩存了信息,則應該將其丟棄。”,讓我們結合JDK源碼看看該方法何時被調用。在java.awt.Container類中,invalidate方法定義如下:
public void invalidate() {
LayoutManager layoutMgr = this.layoutMgr;
if (layoutMgr instanceof LayoutManager2) {
LayoutManager2 lm = (LayoutManager2) layoutMgr;
lm.invalidateLayout(this);
}
super.invalidate();
}
如果在此容器上安裝的 LayoutManager 是一個 LayoutManager2 實例,則在該實例上調用 LayoutManager2.invalidateLayout(Container),并提供此 Container 作為參數”。這個函數在JavaDoc中的注解為:“使容器失效。該容器及其之上的所有父容器被標記為需要重新布置。此方法經常被調用,所以內部實現必須簡潔。
我們在順便看看“super.invalidate();”是如何實現的,java.awt.Container的基類是java.awt.Component,其invalidate方法實現如下:
public void invalidate() {
synchronized (getTreeLock()) {
/* Nullify cached layout and size information.
* For efficiency, propagate invalidate() upwards only if
* some other component hasn't already done so first.
*/
valid = false;
if (!isPreferredSizeSet()) {
prefSize = null;
}
if (!isMinimumSizeSet()) {
minSize = null;
}
if (!isMaximumSizeSet()) {
maxSize = null;
}
if (parent != null && parent.valid) {
parent.invalidate();
}
}
}
在java.awt.Component類的invalidate實現中,把prefSize 、minSize 、maxSize這3個提示屬性給清空(如果大小提示是通過重寫get...Size強制為特定常量或自定義計算規則,那么上述清空操作可能對你沒有實際意義),并且延著層次關系發送到父組件。因為swing組件的基類是javax.swing.JComponent,繼承層次關系是
java.lang.Object
java.awt.Component
java.awt.Container
javax.swing.JComponent
所以對于所有swing組件來說,如果不重寫invalidate方法,都會是這樣的調用行為。
那么LayoutManager2接口的實現中“void invalidateLayout(Container target)”方法中應該做些什么?其實有些布局管理器的實現中是忽略的,例如java.awt.BorderLayout。
正如JavaDoc所說的那樣“使布局失效,指示如果布局管理器緩存了信息,則應該將其丟棄。”,應該按照JavaDoc要求的那樣去做就行了。例如java.awt.BoxLayout布局的實現:
public synchronized void invalidateLayout(Container target) {
checkContainer(target);
xChildren = null;
yChildren = null;
xTotal = null;
yTotal = null;
}
但是也必須警惕,LayoutManager2接口的invalidateLayout(Container target)方法調用也很頻繁,當組件尺寸改變時,該方法就會被調用,因此釋放緩存信息時要小心。
對于布局管理器來說,最重要的方法莫過于“void layoutContainer(Container parent)”。因為組件的最終布局都是在該方法中實現的。這個方法在很多情況下都會被awt-swing框架自動調用,例如改變組件的字體、容器尺寸改變等都會觸發該方法的調用。布局管理器的layoutContainer方法并不會真正繪制組件,它只是調用每個組件的setSize、setLocation、setBounds方法來設置組件的大小和位置。對于自定義組件來說,可以調用revalidate強制實現,或者調用容器的doLayout也可以強制實現。當調用一個組件的revalidate方法時,一個請求將通過包含層次關系發送到第一個容器,容器的大小會不會被容器的大小調整而影響通過調用容器的isValidateRoot方法來確定。然后容器被重新布局。
如果你直接調用容器的doLayout,可以達到強制布局的效果。JDK源代碼中java.awt.Container的doLayout實現如下:
public void doLayout() {
layout();
}
@Deprecated
public void layout() {
LayoutManager layoutMgr = this.layoutMgr;
if (layoutMgr != null) {
layoutMgr.layoutContainer(this);
}
}
可見doLayout方法是直接調用布局管理器的layoutContainer方法。
此外再給出java.awt.Container的validate方法實現代碼:
public void validate() {
/* Avoid grabbing lock unless really necessary. */
if (!valid) {
boolean updateCur = false;
synchronized (getTreeLock()) {
if (!valid && peer != null) {
ContainerPeer p = null;
if (peer instanceof ContainerPeer) {
p = (ContainerPeer) peer;
}
if (p != null) {
p.beginValidate();
}
validateTree();
valid = true;
if (p != null) {
p.endValidate();
updateCur = isVisible();
}
}
}
if (updateCur) {
updateCursorImmediately();
}
}
}
注意“ validateTree();”方法,再給出 validateTree()方法實現:
protected void validateTree() {
if (!valid) {
if (peer instanceof ContainerPeer) {
((ContainerPeer)peer).beginLayout();
}
doLayout();
Component component[] = this.component;
for (int i = 0 ; i < ncomponents ; ++i) {
Component comp = component[i];
if ((comp instanceof Container) && !(comp instanceof Window) && !comp.valid) {
((Container)comp).validateTree();
} else {
comp.validate();
}
}
if (peer instanceof ContainerPeer) {
((ContainerPeer)peer).endLayout();
}
}
valid = true;
}
可以看出在validateTree方法執行過程中調用了“doLayout();”方法。也就是說會調用到LayoutManager接口的void layoutContainer(Container parent)方法。
再給出javax.swing.JComponent類setFont方法實現:
public void setFont(Font font) {
Font oldFont = getFont();
super.setFont(font);
// font already bound in AWT1.2
if (font != oldFont) {
revalidate();
repaint();
}
}
因為字體的改變會影響到組件的尺寸,因此也涉及到布局。如果你查看JDK API相關源碼,就會發現很多情況下“revalidate();”、“ repaint();”兩個方法是一起被先后調用的。這兩個方法都是線程安全的,不需要在事件分發線程中調用它們。
layoutContainer(Container parent)在很多地方都會被調用的。因此可以這樣理解:凡是能影響組件尺寸改變的條件都可能觸發該方法的調用。那么在layoutContainer中需要做的就是,根據收集到的組件提示信息、約束條件、容器的內部邊框、組件的可見性及布局規則等因素對組件進行最終定位。
到此為止,有關布局管理器的整體介紹和工作原理就告一段落。學習布局管理器的最終目的是學會如何自定義布局管理器,好,準備進入下一部分的學習,但是之前最好要把上面講述的消化一遍,尤其是接口方法的調用時機,這將是自定義布局管理器的基礎。
由于平時比較緊,文章基本是周末空閑時間寫,而且目前的工作方向不再是gooey了,所以寫一篇帖很不容易,準備一篇好貼更難。布局管理器這塊本人一直想發表下自己的觀點,敬請關注。
posted on 2007-11-18 15:15 sun_java_studio@yahoo.com.cn(電玩) 閱讀(10287) 評論(4) 編輯 收藏 所屬分類: NetBeans 、Swing