Go to Contents Go to Java Page

実験室

 
 

レイアウトマネージャを作ってみよう

 
 

コンポーネントのレイアウトはめんどうくさい

 
 

GUI をあつかうときにさけて通れないのが、コンテナにコンポーネントをどのように配置するかということです。この死語とはレイアウトマネージャが行うことです。

レイアウトマネージャには JDK 1.0 の頃からあった FlowLayout や BorderLayout などから、J2SE 1.4 でとり入れられた SpringLayout まで各種あります。また、これからもさまざまなレイアウトマネージャが提供されると思います。

しかし、しかし、本当にほしいレイアウトマネージャってないと思いません ?

どれも帯に短し、たすきに長しで、これぞ決定版というのがなかなかありません。結局、使いづらい GridBagLayout をひいこらいって使うか、setLayout(null) としてしまうことも多いのではないでしょうか。

そんなときには作ってしまいましょう。

以外と簡単ですよ。

 

 
 

レイアウトマネージャを作るための準備

 
 

使うときにはまったく意識しなくてもいいのですが、実をいうとレイアウトマネージャには 2 種類あります。

1 つは LayoutManager インタフェースをインプリメントしたもの。

もう 1 つは LayoutManager2 インタフェースをインプリメントしたものです。

さて、この 2 つは何が違うのでしょう。

LayoutManager インタフェースをインプリメントしたクラスには FlowLayout, GridLayout などがあります。LayoutManager2 インタフェースをインプリメントしたクラスには BorderLayout, GridBagLayout, SpringLayout などがあります。

2 つの違いは、LayoutManager インタフェースの方は Container#add(Component comp) メソッドだけを使用するのに対し、LayoutManager2 インタフェースの方は Container#add(Component comp, Object constraints) メソッドを使用するか、XXXXXConstraints クラスをレイアウトマネージャと一緒に使うという違いです。

たとえば、BorderLayout クラスは container.add(button, BorderLayout.CENTER) のような書き方をします。また、GridBagLayout クラスは GridBagConstraints クラス、SpingLayout クラスであれば内部クラスの SpringLayout.Constraints クラスを一緒に使用します。

簡単にいえば LayoutManager2 インタフェースは constraints を使用して、単に並べるだけでなくもう少し賢く並べられるようにしようというものです。

さて、決定版たるレイアウトマネージャを作るとしたら、どちらを選びますか?

もちろん LayoutManger2 インタフェースです。LayoutManager2 インタフェースでレイアウトマネージャが作れれば、LayoutManager インタフェースでレイアウトマネージャを作ることも簡単です。

それでは次章から具体的にレイアウトマネージャを作っていきましょう。

 

 
 

どんなレイアウトマネージャを作ろうか

 
 

作るといっても、どんなものを作るかをまず決めなくてはいけません。

私がやりたかったことは

  • 座標感覚でコンポーネントを配置できる
  • GridLayout をもっと柔軟にした感じ
  • グリッドの大きさとコンポーネントの大きさは同一でなくてもいい
  • 複数のグリッドにまたがってもいい
  • PreferredSize が扱えるようにする
  • グリッドの大きさに合わせるかどうかが決められる
  • グリッドの大きさよりコンポーネントが小さくて、グリッドの大きさに合わせないときに、コンポーネントのグリッド上の配置を決められる

こんな感じです。

GridBagLayout クラスではコンポーネントによってグリッドの大きさが変化してしまうことが、使い方を難しくしています。それならば、グリッドは一定にしてしまおうと思ったのです。

GridLayout という名前は使われてしまっているので、グリッドの代わりに格子 Lattice という言葉を使って LatticeLayout という名前にしましょう。

 

 
 

レイアウトマネージャの動作

 
 

LayoutManager2 インタフェースでは次に示すメソッドをインプリメントしなくてはいけません。

  • void addLayoutComponent(Component comp, Object constraints)
  • Dimension maximumLayoutSize(Container target)
  • float getLayoutAlignmentX(Container target)
  • float getLayoutAlignmentY(Container target)
  • void invalidateLayout(Container target)

