WisdomSoft - for your serial experiences.

5.12 レイアウトマネージャ

コンテナに追加したコンポーネントをピクセル座標で配置してしまうのは推奨されません。レイアウトマネージャを用いることで、より抽象的な意味でコンポーネントを配置でき、環境や画面解像度への依存を低下できます。

5.12.1 コンテナとレイアウトマネージャの関係

Java は、コンポーネントをコンテナに配置する方法を抽象化しているため、多くのプラットフォームで見られるようなコンポーネントの座標とサイズを使った固定的な配置は行われません。プログラムが明示的にコンポーネントの位置とサイズを設定しても、コンテナに追加して表示すると値が変更されていることがあります。

これは、コンテナに設定されているレイアウトマネージャの影響です。コンテナはコンポーネントを配置する必要がある場合、自らに設定されているレイアウトマネージャを呼び出し、自分自身の子コンポーネントの座標とサイズの設定をすべて委譲します。なぜならば Java はプラットフォームを固定していないため、同じ位置やサイズを指定してもシステムによっては異なる動作をする可能性があるためです。とくに AWT の時代はコンポーネントの外観がシステムに依存していたため、位置やサイズを固定することは困難でした。

また、レイアウトマネージャはコンテナのサイズが変更したときにもコンポーネントを適切に再配置してくれるため、ウィンドウのサイズが変更されるたびにコンポーネントの位置を再計算するようなプログラムを書く必要がなくなりました。ビジネスアプリケーションなどでは、これは大きなメリットとなるでしょう。

レイアウトマネージャの実体は java.awt.LayoutManager インタフェースの実装オブジェクトです。すべてのレイアウトマネージャはこのインタフェースを実装しています。

表1 LayoutManager インタフェースのメソッド
メソッド 解説
public void addLayoutComponent(String name, Component comp) レイアウトにコンポーネントを追加し、指定された文字列に関連付けます。
public void removeLayoutComponent(Component comp) 指定されたコンポーネントをレイアウトから削除します。
public Dimension preferredLayoutSize(Container parent) 指定されたコンテナの推奨サイズの寸法を計算します。
public Dimension minimumLayoutSize(Container parent) 指定されたコンテナの最小サイズの寸法を計算します。
public void layoutContainer(Container parent) 指定されたコンテナを配置します。

LayoutManager の役割は、コンテナに追加されているコンポーネントの情報から、コンテナの推奨サイズと最小サイズを計算することと、コンテナのコンポーネントを適切に配置することです。Component クラスはレイアウトマネージャに推奨サイズを提供する getPreferredSize() メソッド、最小サイズを提供する getMinimumSize() メソッド、最大サイズを提供する getMaximumSize() メソッドを公開してます。「4.6 軽量コンポーネント」で getPreferredSize() メソッドをオーバーライドしていたのは、レイアウトマネージャがコンポーネントを配置するために必要だったからなのです。

Component クラス getMinimumSize() メソッド
public Dimension getMinimumSize()
Component クラス getMaximumSize() メソッド
public Dimension getMaximumSize()

ただし、レイアウトマネージャが必ずしもこれらの情報を利用するとは限りません。コンポーネントの位置やサイズの情報をどのように利用するか、または利用しないかについてはレイアウトマネージャの実装で異なります。

LayoutManager インタフェースがコンテナに含まれているコンポーネントを配置する layoutContainer() メソッドを受けたとき、引数に渡されるオブジェクトは Container だけで、コンポーネントの情報が含まれていません。コンテナに含まれているコンポーネントを配置するには Container クラスから子コンポーネントを取得して配置するのです。

コンテナからコンポーネントを取得するには getComponent() メソッド、または getComponents() メソッドを使います。コンテナに含まれているコンポーネントの数を知りたければ getComponentCount() メソッドで取得することができます。

Container クラス getComponent() メソッド
public Component getComponent(int n)
Container クラス getComponents() メソッド
public Component[] getComponents()
Container クラス getComponentCount() メソッド
public int getComponentCount()

LayoutManager の実装では受け取った Container オブジェクトからこれらのメソッドを呼び出して含まれている子コンポーネントを取り出します。後は for 文で個々のコンポーネントの位置やサイズを計算するという形になるでしょう。

レイアウトマネージャをコンテナに設定するには Container クラスの setLayout() メソッドを、現在設定されているレイアウトマネージャを取得するには getLyaout() メソッドを使います。

