Java桌面技術(shù)

          Java Desktop Technology

          常用鏈接

          統(tǒng)計(jì)

          友情連接

          最新評論

          布局管理器面面觀

          本系列文章將系統(tǒng)地介紹在AWT-Swing組件體系下如何使用布局管理器,從概念開始并結(jié)合JDK1.6 API源代碼講述布局管理器工作原理,然后介紹如何自定義布局管理器并給出2個(gè)自定義的實(shí)現(xiàn)——FormLayout、CenterLayout,同時(shí)還將介紹如何使用絕對定位解決布局問題,最后以通過xml配置文件聲明及布局組件結(jié)束本文。
          本文包括如下部分:
          一、布局管理器簡介與工作原理
          二、如何編寫自定義布局管理器
          三、FormLayout實(shí)現(xiàn)
          四、CenterLayout實(shí)現(xiàn)
          五、如何使用絕對定位解決布局問題
          六、通過xml配置文件定義及布局組件

                                        第一部分:布局管理器簡介與工作原理
          布局管理器是一個(gè)實(shí)現(xiàn)了LayoutManager接口或LayoutManager2接口并且能夠確定一個(gè)容器內(nèi)部所有組件大小和位置的對象。盡管組件能夠提供大小和對齊的提示信息,但是一個(gè)容器的布局管理器將最終決定組件的尺寸和位置。

          布局管理器的工作原理
          基本的布局管理器要實(shí)現(xiàn)LayoutManager接口。LayoutManager接口聲明了5個(gè)基本方法:
          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個(gè)方法:
          void addLayoutComponent(Component comp, Object constraints)
          float getLayoutAlignmentX(Container target)
          float getLayoutAlignmentY(Container target)
          void invalidateLayout(Container target)
          Dimension maximumLayoutSize(Container target)

          以上方法是構(gòu)成布局管理器的所有方法,只有當(dāng)容器添加了布局管理器時(shí),這些方法才可能被調(diào)用到。下面一一講述這些方法的調(diào)用時(shí)機(jī)。

          “void addLayoutComponent(String name, Component comp)”和“void addLayoutComponent(Component comp, Object constraints)”兩個(gè)方法是當(dāng)向容器內(nèi)添加組件時(shí)候可能被調(diào)用。具體調(diào)用那個(gè)由add方法的參數(shù)決定。
          Javadoc中是這么說明的:對于前者的注解是“如果布局管理器使用每組件字符串,則將組件 comp 添加到布局,并將它與 name 指定的字符串關(guān)聯(lián)。”;后者則是“使用指定約束對象,將指定組件添加到布局。”
          例如,使用java.awt.Container類的“Component add(String name, Component comp)”方法添加組件comp時(shí)候,如果該容器(container)設(shè)置了布局管理器,那么該布局管理器的“void addLayoutComponent(String name, Component comp)”方法將被調(diào)用;使用java.awt.Container類的 
          “void add(Component comp, Object constraints)”方法添加組件時(shí),該容器的布局管理器(如果有且實(shí)現(xiàn)了LayoutManager2接口)的“void addLayoutComponent(Component comp, Object constraints)”將被調(diào)用。例如下面這行代碼:
          ....add(new JButton(),BorderLayout.CENTER);
          就會調(diào)用布局管理器的void add(Component comp, Object constraints)。如果你查看java.awt.BorderLayout的源碼,會發(fā)現(xiàn)BorderLayout實(shí)現(xiàn)的是LayoutManager2接口。

          我們看一下JDK源碼是怎樣的調(diào)用關(guān)系。記住,讀源碼是學(xué)習(xí)開源技術(shù)最徹底的方法
          在java.awt.Container的所有add(...)方法中,都是最終調(diào)用“protected void addImpl(Component comp, Object constraints, int index)”這個(gè)實(shí)現(xiàn),add方法的參數(shù)不同,調(diào)用addImpl時(shí)候傳入的參數(shù)也不同。例如,Component add(String name, Component comp)方法的實(shí)現(xiàn)是這樣的:
           public Component add(String name, Component comp) {
                addImpl(comp, name, -1);
                return comp;
          }
          void add(Component comp, Object constraints)方法的實(shí)現(xiàn)是這樣的:
          public void add(Component comp, Object constraints) {
                addImpl(comp, constraints, -1);
          }

          “addImpl”方法實(shí)現(xiàn)很長,不可能全部給出,但是有一段對分析布局管理器有幫助:
          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);
                 }
              }
          ......
          }
          如果這個(gè)容器設(shè)置了布局管理器(layoutMgr != null),那么檢查layoutMgr是否實(shí)現(xiàn)的是LayoutManager2接口,如果是就調(diào)用布局管理器的“void addLayoutComponent(Component comp, Object constraints)”方法,否則(實(shí)現(xiàn)的是LayoutManager接口)再判斷constraints是否是String類型,如果是就調(diào)用布局管理器的“void addLayoutComponent(String name, Component comp)”方法。
          到此為止,布局管理器的“void addLayoutComponent(String name, Component comp)”和“void addLayoutComponent(Component comp, Object constraints)”兩個(gè)方法調(diào)用時(shí)機(jī)已經(jīng)非常明了了,同時(shí)我們還了解了一點(diǎn),那就是如果布局管理器實(shí)現(xiàn)的是LayoutManager2接口,那么它的“void addLayoutComponent(String name, Component comp)”永遠(yuǎn)不會被awt框架調(diào)用到,除非你顯示地調(diào)用。

          LayoutManager接口的“void removeLayoutComponent(Component comp)”方法,是在容器移除子組件時(shí)候被調(diào)用。打開JDK源代碼,java.awt.Container的移除組件的方法實(shí)現(xiàn)如下:
          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);
                  }
                }
              }
            }
          }
          可以看出,每個(gè)添加到容器的組件都被保存在component[]中,刪除組件時(shí)會遍歷數(shù)組,發(fā)現(xiàn)被刪除的組件調(diào)用public void remove(int index)執(zhí)行刪除。在remove(int index)方法中同樣有我們關(guān)注的調(diào)用。
          public void remove(int index) {
          ......
            if (layoutMgr != null) {
                 layoutMgr.removeLayoutComponent(comp);
            }
          ......
          }
          可見,組件從父容器移除過程中會調(diào)用布局管理器(如果設(shè)置了布局管理器)的“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個(gè)方法。
          有時(shí)候,需要自定義一個(gè)組件為它的容器布局管理器提供關(guān)于大小的提示信息,通過指定組件的最小、首選、最大大小維數(shù)可以提供大小的提示信息。可以調(diào)用組件的方法來設(shè)置大小提示信息——setMinimumSize、setPreferredSize、setMaximumSize,或者重寫其對應(yīng)的get...Size方法同樣可以實(shí)現(xiàn)。注意setSize(Dimension d)與set...Size(Dimension preferredSize)是不一樣的,前者能最終確定組件大小,但是只能用在絕對布局(不設(shè)置任何布局管理器)的情況下;后者是給該組件大小提供關(guān)于大小的提示信息,是給布局管理器看的。但是提示畢竟是提示,最終決定組件大小還是布局管理器決定,提示信息只能算是參考。但是話說回來,布局管理器應(yīng)該嚴(yán)格按照組件的尺寸提示信息行事,例如不應(yīng)該把組件的尺寸設(shè)置成小于它的提示最小尺寸等。有時(shí)候preferredSize屬性會比size更重要,因?yàn)榻M件框架內(nèi)部通常考慮組件的首選尺寸而不是實(shí)際尺寸的值。例如要實(shí)現(xiàn)JTree不同結(jié)點(diǎn)有不同的高度(QQ上被選中的好友節(jié)點(diǎn)會加大尺寸顯示),就可以重寫DefaultTreeCellRenderer的getPreferredSize實(shí)現(xiàn)。
          除了提供大小提示信息以外,還可以提供對齊提示。例如,兩個(gè)組件的上邊界對齊。可以通過調(diào)用組件的setAlignmentX和setAlignmentY方法,或重寫對應(yīng)的get方法來設(shè)置對齊提示,但是大多數(shù)布局管理器會忽略該提示。為了簡單起見,只給出preferredLayoutSize的調(diào)用源代碼,其余方法調(diào)用時(shí)機(jī)相似。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中調(diào)用到了layoutMgr.preferredLayoutSize(this),參數(shù)就是當(dāng)前Container的實(shí)例。

          LayoutManager2接口的“void invalidateLayout(Container target)”方法,在JavaDoc的注釋為“使布局失效,指示如果布局管理器緩存了信息,則應(yīng)該將其丟棄。”,讓我們結(jié)合JDK源碼看看該方法何時(shí)被調(diào)用。在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 是一個(gè) LayoutManager2 實(shí)例,則在該實(shí)例上調(diào)用 LayoutManager2.invalidateLayout(Container),并提供此 Container 作為參數(shù)”。這個(gè)函數(shù)在JavaDoc中的注解為:“使容器失效。該容器及其之上的所有父容器被標(biāo)記為需要重新布置。此方法經(jīng)常被調(diào)用,所以內(nèi)部實(shí)現(xiàn)必須簡潔。
          我們在順便看看“super.invalidate();”是如何實(shí)現(xiàn)的,java.awt.Container的基類是java.awt.Component,其invalidate方法實(shí)現(xiàn)如下:
          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實(shí)現(xiàn)中,把prefSize 、minSize 、maxSize這3個(gè)提示屬性給清空(如果大小提示是通過重寫get...Size強(qiáng)制為特定常量或自定義計(jì)算規(guī)則,那么上述清空操作可能對你沒有實(shí)際意義),并且延著層次關(guān)系發(fā)送到父組件。因?yàn)閟wing組件的基類是javax.swing.JComponent,繼承層次關(guān)系是
          java.lang.Object
            java.awt.Component
                java.awt.Container
                    javax.swing.JComponent

          所以對于所有swing組件來說,如果不重寫invalidate方法,都會是這樣的調(diào)用行為。
          那么LayoutManager2接口的實(shí)現(xiàn)中“void invalidateLayout(Container target)”方法中應(yīng)該做些什么?其實(shí)有些布局管理器的實(shí)現(xiàn)中是忽略的,例如java.awt.BorderLayout。
          正如JavaDoc所說的那樣“使布局失效,指示如果布局管理器緩存了信息,則應(yīng)該將其丟棄。”,應(yīng)該按照J(rèn)avaDoc要求的那樣去做就行了。例如java.awt.BoxLayout布局的實(shí)現(xiàn):
          public synchronized void invalidateLayout(Container target) {
                  checkContainer(target);
                  xChildren = null;
                  yChildren = null;
                  xTotal = null;
                  yTotal = null;
          }
          但是也必須警惕,LayoutManager2接口的invalidateLayout(Container target)方法調(diào)用也很頻繁,當(dāng)組件尺寸改變時(shí),該方法就會被調(diào)用,因此釋放緩存信息時(shí)要小心。


          對于布局管理器來說,最重要的方法莫過于“void layoutContainer(Container parent)”。因?yàn)榻M件的最終布局都是在該方法中實(shí)現(xiàn)的。這個(gè)方法在很多情況下都會被awt-swing框架自動調(diào)用,例如改變組件的字體、容器尺寸改變等都會觸發(fā)該方法的調(diào)用。布局管理器的layoutContainer方法并不會真正繪制組件,它只是調(diào)用每個(gè)組件的setSize、setLocation、setBounds方法來設(shè)置組件的大小和位置。對于自定義組件來說,可以調(diào)用revalidate強(qiáng)制實(shí)現(xiàn),或者調(diào)用容器的doLayout也可以強(qiáng)制實(shí)現(xiàn)。當(dāng)調(diào)用一個(gè)組件的revalidate方法時(shí),一個(gè)請求將通過包含層次關(guān)系發(fā)送到第一個(gè)容器,容器的大小會不會被容器的大小調(diào)整而影響通過調(diào)用容器的isValidateRoot方法來確定。然后容器被重新布局。
          如果你直接調(diào)用容器的doLayout,可以達(dá)到強(qiáng)制布局的效果。JDK源代碼中java.awt.Container的doLayout實(shí)現(xiàn)如下:
          public void doLayout() {
              layout();
          }
          @Deprecated
          public void layout() {
              LayoutManager layoutMgr = this.layoutMgr;
              if (layoutMgr != null) {
                  layoutMgr.layoutContainer(this);
              }
          }
          可見doLayout方法是直接調(diào)用布局管理器的layoutContainer方法。
          此外再給出java.awt.Container的validate方法實(shí)現(xiàn)代碼:
          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()方法實(shí)現(xiàn):
          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方法執(zhí)行過程中調(diào)用了“doLayout();”方法。也就是說會調(diào)用到LayoutManager接口的void layoutContainer(Container parent)方法。
          再給出javax.swing.JComponent類setFont方法實(shí)現(xiàn):
          public void setFont(Font font) {
              Font oldFont = getFont();
              super.setFont(font);
              // font already bound in AWT1.2
              if (font != oldFont) {
                  revalidate();
                  repaint();
              }
          }
          因?yàn)樽煮w的改變會影響到組件的尺寸,因此也涉及到布局。如果你查看JDK API相關(guān)源碼,就會發(fā)現(xiàn)很多情況下“revalidate();”、“ repaint();”兩個(gè)方法是一起被先后調(diào)用的。這兩個(gè)方法都是線程安全的,不需要在事件分發(fā)線程中調(diào)用它們。
          layoutContainer(Container parent)在很多地方都會被調(diào)用的。因此可以這樣理解:凡是能影響組件尺寸改變的條件都可能觸發(fā)該方法的調(diào)用。那么在layoutContainer中需要做的就是,根據(jù)收集到的組件提示信息、約束條件、容器的內(nèi)部邊框、組件的可見性及布局規(guī)則等因素對組件進(jìn)行最終定位。

          到此為止,有關(guān)布局管理器的整體介紹和工作原理就告一段落。學(xué)習(xí)布局管理器的最終目的是學(xué)會如何自定義布局管理器,好,準(zhǔn)備進(jìn)入下一部分的學(xué)習(xí),但是之前最好要把上面講述的消化一遍,尤其是接口方法的調(diào)用時(shí)機(jī),這將是自定義布局管理器的基礎(chǔ)。

          由于平時(shí)比較緊,文章基本是周末空閑時(shí)間寫,而且目前的工作方向不再是gooey了,所以寫一篇帖很不容易,準(zhǔn)備一篇好貼更難。布局管理器這塊本人一直想發(fā)表下自己的觀點(diǎn),敬請關(guān)注。

          posted on 2007-11-18 15:15 sun_java_studio@yahoo.com.cn(電玩) 閱讀(10294) 評論(4)  編輯  收藏 所屬分類: NetBeansSwing

          評論

          # re: 布局管理器面面觀 2007-11-18 15:52 ivin

          終于出新的文章 了。一直都在期待啊。天天都打開樓主的blog看看有沒有新的文章。一口氣看完了,寫的非常好,學(xué)了不少東西,期待樓主以后寫更好的文章。  回復(fù)  更多評論   

          # re: 布局管理器面面觀 2007-11-18 20:33 歡樂的豬

          不錯(cuò),尤其期待第六部分的內(nèi)容  回復(fù)  更多評論   

          # re: 布局管理器面面觀 2007-11-18 21:51 dragoon

          期待后面的內(nèi)容……  回復(fù)  更多評論   

          # re: 布局管理器面面觀 2007-12-04 14:12 herui

          頂,樓主寫的很好,期待以后的文章  回復(fù)  更多評論   

          TWaver中文社區(qū)
          主站蜘蛛池模板: 淅川县| 新丰县| 迭部县| 厦门市| 锦州市| 盐亭县| 调兵山市| 莱芜市| 金阳县| 达尔| 澳门| 五莲县| 太原市| 洛阳市| 芒康县| 阿拉尔市| 新绛县| 辽宁省| 河北省| 大洼县| 临江市| 司法| 麻城市| 阿鲁科尔沁旗| 大竹县| 天台县| 府谷县| 东港市| 刚察县| 黄山市| 瓮安县| 额济纳旗| 类乌齐县| 达拉特旗| 永胜县| 民勤县| 勃利县| 荣成市| 德安县| 建宁县| 黄浦区|