また、親インタフェースの LayoutManager インタフェースでは次のメソッドをインプリメントします。

  • void addLayoutComponent(String name, Component comp)
  • void removeLayoutComponent(Component comp)
  • Dimension preferredLayoutSize(Container parent)
  • Dimension minimumLayoutSize(Container parent)
  • void layoutContainer(Container parent)

全部を 0 から実装していたら大変なので、ある程度は他の LayoutManager から Cut & Paste してしまいましょう。

さて、これらのメソッドはどのようにコールされているのでしょう。簡単なプログラムを作って試してみました。GridBagLayout クラスを派生させたクラスをつくり、LayoutManager/LayoutManager2 で定義されたメソッドをオーバロードします。処理自体は親クラスに委譲して、コールされたことを出力するだけのプログラムです。

ソース MyGridBagLayout.java
LayoutTest.java

 

import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
 
public class MyGridBagLayout extends GridBagLayout {
 
    public MyGridBagLayout() {
        super();
        System.out.println("()");
    }
 
    public void addLayoutComponent(Component comp, Object constraints) {
        System.out.println("addLayoutComponent(Component [" 
                           + comp.getName() + ", constraints)");
 
        super.addLayoutComponent(comp, constraints);
    }
 
    public void removeLayoutComponent(Component comp) {
        System.out.println("removeLayoutComponent(Component ["
                           + comp.getName() + "])");
        super.removeLayoutComponent(comp);
    }
 
    public Dimension minimumLayoutSize(Container target) {
        System.out.println("minimumLayoutSize(Container ["
                           + target.getName() + "])");
        return super.minimumLayoutSize(target);
    }
 
    public Dimension preferredLayoutSize(Container target) {
        System.out.println("preferredLayoutSize(Container ["
                           + target.getName() + "])");
        return super.preferredLayoutSize(target);
    }
 
    public Dimension maximumLayoutSize(Container target) {
        System.out.println("maximumLayoutSize(Container ["
                           + target.getName() + "])");
        return super.maximumLayoutSize(target);
    }
 
    public float getLayoutAlignmentX(Container parent) {
        System.out.println("getLayoutAlignmentX(Container ["
                           + parent.getName() + "])");
        return super.getLayoutAlignmentX(parent);
    }
 
    public float getLayoutAlignmentY(Container parent) {
        System.out.println("getLayoutAlignmentY(Container ["
                            + parent.getName() + "])");
        return super.getLayoutAlignmentY(parent);
    }
 
    public void invalidateLayout(Container target) {
        System.out.println("invalidateLayout(Container ["
                            + target.getName() + "])");
        super.invalidateLayout(target);
    }
 
    public void layoutContainer(Container target) {
        System.out.println("layoutContainer(Container ["
                           + target.getName() + "])");
        super.layoutContainer(target);
    }
 
    public void setConstraints(Component comp, GridBagConstraints constraints) {
        System.out.println("setConstraints(Component ["
                           + comp.getName() + "], constraints)");
        super.setConstraints(comp, constraints);
    }
}

これを使って次のテストプログラムを動作させて見ましょう。ボタン 3 つを横に並べただけのプログラムです。

import java.awt.Button;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.Panel;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
 
public class LayoutTest {
    public LayoutTest() {
        Frame frame = new Frame("LayoutTest");
        frame.setBounds(100, 100, 400, 400);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                System.exit(1);
            }
        });
  
        System.out.println("construct GridBag");
        GridBagConstraints con = new GridBagConstraints();
        MyGridBagLayout gridbag = new MyGridBagLayout();
 
        frame.setLayout(gridbag);
  
        con.fill = GridBagConstraints.BOTH;
        con.weightx = 1.0;
        Button button = new Button("Button1");
        gridbag.setConstraints(button, con);
 
        System.out.println("Frame#add(Button1)");
        frame.add(button);
  
        con.gridwidth = GridBagConstraints.RELATIVE;
        button = new Button("Button2");
        gridbag.setConstraints(button, con);
 
        System.out.println("Frame#add(Button2)");
        frame.add(button);
  
        con.gridwidth = GridBagConstraints.REMAINDER;
        button = new Button("Button3");
        gridbag.setConstraints(button, con);
 
        System.out.println("Frame#add(Button3)");
        frame.add(button);
 
        frame.setVisible(true);
    }
 
    public static void main(String[] args) {
        new LayoutTest();
    }
}

 