Container クラス setLayout() メソッド
public void setLayout(LayoutManager mgr)
Container クラス getLayout() メソッド
public LayoutManager getLayout()

レイアウトマネージャには null を指定することも可能です。レイアウトマネージャが null の場合は、コンテナはコンポーネントに設定されている位置とサイズをそのまま使います。

ただし、Swing を採用している場合は重量ウィンドウに対して直接レイアウトマネージャを設定できないので注意してください。JFrame や JWindow、JApplet クラスには子コンポーネントを追加することはできず、これらの唯一の子であるコンテンツペインにコンポーネントを追加していました。レイアウトマネージャも同様で、コンテンツペインに対して設定しなければなりません。

5.12.2 フローレイアウト

レイアウトマネージャの中でも最も単純にコンポーネントを配置する方法がフローレイアウトでしょう。これは AWT のアプレットがデフォルトで設定しているレイアウトマネージャで、コンテナに含まれているコンポーネントを左から右に、上から下へ順番に並べます。メニューバーやツールバー、ステータスバーのような横長、または縦長のコンテナで、ラベルやボタンを一方向に並べるだけの場合に利用することができるでしょう。

フローレイアウトは java.awt.FlowLayout クラスを使います。 このレイアウトマネージャはコンポーネントの推奨サイズを用いて、コンテナに追加されている順番で配置してくれます。

java.awt.FlowLayout クラス
java.lang.Object
  |
  +--java.awt.FlowLayout
public class FlowLayout extends Object implements LayoutManager, Serializable

FlowLayout クラスは、レイアウトの調整のために水平方向と垂直方向のコンポーネント間の間隔を設定する値と、コンポーネントを右揃えにするのか、左揃えにするのか、または中央揃えにするのかを表す定数を管理します。これらのプロパティを参照しながら、コンテナのコンポーネントを適切に配置してくれるでしょう。

表2 BorderLayout クラスのコンストラクタとメソッド(抜粋)
コンストラクタ 解説
public FlowLayout() 5 単位の水平間隔と垂直間隔、中央揃えで構築する。
public FlowLayout(int align) 5 単位の水平間隔と垂直間隔、指定された配置で構築する。
public FlowLayout(int align, int hgap, int vgap) 指定された配置および指定された水平間隔および垂直間隔で構築する。
メソッド
public void setAlignment(int align)

このレイアウトの配置を設定する。

public int getAlignment() このレイアウトの配置を返す。
ublic void setHgap(int hgap) コンポーネント間の水平方向の間隔を設定する。
public int getHgap() コンポーネント間の水平方向の間隔を返す。
public void setVgap(int vgap) コンポーネント間の垂直方向の間隔を設定する。
public int getVgap() コンポーネント間の垂直方向の間隔を返す。

レイアウトの配置を設定する数値は FlowLayout クラスが公開している静的なフィールド定数を使います。左揃えならば FlowLayout.LEFT、中央揃えならば FlowLayout.CENTER、右揃えならば FlowLayout.RIGHT となります。

コード1
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;

public class Test {
	public static void main(String args[]) {
		JFrame win = new JFrame();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);
		win.getContentPane().setLayout(new FlowLayout(FlowLayout.LEFT));
		for(int i = 0 ; i < 5 ; i++) {
			LabelEx label = new LabelEx("Label " + i);
			label.setBorder(new LineBorder(Color.BLACK));
			win.getContentPane().add(label);
		}
		win.show();
	}
}

class LabelEx extends JComponent {
	private String label;
	private int margin = 5;

	public LabelEx(String label) { this.label = label; }

	public Dimension getPreferredSize() {
		FontMetrics fm = getFontMetrics(getFont());
		return new Dimension(
			fm.stringWidth(this.label) + margin * 2 ,
			fm.getHeight() + margin * 2
		);
	}

	public void paintComponent(Graphics g) {
		if (label != null) {
			FontMetrics fm = getFontMetrics(getFont());
			g.drawString(label ,
				getWidth() / 2 -  fm.stringWidth(this.label) / 2 ,
				getHeight() / 2 + fm.getDescent()
			);
		}
	}
}
実行結果
コード1 実行結果

