自定義布局管理器-FormLayout
第二部分:自定義布局管理器
在java.awt包與javax.swing包下有許多現成的布局類,比如BorderLayout、FlowLayout,還有較為復雜的、用于精確定位的布局類GridBagLayout、SpringLayout等。起初我剛剛從事gooey時(06年中),企圖依靠JDK自帶的布局類進行布局,但是實際不可能或者說很難做到。對于復雜的GridBagLayout、SpringLayout來說又望而生畏,而且使用GridBagLayout、SpringLayout來完成布局的話工作量相當可觀,因此當時放棄了布局管理器,采用ComponentListener等尺寸監聽事件來布局組件。雖然這種做法沒有工具支持、采用手工coding,但是自由度上升了很多,而且熟悉了以后編碼效率也大幅其高。與此同時,我開始接觸SWT,發現org.eclipse.swt.layout.FormLayout布局很強大、用起來愛不釋手、要多好有多好、要多強有多強......。于是當時我用來布局組件的方式是采用ComponentListener監聽與FormLayout結合的方式,也是在同期,我領悟到了九宮圖這種專業布局,因此之后九宮圖的實現也都采用上述兩種方法。隨著對SWT的不斷了解外加IM軟件界面的專業性,我發現SWT并不非常適合做專業外觀,也因為此我逐漸將精力轉向Swing。
在介紹如何編寫自定義布局管理器前,我想先把SWT體系下的FormLayout布局(表單布局)特點做個簡要介紹。
SWT體系下的FormLayout是非常靈活、精確的布局,FormLayout布局組件的特點是采用百分比+偏移量的方式。前者可以應付容器尺寸變化時內部組件隨之等比例調整;后者以應付精確的布局。這一特征是通過org.eclipse.swt.layout.FormData和org.eclipse.swt.layout.FormAttachment兩個類來實現。
通常使用FormLayout來定位一個組件要確定4個FormAttachment對象:top、bottom、left、right,即組件的4條邊。而且通常是使用FormAttachment(int numerator,int offset)這個構造器,也就是百分比+偏移量。當然FormAttachment不只這一種,但是都是可選的,如果想深入研究FormLayout可以參閱SWT相關的介紹。
下面給出一段SWT示例程序:
運行效果如下:

由運行效果可以看出,FormLayout通過指定組件的四條邊來完成布局。
FormLayout很強大、靈活,但是AWT、Swing包中卻沒有,但是不等于說不能實現,學習了上文之后當然可以移植到Swing中來。
SWT中使用FormLayout還要結合FormData(表單數據)與FormAttachment(表單附件)。下面給出這兩個移植過來的類實現
你應該了解坐標系的概念,Java中的坐標系以向右、向下為正方向。因此對于offset,正值是向右、向下偏移;負值是向左、向上偏移。
在FormLayout布局中,定位一個組件需要最多4個FormAttachment對象,但是可以不必全部指定,稍后可以看到缺省的行為。
如果你的布局管理器比較簡單,可以實現LayoutManager接口。但是正如上文所述,LayoutManager的addLayoutComponent(String name, Component comp)方法是必須通過java.awt.Container類的“Component add(String name, Component comp)”方法觸發調用,其中的字符串參數指定了布局信息。但是字符串表達方式很有限,因此應當采用LayoutManager2接口,這樣,addLayoutComponent(Component comp, Object constraints)方法被調用時,“Object constraints”可以是任何類型的對象,很方便。下面逐步實現這個類。
首先搭建的原型如下
public final class FormLayout implements LayoutManager2 {
public void addLayoutComponent(Component comp, Object constraints) {}
public float getLayoutAlignmentX(Container target) {
return 0;
}
public float getLayoutAlignmentY(Container target) {
return 0;
}
public void invalidateLayout(Container target) {}
public Dimension maximumLayoutSize(Container target) {
return null;
}
public void addLayoutComponent(String name, Component comp) {}
public void layoutContainer(Container parent) {}
public Dimension minimumLayoutSize(Container parent) {
return null;
}
public Dimension preferredLayoutSize(Container parent) {
return null;
}
public void removeLayoutComponent(Component comp) {}
}
再聲明一個保存組件與布局信息對應關系的映射:private final Map<Component, FormData> componentConstraints = new HashMap<Component, FormData>();
getContentPane().setLayout(new FormLayout());
JButton button = new JButton();
button.setText("button");
FormData formData = new FormData();
formData.top = new FormAttachment(0.2f, 0);
formData.left = new FormAttachment(0.5f, 0);
formData.bottom = new FormAttachment(0.2f, 30);
formData.right = new FormAttachment(0.5f, 50);
getContentPane().add(button,formData);
如上所示,當調用“getContentPane().add(button,formData);”時,布局類的 public void addLayoutComponent(Component comp, Object constraints)方法便會調用,constraints參數就是FormData對象。所以在addLayoutComponent方法中需要做的就是把組件與布局信息關聯起來。下面是完整實現:
}
對于addLayoutComponent(String name, Component comp)方法,由于通過查看源碼發現“實現了LayoutManager2 接口的布局類該方法永遠不會被調用”(未來的JDK版本如何實現不能保證),所以該方法空實現,并在注視上作@deprecated標記。
/**
* @deprecated
*/
public void addLayoutComponent(String name, Component comp) {}
除了layoutContainer方法,其余方法均很簡單。一并給出:
public float getLayoutAlignmentX(Container target) {
return 0.5f;
}
public float getLayoutAlignmentY(Container target) {
return 0.5f;
}
public void invalidateLayout(Container target) {}
public Dimension maximumLayoutSize(Container target) {
return new Dimension(Integer.MAX_VALUE, Integer.MAX_VALUE);
}
public Dimension minimumLayoutSize(Container target) {
return new Dimension(0, 0);
}
public Dimension preferredLayoutSize(Container target) {
return new Dimension(0, 0);
}
public void removeLayoutComponent(Component comp) {
synchronized (comp.getTreeLock()) {
componentConstraints.remove(comp);
}
}
根據上文所述,這些方法不難理解。其實對于FormLayout來說,...LayoutSize(Container target)、getLayoutAlignmentX等方法不是很重要。重要的是public void layoutContainer(Container target)的實現,也是所有布局類最重要的一個類。
首先該方法的第一步也要套上 synchronized (target.getTreeLock()) {},所有的代碼放入同步塊中。接下來是:
final FormData formData = componentConstraints.get(comp);
if (formData == null) {
continue;
}
因為在addLayoutComponent(Component comp, Object constraints)方法中已經關聯了組件與布局信息,所以可以通過componentMap.get(comp)這一行得到組件的布局信息,加上空值判斷確保代碼萬無一失。
接下來取出4個FormAttachment對象,表示組件的四條邊。
然后計算Location信息(組件左上角的坐標)x、y:
然后計算組件的長、高,width、height:
計算時根據給出right與bottom布局分為兩種情況,如果未給出,那么根據組件的getPreferredSize方法得到組件的最佳大小,以這個大小決定組件的尺寸。作為規范,使用布局管理器布局不是參照組件的getSize而是參照getPreferredSize來最終決定組件的尺寸,所有布局管理器也都是這么實現的。所以如果你企圖
設置組件的setSize()方法來達到在布局管理器中布局的目的是不可能的,所以你應該視圖調用組件的setPreferredSize方法。接上,如果right和bottom都不是null,那么計算組件尺寸將忽略getPreferredSize,計算x2和y2的坐標,然后兩坐標相減得到長寬。最后調用組件的setBounds進行最終定位。
作為FormLayout需要補充的是,在進行最終布局“component.setBounds(x, y, width, height);”之前,未進行邏輯判斷,所以x、y可能會超出了容器的范圍而width、height也可能是負值,這都會導致組件“莫名其妙”地不可見,這都不是布局管理器的問題。例如以下兩行代碼:
formData.left = new FormAttachment(0.5f, 30);
formData.right = new FormAttachment(0.5f, 20);
就會使組件永遠不能顯示,因為對于left的定位,是位于容器50%處向右30像素處,而right是位于容器50%處向右20像素處,這樣組件的長度就是-10,怎么能顯示出來呢?
FormLayout就介紹到這里,因為發帖只能在周末,加上最近一段時間還有別的事,能擠出一點時間真不容易。請關注下一篇CenterLayout的實現。
posted on 2007-11-24 18:26 sun_java_studio@yahoo.com.cn(電玩) 閱讀(16908) 評論(6) 編輯 收藏 所屬分類: NetBeans 、GUI Design