実行するとボタンが 3 つ並んだウィンドウが描画されるはずです。コマンドプロンプトには次のように出力されました。

construct GridBag
<init>()
setConstraints(Component [button0], constraints)
Frame#add(Button1)
addLayoutComponent(Component [button0], constraints)
setConstraints(Component [button1], constraints)
Frame#add(Button2)
addLayoutComponent(Component [button1], constraints)
setConstraints(Component [button2], constraints)
Frame#add(Button3)
addLayoutComponent(Component [button2], constraints)
invalidateLayout(Container [frame0])
layoutContainer(Container [frame0])
invalidateLayout(Container [frame0])    <--- ここでフレームのリサイズを行った
layoutContainer(Container [frame0])

これから分かることは、

  • コンテナにコンポーネントが add されると、LayoutManager2#addLayoutComponent がコールされる
  • レイアウトを行う前に、LayoutManager2#invalidLayout がコールされる
  • レイアウトは LayoutManager2#layoutContainer で行われる

また、類推できることは

  • コンテナからコンポーネントが remove されると、LayoutManager#removeLayoutComponent がコールされる

XXXXLayoutSize というメソッドはコンテナが自分のサイズを決めるときに使用されます。例えば、JScrollPane に JPanel が貼られるときに、JPanel の大きさが分からなければ、JScrollPane もスクロールバーを出していいのかどうか分かりません。

かといって JPanel は自分にどのようなコンポーネントがはられているか、また自分のレイアウトマネージャによってその大きさは変わってしまいます。このような情報は誰がもっているかというとレイアウトマネージャが持っているので、サイズを決めるときにもレイアウトマネージャに問い合わせするのです。

 

 
 

Constraints

 
 

レイアウトマネージャのだいたいの動作が分かったところで、さっそくレイアウトマネージャを作っていきたいところですが、その前に Constraints を作ってしまいましょう。

LayoutManger2 インタフェースで使われるのが Constraints です。Constraints には BorderLayout の BorderLayout.CENTER, BorderLayout.SOUTH のように簡単な Constraints から、GridBagConstraints クラスや SprintLayout.Constraints クラスのようにいろいろとプロパティを持つものまで各種あります。

LatticeLayout クラスでも GridBagConstraints のように独立したクラスで Constraints を作っていきます。

Constraints にはレイアウトするときの条件を記述できるようにします。前述したような特徴を持つとしたらどんな条件が必要でしょうか。

  • 格子の x 座標
  • 格子の y 座標
  • 幅 格子を単位にして
  • 高さ 同じく格子を単位にして
  • 上記の幅よりコンポーネントの幅が狭いときの横方向の配置方法 (左詰、右詰、中央)
    ただし、下記の拡大が指定されていない場合
  • 上記の高さよりコンポーネントの高さがないときの縦方向の配置方法 (上、下、中央)
    ただし、下記の拡大が指定されていない場合
  • 指定した格子の大きさよりコンポーネントが小さいときにコンポーネントのサイズを拡大するかどうか (しない、縦のみ、横のみ、両方)
  • 指定した格子の大きさよりコンポーネントが大きいときにコンポーネントのサイズを縮小するかどうか (しない、縦のみ、横のみ、両方)
  • コンポーネントのまわりの余白分 (Insets)

結構ありました。これだけあれば十分でしょう。後はこれをクラスにして、指定を行いやすいように定数を定義します。

また、なにも指定しないときのデフォルト値もちゃんと決めておきましょう。

忘れていけないのは、コンストラクタにコピーコンストラクタか clone メソッドを用意することです。GridBagConstraints などの使い方を見てみると、プロパティは public になっておりコンポーネントごとに値を直接変えています。したがって、setConstraints メソッドがコールされたときに、Constraints をそのまま使ってしまっては値が変わってしまうので、必ずコピーをとるようにします。その時に使われるのが、コピーコンストラクタか clone メソッドです。

というわけでこんな風になりました。分量はありますが、中身はたいしたことないはずです。

package jp.gr.java_conf.skrb.gui.lattice;
 