コード1は独自の軽量コンポーネント LabelEx クラスのインスタンスを BorderLayout クラスの制約と共に add() メソッドでコンテナに追加しています。コンテンツペインには BorderLayout を設定しているため、レイアウトマネージャはこの制約の意味を正しく理解し、コンポーネントに関連付けられている位置情報に従って適切に配置してくれるでしょう。因みに BorderLayout はコンポーネントの推奨サイズは完全に無視しています。

5.12.3 ボーダーレイアウト

コンポーネントの推奨サイズとは関係なく、コンテナのサイズを基準にコンポーネントを上下左右及び中央に分けて配置したい場合はボーダーレイアウトを使います。これは java.awt.BorderLayout クラスで実装されているレイアウトで、コンテナに他のコンテナを追加するような重複した複雑なレイアウトを実現する場合に利用することができます。

java.awt.BorderLayout クラス
java.lang.Object
  |
  +--java.awt.BorderLayout
public class BorderLayout extends Object implements LayoutManager2, Serializable

Swing のコンテンツペインはデフォルトで BorderLayout が設定されています。このクラスで注目するべきは LayoutManager2 というインタフェースを実装していることです。これは java.awt.LayoutManager2 インタフェースのことで、このインタフェース自体が LayoutManager インタフェースを継承しています。LayoutManager2 は従来のレイアウトマネージャに加えてレイアウトに制約を与える機能を持ちます。

ボーダーレイアウトを実現するには、コンポーネントを上下左右、または中央のどこに配置するかを明確に指定しなければなりません。そこで、コンポーネントとレイアウトの配置情報を関連付ける手段が制約なのです。LayoutManager2 は本書では省略しますが、独自に制約を持つレイアウトマネージャを実装してみたい方はドキュメントを読んでみると良いでしょう。

表3 BorderLayout クラスのコンストラクタとメソッド(抜粋)
コンストラクタ 解説
public BorderLayout() コンポーネント間に間隔を設けずに、新しいボーダレイアウトを構築します。
public BorderLayout(int hgap, int vgap) コンポーネント間に間隔を指定して、新しいボーダレイアウトを構築します。
メソッド
public void setHgap(int hgap) コンポーネント間の水平方向の間隔を設定します。
public int getHgap() コンポーネント間の水平方向の間隔を返します。
public void setVgap(int vgap) コンポーネント間の垂直方向の間隔を設定します。
ublic int getVgap() コンポーネント間の垂直方向の間隔を返します。

BorderLayout クラスが管理する値はコンポーネントの間に空ける間隔だけで、これ以外については設定する値はありません。では、コンポーネントをどのようにして関連付けるのでしょう。

コンポーネントの配置方法を指定するには Container クラスの add() メソッドで指定します。add() メソッドはコンポーネントだけを指定する以外に Object 型のオブジェクトをコンポーネントと一緒に設定できました。このとき指定する Object 型のオブジェクトこそ、コンポーネントに指定する配置方法を表す値なのです。

レイアウトマネージャがボーダーレイアウトである場合、add() メソッドに指定する制約として BorderLayout クラスが提供している静的なフィールド定数を指定することができます。指定可能なフィールド定数は BorderLayout.NORTH (上端)、BorderLayout.SOUTH (下端)、BorderLayout.EAST (右端)、BorderLayout.WEST (左端)、および BorderLayout.CENTER (中央) のいずれかです。何も指定しない場合は中央となるでしょう。

コード2
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;

public class Test {
	public static void main(String args[]) {
		JFrame win = new JFrame();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);

		win.getContentPane().setLayout(new BorderLayout());
		win.getContentPane().add(new LabelEx("NORTH") , BorderLayout.NORTH);
		win.getContentPane().add(new LabelEx("WEST") , BorderLayout.WEST);
		win.getContentPane().add(new LabelEx("SOUTH") , BorderLayout.SOUTH);
		win.getContentPane().add(new LabelEx("EAST") , BorderLayout.EAST);
		win.getContentPane().add(new LabelEx("CENTER") , BorderLayout.CENTER);

		win.show();
	}
}

class LabelEx extends JComponent {
	private String label;
	private int margin = 5;

	public LabelEx(String label) { 
		setBorder(new LineBorder(Color.BLACK));
		this.label = label;
	}

	public Dimension getPreferredSize() {
		FontMetrics fm = getFontMetrics(getFont());
		return new Dimension(
			fm.stringWidth(this.label) + margin * 2 ,
			fm.getHeight() + margin * 2
		);
	}

