Java桌面技術(shù)

          Java Desktop Technology

          常用鏈接

          統(tǒng)計(jì)

          友情連接

          最新評論

          自定義布局管理器-FormLayout

                                                 第二部分:自定義布局管理器
          在java.awt包與javax.swing包下有許多現(xiàn)成的布局類,比如BorderLayout、FlowLayout,還有較為復(fù)雜的、用于精確定位的布局類GridBagLayout、SpringLayout等。起初我剛剛從事gooey時(shí)(06年中),企圖依靠JDK自帶的布局類進(jìn)行布局,但是實(shí)際不可能或者說很難做到。對于復(fù)雜的GridBagLayout、SpringLayout來說又望而生畏,而且使用GridBagLayout、SpringLayout來完成布局的話工作量相當(dāng)可觀,因此當(dāng)時(shí)放棄了布局管理器,采用ComponentListener等尺寸監(jiān)聽事件來布局組件。雖然這種做法沒有工具支持、采用手工coding,但是自由度上升了很多,而且熟悉了以后編碼效率也大幅其高。與此同時(shí),我開始接觸SWT,發(fā)現(xiàn)org.eclipse.swt.layout.FormLayout布局很強(qiáng)大、用起來愛不釋手、要多好有多好、要多強(qiáng)有多強(qiáng)......。于是當(dāng)時(shí)我用來布局組件的方式是采用ComponentListener監(jiān)聽與FormLayout結(jié)合的方式,也是在同期,我領(lǐng)悟到了九宮圖這種專業(yè)布局,因此之后九宮圖的實(shí)現(xiàn)也都采用上述兩種方法。隨著對SWT的不斷了解外加IM軟件界面的專業(yè)性,我發(fā)現(xiàn)SWT并不非常適合做專業(yè)外觀,也因?yàn)榇宋抑饾u將精力轉(zhuǎn)向Swing。

          在介紹如何編寫自定義布局管理器前,我想先把SWT體系下的FormLayout布局(表單布局)特點(diǎn)做個(gè)簡要介紹。
          SWT體系下的FormLayout是非常靈活、精確的布局,F(xiàn)ormLayout布局組件的特點(diǎn)是采用百分比+偏移量的方式。前者可以應(yīng)付容器尺寸變化時(shí)內(nèi)部組件隨之等比例調(diào)整;后者以應(yīng)付精確的布局。這一特征是通過org.eclipse.swt.layout.FormData和org.eclipse.swt.layout.FormAttachment兩個(gè)類來實(shí)現(xiàn)。
          通常使用FormLayout來定位一個(gè)組件要確定4個(gè)FormAttachment對象:top、bottom、left、right,即組件的4條邊。而且通常是使用FormAttachment(int numerator,int offset)這個(gè)構(gòu)造器,也就是百分比+偏移量。當(dāng)然FormAttachment不只這一種,但是都是可選的,如果想深入研究FormLayout可以參閱SWT相關(guān)的介紹。
          下面給出一段SWT示例程序:

          public static void main(String[] args) {
          Display display = new Display();
          Shell shell = new Shell(display);
          shell.setText("SWT Application");
          shell.setLayout(new FormLayout());
          final Button button = new Button(shell, SWT.NONE);
          button.setText("button");
          final FormData formData = new FormData();
          formData.top = new FormAttachment(20, 0);
          formData.left = new FormAttachment(50, 0);
          formData.bottom = new FormAttachment(20, 30);
          formData.right = new FormAttachment(50, 50);
          button.setLayoutData(formData);
          shell.open();
          while (!shell.isDisposed()) {
          if (!display.readAndDispatch()) {
          display.sleep();
          }
          }
          display.dispose();
          }
          運(yùn)行效果如下:

          由運(yùn)行效果可以看出,F(xiàn)ormLayout通過指定組件的四條邊來完成布局。
          FormLayout很強(qiáng)大、靈活,但是AWT、Swing包中卻沒有,但是不等于說不能實(shí)現(xiàn),學(xué)習(xí)了上文之后當(dāng)然可以移植到Swing中來。

          SWT中使用FormLayout還要結(jié)合FormData(表單數(shù)據(jù))與FormAttachment(表單附件)。下面給出這兩個(gè)移植過來的類實(shí)現(xiàn)

           

          public final class FormAttachment {
              float percentage; // 這個(gè)參數(shù)與SWT中的不同,不叫numerator,而是其等價(jià)的小數(shù)形式
              int offset;
              public FormAttachment(float percentage, int offset) {
                  this.percentage = percentage;
                  this.offset = offset;
              }
          }

          public final class FormData {
              public FormAttachment left;
              public FormAttachment right;
              public FormAttachment top;
              public FormAttachment bottom;
          }


          你應(yīng)該了解坐標(biāo)系的概念,Java中的坐標(biāo)系以向右、向下為正方向。因此對于offset,正值是向右、向下偏移;負(fù)值是向左、向上偏移。

          與SWT的FormAttachment稍有不同的是,我自定義的構(gòu)造器第一個(gè)參數(shù)是float類型,它代表的意思與“FormAttachment(int numerator,int offset)”相同,都是表示百分比,只不過前者用整數(shù)表示,后者用小數(shù)表示。例如SWT中“new FormAttachment(20,0);”用后者表示就是“new FormAttachment(0.2f,0);”。
          在FormLayout布局中,定位一個(gè)組件需要最多4個(gè)FormAttachment對象,但是可以不必全部指定,稍后可以看到缺省的行為。
          如果你的布局管理器比較簡單,可以實(shí)現(xiàn)LayoutManager接口。但是正如上文所述,LayoutManager的addLayoutComponent(String name, Component comp)方法是必須通過java.awt.Container類的“Component add(String name, Component comp)”方法觸發(fā)調(diào)用,其中的字符串參數(shù)指定了布局信息。但是字符串表達(dá)方式很有限,因此應(yīng)當(dāng)采用LayoutManager2接口,這樣,addLayoutComponent(Component comp, Object constraints)方法被調(diào)用時(shí),“Object constraints”可以是任何類型的對象,很方便。下面逐步實(shí)現(xiàn)這個(gè)類。
          首先搭建的原型如下

          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) {}

          }
          再聲明一個(gè)保存組件與布局信息對應(yīng)關(guān)系的映射:private final Map<Component, FormData> componentConstraints = new HashMap<Component, FormData>();

          接著完成addLayoutComponent方法的實(shí)現(xiàn)。在完成編寫之前我們看一下怎樣去使用FormLayout以做到心中有數(shù)。下面的一段代碼是調(diào)用FormLayout示例:
          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);

          如上所示,當(dāng)調(diào)用“getContentPane().add(button,formData);”時(shí),布局類的 public void addLayoutComponent(Component comp, Object constraints)方法便會(huì)調(diào)用,constraints參數(shù)就是FormData對象。所以在addLayoutComponent方法中需要做的就是把組件與布局信息關(guān)聯(lián)起來。下面是完整實(shí)現(xiàn):
          public void addLayoutComponent(Component comp, Object constraints) {
              if (constraints == null) {
                  throw new IllegalArgumentException("constraints can't be null");
              } else if (!(constraints instanceof FormData)) {
                  throw new IllegalArgumentException("constraints must be a " + FormData.class.getName() + " instance");
              } else {
                  synchronized (comp.getTreeLock()) {
                      FormData formData = (FormData) constraints;
                      if (formData.left == null || formData.top == null) {
                          throw new IllegalArgumentException("left FormAttachment and top FormAttachment can't be null");
                      }
                       componentConstraints.put(comp, (FormData) constraints);
                  }
              }
          }
          前面的合法性檢查是必需的,你懂的。然后比較重要的就是“synchronized (comp.getTreeLock()) ”,這是保障在多線程的環(huán)境下能安全執(zhí)行,如果你察看JDK源碼布局類的實(shí)現(xiàn),會(huì)發(fā)現(xiàn)這個(gè)同步多次用到,我這么用也是參考JDK的實(shí)現(xiàn)。關(guān)于getTreeLock的實(shí)現(xiàn)在JDK6.0源碼中是這樣實(shí)現(xiàn)的。
          public abstract class Component implements ImageObserver, MenuContainer, Serializable {
          ...
              static final Object LOCK = new AWTTreeLock();
              static class AWTTreeLock {}
          ...
              public final Object getTreeLock() {
                  return LOCK;
              }
          ...
          }
          還要注意的是傳入的FormData實(shí)例的left、top FormAttachment必須要給出,因?yàn)檫@兩個(gè)FormAttachment代表的是Location(位置)信息,因此必須指定。對于right、bottom可以不指定,但是如果不指定的話,必須能從getPreferredSize()中得到信息,否則組件的尺寸將無法確定。

          對于addLayoutComponent(String name, Component comp)方法,由于通過查看源碼發(fā)現(xiàn)“實(shí)現(xiàn)了LayoutManager2 接口的布局類該方法永遠(yuǎn)不會(huì)被調(diào)用”(未來的JDK版本如何實(shí)現(xiàn)不能保證),所以該方法空實(shí)現(xiàn),并在注視上作@deprecated標(biāo)記。
              /**
               * @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);
                  }
          }
          根據(jù)上文所述,這些方法不難理解。其實(shí)對于FormLayout來說,...LayoutSize(Container target)、getLayoutAlignmentX等方法不是很重要。重要的是public void layoutContainer(Container target)的實(shí)現(xiàn),也是所有布局類最重要的一個(gè)類。
          首先該方法的第一步也要套上 synchronized (target.getTreeLock()) {},所有的代碼放入同步塊中。接下來是:
          final int w = parent.getWidth();
          final int h = parent.getHeight();
          final Component[] components = parent.getComponents();
          for (final Component comp : components) {}
          不難理解,是要首先獲取容器當(dāng)前的長、高,然后遍歷容器內(nèi)的所有組件逐一進(jìn)行布局。下面的工作就是在for循環(huán)體中做文章了。循環(huán)體第一段是
          final FormData formData = componentConstraints.get(comp);
          if (formData == null) {
              continue;
          }
          因?yàn)樵赼ddLayoutComponent(Component comp, Object constraints)方法中已經(jīng)關(guān)聯(lián)了組件與布局信息,所以可以通過componentMap.get(comp)這一行得到組件的布局信息,加上空值判斷確保代碼萬無一失。
          接下來取出4個(gè)FormAttachment對象,表示組件的四條邊。
          final FormAttachment left = formData.left;
          final FormAttachment right = formData.right;
          final FormAttachment top = formData.top;
          final FormAttachment bottom = formData.bottom;
          然后計(jì)算Location信息(組件左上角的坐標(biāo))x、y:
          final int x = (int) (left.percentage * w) + left.offset;     // 左邊的x坐標(biāo)
          final int y = (int) (top.percentage * h) + top.offset;     // 上邊的y坐標(biāo)
          計(jì)算方法就是FormLayout的布局方式:(百分比*容器尺寸)+像素偏移量。

          然后計(jì)算組件的長、高,width、height:

          final int width;
          final int height;
          if (right == null || bottom == null) {
              final Dimension size = comp.getPreferredSize();
              if (size == null) {
                  throw new RuntimeException("If right FormAttachment or bottom FormAttachment is null,the component must have preferred-size");
              } else {
                  width = size.width;
                  height = size.height;
              }
          } else {
              final int x2 = (int) (right.percentage * w) + right.offset;          // 右邊的x坐標(biāo)
              final int y2 = (int) (bottom.percentage * h) + bottom.offset;   // 下邊的y坐標(biāo)
              width = x2 - x;
              height = y2 - y;
          }

           

          計(jì)算時(shí)根據(jù)給出right與bottom布局分為兩種情況,如果未給出,那么根據(jù)組件的getPreferredSize方法得到組件的最佳大小,以這個(gè)大小決定組件的尺寸。作為規(guī)范,使用布局管理器布局不是參照組件的getSize而是參照getPreferredSize來最終決定組件的尺寸,所有布局管理器也都是這么實(shí)現(xiàn)的。所以如果你企圖 
          設(shè)置組件的setSize()方法來達(dá)到在布局管理器中布局的目的是不可能的,所以你應(yīng)該視圖調(diào)用組件的setPreferredSize方法。接上,如果right和bottom都不是null,那么計(jì)算組件尺寸將忽略getPreferredSize,計(jì)算x2和y2的坐標(biāo),然后兩坐標(biāo)相減得到長寬。最后調(diào)用組件的setBounds進(jìn)行最終定位。

          comp.setBounds(x, y, width, height);
          可見對于布局管理器,其布局原理與使用絕對布局一樣,調(diào)用setBounds實(shí)現(xiàn),沒什么特別之處。只不過是把布局單獨(dú)抽出成一個(gè)類來實(shí)現(xiàn)罷了。
          layoutContainer的完整代碼如下:
          public void layoutContainer(final Container parent) {
              synchronized (parent.getTreeLock()) {
                  final int w = parent.getWidth();
                  final int h = parent.getHeight();
                  final Component[] components = parent.getComponents();
                  for (final Component comp : components) {
                      final FormData formData = componentConstraints.get(comp);
                      if (formData == null) {
                          continue;
                      }
                      final FormAttachment left = formData.left;
                      final FormAttachment right = formData.right;
                      final FormAttachment top = formData.top;
                      final FormAttachment bottom = formData.bottom;
                      final int x = (int) (left.percentage * w) + left.offset;
                      final int y = (int) (top.percentage * h) + top.offset;
                      final int width;
                      final int height;
                      if (right == null || bottom == null) {
                          final Dimension size = comp.getPreferredSize();
                          if (size == null) {
                              throw new RuntimeException("If right FormAttachment or bottom FormAttachment is null,the component must have preferred-size");
                          } else {
                              width = size.width;
                              height = size.height;
                          }
                      } else {
                          final int x2 = (int) (right.percentage * w) + right.offset;
                          final int y2 = (int) (bottom.percentage * h) + bottom.offset;
                          width = x2 - x;
                          height = y2 - y;
                      }
                      comp.setBounds(x, y, width, height);
                  }
              }
          }

           

          作為FormLayout需要補(bǔ)充的是,在進(jìn)行最終布局“component.setBounds(x, y, width, height);”之前,未進(jìn)行邏輯判斷,所以x、y可能會(huì)超出了容器的范圍而width、height也可能是負(fù)值,這都會(huì)導(dǎo)致組件“莫名其妙”地不可見,這都不是布局管理器的問題。例如以下兩行代碼:
          formData.left = new FormAttachment(0.5f, 30);
          formData.right = new FormAttachment(0.5f, 20);
          就會(huì)使組件永遠(yuǎn)不能顯示,因?yàn)閷τ趌eft的定位,是位于容器50%處向右30像素處,而right是位于容器50%處向右20像素處,這樣組件的長度就是-10,怎么能顯示出來呢?

          FormLayout就介紹到這里,因?yàn)榘l(fā)帖只能在周末,加上最近一段時(shí)間還有別的事,能擠出一點(diǎn)時(shí)間真不容易。請關(guān)注下一篇CenterLayout的實(shí)現(xiàn)。

          posted on 2007-11-24 18:26 sun_java_studio@yahoo.com.cn(電玩) 閱讀(16907) 評論(6)  編輯  收藏 所屬分類: NetBeansGUI Design

          評論

          # re: 自定義布局管理器-FormLayout 2007-12-01 23:08 Java.net

          這個(gè)應(yīng)該比較有用,表單的設(shè)計(jì)還是用的很多的....  回復(fù)  更多評論   

          # re: 自定義布局管理器-FormLayout 2009-09-07 14:39 mn

          你好,我在自己嘗試的時(shí)候發(fā)現(xiàn)一個(gè)問題,就是public void addLayoutComponent(Component comp, Object constraints) 這個(gè)方法調(diào)用時(shí)formData作為Object 傳入以后再驗(yàn)證時(shí)就不是FormData的實(shí)例了,這個(gè)要怎么解決呢?  回復(fù)  更多評論   

          # re: 自定義布局管理器-FormLayout 2009-09-08 10:34 sun_java_studio@yahoo.com.cn(電玩)

          @mn
          不要調(diào)用布局管理器的addLayoutComponent(Component comp, Object constraints)方法。
          這個(gè)方法是在向組件的add方法時(shí)自動(dòng)被調(diào)用的。至于你說的“傳入以后再驗(yàn)證時(shí)就不是FormData的實(shí)例了”,想問是怎么傳入的?  回復(fù)  更多評論   

          # re: 自定義布局管理器-FormLayout 2009-09-09 17:14 mn

          @sun_java_studio@yahoo.com.cn(電玩)
          hi,不好意思后來發(fā)現(xiàn)實(shí)際上問題不是出在addLayoutComponent上。
          在普通的application里沒有任何問題,但在Netbeans Platform中使用發(fā)現(xiàn)幾個(gè)問題。
          我是定義了一個(gè)JPanel使用了這個(gè)布局,在TopComponent中add這個(gè)panel,但是panel中加入的任何組件都顯示不出來,發(fā)現(xiàn)是invalidateLayout這個(gè)方法在在一開始就被調(diào)用了,導(dǎo)致layoutContainer在被調(diào)用之前HashMap中的值已經(jīng)被清空了。
          還有一個(gè)問題就是在PlatForm中new一個(gè)JFrame時(shí),
          public void removeLayoutComponent(Component comp) {
          synchronized (comp.getTreeLock()) {
          componentMap.remove(comp);
          }
          }
          這個(gè)方法報(bào)一個(gè)java.lang.StackOverflowError的錯(cuò)誤,不知道是為什么  回復(fù)  更多評論   

          # re: 自定義布局管理器-FormLayout 2009-09-10 13:30 電玩

          @mn
          FormLayout和CenterLayout的實(shí)現(xiàn)我作了些更改,現(xiàn)在看到的是最新內(nèi)容。
          具體改動(dòng)如下:

          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);
          }

          invalidateLayout將原先的移除代碼去掉了,因?yàn)樵谖业捻?xiàng)目中也發(fā)現(xiàn)你所說的問題。還有xxxLayoutSize直接返回常量就可以了。你說的java.lang.StackOverflowError這個(gè)是堆棧溢出錯(cuò)誤,我以前也遇到過,應(yīng)該是先前的xxxLayoutSize做了遞歸調(diào)用導(dǎo)致,不應(yīng)該是removeLayoutComponent所為,你可以參考BorderLayout的removeLayoutComponent方法實(shí)現(xiàn),原理是一樣的。

          更正后的代碼我們公司的很多java程序都要用,沒有出現(xiàn)過問題。  回復(fù)  更多評論   

          # re: 自定義布局管理器-FormLayout 2014-07-21 14:29 我們

          做好的東西方法未必是唯一的,應(yīng)當(dāng)允許接納不同方法.  回復(fù)  更多評論   

          TWaver中文社區(qū)
          主站蜘蛛池模板: 泾源县| 桐庐县| 开阳县| 汉寿县| 邵阳县| 遂昌县| 左权县| 岑溪市| 台中市| 凌云县| 张家川| 达拉特旗| 安多县| 六安市| 灵宝市| 昭觉县| 西安市| 武胜县| 安顺市| 墨玉县| 永寿县| 昆明市| 精河县| 攀枝花市| 张掖市| 呈贡县| 祁东县| 长丰县| 棋牌| 辰溪县| 大安市| 华坪县| 大埔县| 密云县| 唐河县| 普兰县| 广德县| 都安| 高邮市| 汝阳县| 荔浦县|