import java.awt.Insets;
 
/**
 * LatticeConstraints クラスは LatticeLayout クラス用の制約クラスです。
 * 
 * @see LatticeLayout
 */
public class LatticeConstraints implements Cloneable, java.io.Serializable {
    /**
     * コンポーネントの x 座標
     * デフォルトは 0
     */
    public int x;
 
    /**
     * コンポーネントの y 座標
     * デフォルトは 0
     */
    public int y;
 
    /**
     * コンポーネントの表示領域幅
     * デフォルトは 1
     */
    public int width;
 
    /**
     * コンポーネントの表示領域高
     * デフォルトは 1
     */
    public int height;
 
    /**
     * コンポーネントが表示領域幅より小さい場合、描画の指定に使用する
     * LEFT, CENTER, RIGHT のいずれかを値をとる
     * デフォルトは LEFT
     */
    public int halign;
 
    /**
     * コンポーネントが表示領域高より小さい場合、描画の指定に使用する
     * TOP, CENTER, BOTTOM のいずれかを値をとる
     * デフォルトは TOP
     */
    public int valign;
 
    /**
     * コンポーネントが表示領域より小さい場合、拡大表示を行うかどうか指定する
     * NONE, HORIZONTAL, VERTICAL, BOTH のいずれかを値をとる
     * デフォルトは NONE
     */
    public int fill;
 
    /**
     * コンポーネントが表示領域より大きい場合、縮小表示を行うかどうか指定する
     * NONE, HORIZONTAL, VERTICAL, BOTH のいずれかを値をとる
     * デフォルトは NONE
     */
    public int adjust;
 
    /**
     * 左側の余白
     * デフォルトは 0
     */
    public int left;
 
    /**
     * 右側の余白
     * デフォルトは 0
     */
    public int right;
 
    /**
     * 上部の余白
     * デフォルトは 0
     */
    public int top;
 
    /**
     * 下部の余白
     * デフォルトは 0
     */
    public int bottom;
 
    /**
     * fill と adjust で使用する定数。拡大・縮小をしない場合
     */
    public final static int NONE = -1;
 
    /**
     * fill と adjust で使用する定数。拡大・縮小を縦・横共に行う場合
     */
    public final static int BOTH = 0;
 
    /**
     * fill と adjust で使用する定数。縦方向の拡大・縮小を行う場合
     */
    public final static int VERTICAL = 1;
 
    /**
     * fill と adjust で使用する定数。横方向の拡大・縮小を行う場合
     */
    public final static int HORIZONTAL = 2;
 
    /**
     * halign と valign で使用する定数。中央に表示する場合
     */
    public final static int CENTER = 0;
 
    /**
     * valign で使用する定数。上付きで表示する場合
     */
    public final static int TOP = 1;
 
    /**
     * valign で使用する定数。下付きで表示する場合
     */
    public final static int BOTTOM = 2;
 
    /**
     * halign で使用する定数。左詰で表示する場合
     */
    public final static int LEFT = 3;
 
    /**
     * halign で使用する定数。右詰で表示する場合
     */
    public final static int RIGHT = 4;
 
     /**
     * 全てをデフォルト値で生成するコンストラクタ
     */
    public LatticeConstraints() {
        this(0, 0, 1, 1, LEFT, TOP, NONE, NONE, 0, 0, 0, 0);
    }
 
    public LatticeConstraints(int x, int y, int width, int height) {
        this(x, y, width, height, LEFT, TOP, NONE, NONE, 0, 0, 0, 0);
    }
 
    public LatticeConstraints(int x, int y, int width, int height, Insets insets) {
        this(x, y, width, height, LEFT, TOP, NONE, NONE,
             insets.top, insets.left, insets.bottom, insets.right);
    }
 
    public LatticeConstraints(int x, int y, int width, int height,
                              int fill, int adjust, Insets insets) {
        this(x, y, width, height, LEFT, TOP, fill, adjust,
             insets.top, insets.left, insets.bottom, insets.right);
    }
 
    public LatticeConstraints(int x, int y, int width, int height,
                              int halign, int valign,
                              int fill, int adjust, Insets insets) {
        this(x, y, width, height, valign, halign, fill, adjust,
             insets.top, insets.left, insets.bottom, insets.right);
    }
 