	public void paintComponent(Graphics g) {
		if (label != null) {
			FontMetrics fm = getFontMetrics(getFont());
			g.drawString(label ,
				getWidth() / 2 -  fm.stringWidth(this.label) / 2 ,
				getHeight() / 2 + fm.getDescent()
			);
		}
	}
}
実行結果
コード2 実行結果

コード2は独自の軽量コンポーネント LabelEx クラスのインスタンスを BorderLayout クラスの制約と共に add() メソッドでコンテナに追加しています。コンテンツペインには BorderLayout を設定しているため、レイアウトマネージャはこの制約の意味を正しく理解し、コンポーネントに関連付けられている位置情報に従って適切に配置してくれるでしょう。因みに BorderLayout はコンポーネントの推奨サイズは完全に無視しています。

5.12.4 カードレイアウト

カードレイアウトは常にコンテナに含まれているコンポーネントのいずれか 1 つだけを表示するレイアウトマネージャです。状況に応じて表示するコンポーネントを変更するようなコンテナに向いているレイアウトで、タブコンポーネントなどでの利用が考えられます。このレイアウトを実装するのは java.awt.CardLayout クラスです。

java.awt.CardLayout クラス
java.lang.Object
  |
  +--java.awt.CardLayout
public class CardLayout extends Object implements LayoutManager2, Serializable

カードレイアウトはコンポーネントをコンテナに含まれている順番でコンポーネントを入れ替えます。コンテナに含まれるコンポーネントはカードレイアウトで個々のコンポーネントを識別するための文字列を制約として関連付けることができます。

表4 CardLayout クラスのコンストラクタとメソッド(抜粋)
コンストラクタ 解説
public CardLayout() カードレイアウトをサイズゼロの間隔で作成する。
public CardLayout(int hgap, int vgap) 指定された水平方向および垂直方向の間隔を使ってカードレイアウトを作成する。
メソッド
public void setHgap(int hgap) コンポーネント間の水平方向の間隔を設定する。
public int getHgap() コンポーネント間の水平方向の間隔を返す。
public void setVgap(int vgap) コンポーネント間の垂直方向の間隔を設定する。
public int getVgap() コンポーネント間の垂直方向の間隔を返す。
public void first(Container parent) 指定されたコンテナの最初のカードに切り替える。
public void next(Container parent) 指定されたコンテナの次のカードに切り替える。
public void previous(Container parent) 指定されたコンテナの前のカードに切り替える。
public void last(Container parent) 指定されたコンテナの最後のカードに切り替える。
public void show(Container parent, String name) 指定された名前を持つコンポーネントに切り替えます。

表4は CardLayout クラスのコンストラクタと主なメソッドを表してます。next() メソッドprevious() メソッドを使えば、そのコンテナに設定されている現在のコンポーネントから次のコンポーネントや前のコンポーネントに切り替えることができます。次のコンポーネントが存在しない場合は最初のコンポーネントに、前のコンポーネントが存在しない場合は次のコンポーネントに切り替わるでしょう。

add() メソッドにコンポーネントと関連付けられる制約は show() メソッドを使うときに利用されます。show() メソッドを使えば、コンテナにコンポーネントを追加したときに関連付けられたコンポーネントの名前を使って自由にコンポーネントを指定することができるでしょう。

コード3
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;

public class Test extends JFrame  {
	public static void main(String args[]) {
		JFrame win = new Test();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);
		win.show();
	}

	private CardLayout layout = new CardLayout();
	public Test() {
		getContentPane().setLayout(layout);
		for(int i = 1 ; i <= 5 ; i++) {
			LabelEx label = new LabelEx("Label " + i);
			label.setFont(new Font("Serif" , Font.PLAIN , i * 10 ));
			label.addMouseListener(new MouseAdapter() {
				public void mouseClicked(MouseEvent e) { 
					layout.next(getContentPane());
				}
			});
			getContentPane().add(label , label.toString());
		}	
	}
}

class LabelEx extends JComponent {
	private String label;
	private int margin = 5;

	public LabelEx(String label) { 
		setBorder(new LineBorder(Color.BLACK));
		this.label = label;
	}

	public Dimension getPreferredSize() {
		FontMetrics fm = getFontMetrics(getFont());
		return new Dimension(
			fm.stringWidth(this.label) + margin * 2 ,
			fm.getHeight() + margin * 2
		);
	}