    /**
     * 全てのプロパティを指定して生成するコンストラクタ
     */
    public LatticeConstraints(int x, int y, int width, int height,
                              int halign, int valign, int fill, int adjust,
                              int top, int left, int bottom, int right) {
        this.x = x;
        this.y = y;
        this.width = width;
        this.height = height;
        this.halign = halign;
        this.valign = valign;
        this.fill = fill;
        this.adjust = adjust;
        this.top = top;
        this.left = left;
        this.bottom = bottom;
        this.right = right;
    }
 
    /**
     * オブジェクトの複製を作成
     */
    public Object clone () {
        try { 
            LatticeConstraints c = (LatticeConstraints)super.clone();
            return c;
        } catch (CloneNotSupportedException e) { 
            throw new InternalError();
        }
    }
 
    /**
     * 文字列への変更
     */
    public String toString() {
        String fillStr = "";
        switch (fill) {
        case NONE:
            fillStr = "none";
            break;
        case BOTH:
            fillStr = "both";
            break;
        case VERTICAL:
            fillStr = "vertical";
            break;
        case HORIZONTAL:
            fillStr = "horizontal";
            break;
        }
 
        String adjustStr = "";
        switch (adjust) {
        case NONE:
            adjustStr = "none";
            break;
        case BOTH:
            adjustStr = "both";
            break;
        case VERTICAL:
            adjustStr = "vertical";
            break;
        case HORIZONTAL:
            adjustStr = "horizontal";
            break;
        }
 
        String halignStr = "";
        switch (halign) {
        case LEFT:
            halignStr = "left";
            break;
        case CENTER:
            halignStr = "center";
            break;
        case RIGHT:
            halignStr = "right";
            break;
        }
             
        String valignStr = "";
        switch (valign) {
        case TOP:
            valignStr = "top";
            break;
        case CENTER:
            valignStr = "center";
            break;
        case BOTTOM:
            valignStr = "bottom";
            break;
        }
            
        return "LatticeConstraints [x: " + x + " y: " + y 
            + " width: " + width + " height: " +height 
            + " halign: " + halignStr + " valign: " + valignStr 
            + " fill: " + fillStr + " adjust: " + adjustStr
            + " top: " + top + " left: " + left 
            + " bottom: " + bottom + " right: " + right + "]";
    }
}
 
 

雑魚を片づけてしまう

 
 

さて、本題のレイアウトマネージャに移りましょう。

レイアウトマネージャの中で一番重要なのが、layoutContainer メソッドであることは動作を考えればすぐ分かると思います。これは後回しにして、そのほかのメソッドを片付けてしまいましょう。

まずはプロパティ。

プロパティとしては格子のサイズを決めなくてはいけません。縦と横です。

もう一つ、レイアウトするコンポーネントを保持するためのコンテナが必要です。ここではコンポーネントと LatticeConstraints をペアで持つ必要があるので、Map を使用しました。

コンストラクタではこれらのプロパティの初期設定を行っています。

    private Map compTable;
    private int latticeX = 1;
    private int latticeY = 1;
 
    public LatticeLayout() {
        compTable = new HashMap();
    }
 
    public LatticeLayout(int x, int y) {
        this();
 
        latticeX = x;
        latticeY = y;
    }

次にたいしたことのないメソッドです。まず getLayoutAlignmntX/getLayoutAlignmentY メソッドです。このメソッドはどういう時使われるのかいまいちよく分かりません。そのほかのレイアウトマネージャを見てみると 0.5 を返しているだけのようなので、ここでもそうしました。

    public float getLayoutAlignmentX(Container parent) {
        return 0.5f;
    }
 
    public float getLayoutAlignmentY(Container parent) {
        return 0.5f;
    }

次に invalidLayout メソッドです。レイアウトを無効にするときに呼ばれるのですが、特に何も行いません。

    public void invalidateLayout(Container target) {}

さて、だんだん核心に近づいてきました。次はコンポーネントの add/remove 関連のメソッドです。addLayoutComponent メソッドと removeLayoutComponent メソッドですが、それ以外に GridBagLayout クラスでも使われている setConstraints メソッドも作成しました。

addLayoutComponent(String name, Component comp) メソッドは今は使われていないメソッドなので、定義だけです。

もう 1 つの addLayoutComponent メソッドはコンポーネントと constraints がペアになっているので、それを compTable に格納します。もし、constraints が LatticeConstraints オブジェクトでなければ IllegalArgumentException を発生させています。

setConstraints メソッドは単純に compTable にコンポーネントと constraints を格納するだけです。

    public void addLayoutComponent(String name, Component comp) {}
 
    public void addLayoutComponent(Component comp, Object constraints) {
        if (constraints instanceof LatticeConstraints) {
            setConstraints(comp, (LatticeConstraints)constraints);
        } else if (constraints != null) {
            throw new IllegalArgumentException(
                "cannot add to layout: constraints must be a LatticeConstraint");
        }
    }
 
    public void removeLayoutComponent(Component comp) {
        compTable.remove(comp);
    }
 
    public void setConstraints(Component comp, LatticeConstraints constraints) {
        compTable.put(comp, constraints.clone());
    }

setConstratins メソッドがあると、getConstraints メソッドも欲しくなります。

getConstraints メソッドは内部的に lookupConstratins メソッドをコールします。lookupConstraints メソッドは compTable から目当ての LatticeConstraints オブジェクトを取り出します。

もし、constraints が null だったらデフォルト値の入った LatticeConstraints オブジェクトをセットしておきます。

lookupConstraints メソッドは内部で使われることを前提にしていますが、getConstraints メソッドは外部からコールされれます。そのために、constraints の複製を返すようにしています。

もし、constraints を複製しなければ、勝手に constraints のプロパティが書き換えられてしまい、レイアウトがおかしくなってしまいかねません。

    private final LatticeConstraints defaultConstraints = new LatticeConstraints();
 
    public LatticeConstraints getConstraints(Component comp) {
        LatticeConstraints constraints = lookupConstraints(comp);

        return (LatticeConstraints)constraints.clone();
    }
 
    protected LatticeConstraints lookupConstraints(Component comp) {
        LatticeConstraints constraints = (LatticeConstraints)compTable.get(comp);
        if (constraints == null) {
            setConstraints(comp, defaultConstraints);
            constraints = (LatticeConstraints)compTable.get(comp);
        }
        return constraints;
  }

残るは preferredLayoutSize/maximumLayoutSize/minimumLayoutSize メソッドです。

これらは Container オブジェクトの preferredSize/maximumSize/minimumSize を決めるために使われるのですが、どのようにして決めるようにしましょう ?

とても単純ですが、次のようにしたいと思います。

  • preferredSize 各コンポーネントの preferredSize の中から最大を調べる。その値 × 格子数
  • maximumSize 各コンポーネントの maximumSize の中から最大を調べる。その値 × 格子数
  • minimumSize 各コンポーネントの minimumSize の中から最大を調べる。その値 × 格子数

ようするに一番大きなコンポーネントのサイズを格子 1 枡分としてしまっているのです。preferred/maximum/minimum の違いはありますが、行う処理は同じです。

結局こうなりました。

    private static final int PREFERRED_SIZE = 0;
    private static final int MINIMUM_SIZE = 1;
    private static final int MAXIMUM_SIZE = 2;
 
    private Dimension getComponentLayoutSize(Component comp, int preferred) {
        Dimension d;
 
        if (preferred == PREFERRED_SIZE) {
            d = comp.getPreferredSize();
        } else if (preferred == MINIMUM_SIZE) {
            d = comp.getMinimumSize();
        } else {
            d = comp.getMaximumSize();
        }
 
        return d;
    }
 
    private Dimension getLayoutSize(Container parent, int preferred) {
        Dimension dim = new Dimension(0, 0);
        int num = parent.getComponentCount();
        
        for (int i = 0; i < num; i++) {
            Component m = parent.getComponent(i);
            if (m.isVisible()) {
                Dimension d = getComponentLayoutSize(m, preferred);
                dim.width = Math.max(d.width, dim.width);
                dim.height = Math.max(d.height, dim.height);
            }
        }
        
        dim.width = dim.width * latticeX;
        dim.height = dim.height * latticeY;
        
        return dim;
    }
 
    public Dimension preferredLayoutSize(Container parent) {
        return getLayoutSize(parent, PREFERRED_SIZE);
    }
 
    public Dimension minimumLayoutSize(Container parent) {
        return getLayoutSize(parent, MINIMUM_SIZE);
    }
 
    public Dimension maximumLayoutSize(Container parent) {
        return getLayoutSize(parent, MAXIMUM_SIZE);
    }

 

 
 