	public void paintComponent(Graphics g) {
		if (label != null) {
			FontMetrics fm = getFontMetrics(getFont());
			g.drawString(label ,
				getWidth() / 2 -  fm.stringWidth(this.label) / 2 ,
				getHeight() / 2 + fm.getDescent()
			);
		}
	}
}
実行結果
コード3 実行結果

コード3は 5 つの LabelEx コンポーネントをコンテナに追加していますが、実際に表示されるのは 1 つのコンポーネントだけです。これはカードレイアウトによってコンテナが表示するコンポーネントが決定されているためです。

コンポーネントにはマウスリスナを追加して、クリックすると CardLayout クラスの next() メソッドを呼び出すようにプログラムしてあります。その結果、コンポーネントをクリックするたびにコンテナに設定されている次のコンポーネントに切り替わります。

5.12.5 グリッドレイアウト

関係データベースの情報など、コンポーネントを表として表示する必要がある場合はグリッドレイアウトが適しています。このレイアウトはコンポーネントを行や列に配置することができるため、一般的なコンポーネントの配置に使うことができる柔軟なレイアウトです。

グリッドレイアウトは java.awt.GridLayout クラスを使います。このクラスのコンストラクタで行と列の数を指定することができ、コンテナのコンポーネントはこれにしたがって順に配置されます。

java.awt.GridLayout クラス
java.lang.Object
  |
  +--java.awt.GridLayout
public class GridLayout extends Object implements LayoutManager, Serializable

グリッドレイアウトにはコンポーネントの制約は存在せず、コンポーネントの推奨サイズを無視して、コンテナのサイズと行と列の数でコンポーネントの位置とサイズを決定します。

表5 GridLayout クラスのコンストラクタとメソッド(抜粋)
コンストラクタ 解説
public GridLayout() 1 行 1 列のグリッドレイアウトを作成します。
public GridLayout(int rows, int cols) 指定された数の行と列を持つグリッドレイアウトを作成します。
public GridLayout(int rows, int cols, int hgap, int vgap) 指定された数の行と列を持つグリッドレイアウトを作成します。
メソッド
public void setColumns(int cols) このレイアウトの列数を、指定された値に設定します。
public int getColumns() このレイアウト内の列数を返します。
public void setRows(int rows) このレイアウト内の行数を、指定された値に設定します。
public int getRows() このレイアウト内の行数を返します。
public void setHgap(int hgap) コンポーネント間の水平方向の間隔を、指定された値に設定します。
public int getHgap() コンポーネント間の水平方向の間隔を返します。
public void setVgap(int vgap) コンポーネント間の垂直方向の間隔を、指定された値に設定します。
public int getVgap() コンポーネント間の垂直方向の間隔を返します。

表5はコンストラクタと主なメソッドです。コンストラクタ、または setColumns() メソッドsetRows() メソッドを用いて列数と行数を設定すれば、このレイアウトマネージャはこの値にしたがってコンポーネントを並べます。

コード4
import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;

public class Test {
	public static void main(String args[]) {
		JFrame win = new JFrame();
		win.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
		win.setBounds(10 , 10 , 400 , 300);
		win.getContentPane().setLayout(new GridLayout(5 , 2));
		for(int i = 0 ; i < 10 ; i++) {
			LabelEx label = new LabelEx("Label " + i);
			label.setBorder(new LineBorder(Color.BLACK));
			win.getContentPane().add(label);
		}
		win.show();
	}
}

class LabelEx extends JComponent {
	private String label;
	private int margin = 5;

	public LabelEx(String label) { this.label = label; }

	public Dimension getPreferredSize() {
		FontMetrics fm = getFontMetrics(getFont());
		return new Dimension(
			fm.stringWidth(this.label) + margin * 2 ,
			fm.getHeight() + margin * 2
		);
	}

	public void paintComponent(Graphics g) {
		if (label != null) {
			FontMetrics fm = getFontMetrics(getFont());
			g.drawString(label ,
				getWidth() / 2 -  fm.stringWidth(this.label) / 2 ,
				getHeight() / 2 + fm.getDescent()
			);
		}
	}
}
実行結果
コード4 実行結果

コード4は行数が 5、列数 2 のグリッドレイアウトを設定し、独自の軽量コンポーネント LabelEx をコンテナに追加しています。実行結果のように、このレイアウトマネージャは定められている行数と列数とコンテナから個々のセルのサイズを決定し、コンポーネントを適切なセルに配置します。