レイアウト

 
 

やっと核心のレイアウトまできました。

レイアウトを行うというのはどういうことなのでしょう。簡単にいえば、貼られているコンポーネントに対し Component#setBounds メソッドで位置と大きさを設定するということです。

その第 1 歩としてコンテナの大きさを取得して、格子の大きさを決めてしまいましょう。コンテナの大きさは getSize で取れますが、忘れていけないのが余白分です。これは getInsets で取得できます。

    public void layoutContainer(Container parent) {
        Dimension containerSize = parent.getSize();
        Insets insets = parent.getInsets();
        
        double oneLatticeX 
            = (double)(containerSize.width - insets.left - insets.right) / latticeX;
        double oneLatticeY
            = (double)(containerSize.height - insets.top - insets.bottom) / latticeY;

そして、貼られているコンポーネントを取り出して、その位置と大きさを決めていきます。

コンポーネントを取り出すのは getComponents メソッドです。それをループで 1 つ 1 つ処理していきます。

位置の x と y は constraints で設定された値から求めることができます。

問題は大きさの方です。コンポーネントには preferredSize と、constraints で設定した大きさの 2 種類があります。この大きさによってコンポーネントを拡大・縮小したり、配置を考えたりしなくてはいけません。

        Component[] comps = parent.getComponents();
 
        for (int i = 0 ; i < comps.length ; i++) {
            Component comp = comps[i];
            Dimension dim = comp.getPreferredSize();
            int prefW = dim.width;
            int prefH = dim.height;
            LatticeConstraints cons = lookupConstraints(comp);
 
            int x = (int)(cons.x * oneLatticeX) + cons.left + insets.left;
            int y = (int)(cons.y * oneLatticeY) + cons.top + insets.top;
            int w = (int)(cons.width * oneLatticeX) - cons.left - cons.right;
            int h = (int)(cons.height * oneLatticeY) - cons.top - cons.bottom;

まず、横軸方向から考えましょう。

設定された幅の方が preferredSize より大きい場合、拡大するか、右詰にするか、中央に配置するか、左詰にします。

fill が NONE と VERTICAL の時は拡大しないので、配置を考えます。halign が LEFT だったらそのままでいいので何もしません。CENTER と RIGHT の時は x を再設定します。コンポーネントの大きさは preferredSize のままにします。

fill がその他の場合は拡大しますが、その大きさは w のままでいいのでコードには記述しなくても大丈夫です。

w が prefW より小さい場合縮小するか、そのままにするかのどちらかです。adjust の値が NONE と VERTICAL の時はそのままなので w を prefW にします。縮小は w のままなので、記述しません。

            if (w > prefW) {
                switch (cons.fill) {
                case LatticeConstraints.NONE:
                case LatticeConstraints.VERTICAL:
                    switch (cons.halign) {
                    case LatticeConstraints.CENTER:
                        x += ((w - prefW) / 2);
                        break;
                    case LatticeConstraints.RIGHT:
                        x += (w - prefW);
                        break;
                    }
                    w = prefW;
                    break;
                }
            } else {
                switch(cons.adjust) {
                case LatticeConstraints.NONE:
                case LatticeConstraints.VERTICAL:
                    w = prefW;
                    break;
                }
            }

縦方向も横方向とほぼ同じなので省略します。

最後に大きさや位置がおかしくなっていないかを調べておきましょう。大丈夫なら位置 x, y で大きさ w, h でコンポーネントを設定します。

            if (x + w > containerSize.width
                 || x + w < 0
                 || y + h < 0
                 || y + h > containerSize.height) {
                comp.setBounds(0, 0, 0, 0);
            } else {
                comp.setBounds(x, y, w, h);
            }

これでできあがりです。

 

 
 

使ってみよう

 
 

せっかくできたので、サンプルを作ってみました。

アプレットとしてもアプリケーションとしても使用できます。

LatticeConstraints オブジェクトに適当な値を設定して、LatticeLayout.setConstraints メソッドを使ってコンポーネントと結び付けて起きます。その後、コンテナに add すれば OK。

Applet の HTML LayoutSample.html
Applet のソース LayoutSample.java

 

import java.applet.Applet;
import java.awt.Button;
import java.awt.Frame;
import java.awt.Label;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
 
import jp.gr.java_conf.skrb.gui.lattice.LatticeConstraints;
import jp.gr.java_conf.skrb.gui.lattice.LatticeLayout;
 
public class LayoutSample extends Applet {
    public void start () {
        Button button1 = new Button("Button 1 12345");
        Button button2 = new Button("Button 2 67890");
        Button button3 = new Button("Button 3 ABCDEFG");
        Button button4 = new Button("Button 4 abcdefg");
        Button button5 = new Button("Button 5 hijklmn");
        Button button6 = new Button("Button 6 あいう");
        Button button7 = new Button("Button 7 えおか");
        Button button8 = new Button("Button 8 きくけ");
 
        LatticeLayout layout = new LatticeLayout(6, 4);
        LatticeConstraints cons = new LatticeConstraints();
        setLayout(layout);
 
        cons.x = 0;
        cons.y = 0;
        cons.width = 3;
        cons.height = 2;
        cons.fill = LatticeConstraints.BOTH;
        layout.setConstraints(button1, cons);
        add(button1);
 
        cons.x = 4;
        cons.y = 0;
        cons.width = 1;
        cons.height = 1;
        cons.adjust = LatticeConstraints.BOTH;
        layout.setConstraints(button2, cons);
        add(button2);
 
        cons.x = 0;
        cons.y = 2;
        cons.width = 1;
        cons.height = 1;
        layout.setConstraints(button3, cons);
        add(button3);
 
        cons.x = 1;
        Label label = new Label("Label AAAAA");
        layout.setConstraints(label, cons);
        add(label);
 
        cons.x = 2;
        cons.width = 2;
        cons.fill = LatticeConstraints.HORIZONTAL;
        cons.valign = LatticeConstraints.CENTER;
        layout.setConstraints(button4, cons);
        add(button4);
 
        cons.x = 4;
        cons.fill = LatticeConstraints.VERTICAL;
        layout.setConstraints(button5, cons);
        add(button5);
 
        cons.x = 0;
        cons.y = 3;
        cons.fill = LatticeConstraints.NONE;
        cons.valign = LatticeConstraints.BOTTOM;
        layout.setConstraints(button6, cons);
        add(button6);
 
        cons.x = 2;
        cons.fill = LatticeConstraints.BOTH;
        layout.setConstraints(button7, cons);
        add(button7);
 
        cons.x = 4;
        cons.left = 10;
        cons.top = 10;
        cons.right = 2;
        cons.bottom = 5;
        layout.setConstraints(button8, cons);
        add(button8);
    }
 
    static Frame frame;
 
    public static void main(String[] args) {
        frame = new Frame("LayoutSample");
        frame.setSize(400, 400);
        frame.addWindowListener(new WindowAdapter() {
            public void windowClosing(WindowEvent event) {
                frame.dispose();
            }
        });
 
        LayoutSample sample = new LayoutSample();
        frame.add(sample);
        sample.start();
 
        frame.setVisible(true);
    }
}

 

 
 

おわりに

 
 

どうでしょう。私は使いやすいと思うんだけど、どうですか。すくなくとも GridBagLayout よりは分かりやすいと思います。

こんな風に結構簡単にレイアウトマネージャは作れるので、もしコアで提供されているものでどうしても満足できなければ作ってしまうのもいいのではないでしょうか。

 

今回作成したソース

ソース LatticeLayout.java
LatticeConstraint.java
JAR lattice10.jar
サンプル LayoutSample.java

(Sep. 2003)

 
 
Go to Contents Go to Java Page