WisdomSoft - for your serial experiences.

4.6 軽量コンポーネント

システムで用意されているコンポーネントを重量コンポーネントと呼び、これに対して Java によって描画されるコンポーネントを軽量コンポーネントと呼びます。

4.6.1 透明なコンポーネント

アプレットやアプリケーションのウィンドウを表示するのは、Java を実装するシステムの役割です。AWT では、メニューやボタン、テキスト入力コンポーネントを表示する場合でも、最終的には OS の API を呼び出してウィンドウを生成します。つまり、同じボタンを表示するプログラムでも Windows と X Window では見た目が異なるということになります。

AWT のコンポーネントはシステムのネイティブな GUI コンポーネントを生成するため重量コンポーネントと呼びます。そのため、コンポーネントは必ず不透明な矩形となります。しかし、これは特殊な形のコンポーネントや、透明なコンポーネントを生成することができません。さらに、ネイティブなシステムの GUI コンポーネントを使うということは、AWT を実行する OS によって見た目や動作が異なってしまいます。動作はするものの、どこでも動き、どのコンピュータでも同じ計算結果を算出する Java 仮想マシンの理想からは外れてしまいます。重量コンポーネントは Java の可能性を制限することになってしまいます。

そこで、AWT はコンポーネントの描画処理も含めて Java プログラムで行う軽量コンポーネントを実現しました。軽量コンポーネントはネイティブなシステムのウィンドウは生成しないので、GUI システムに依存しないコンポーネントの作成が可能です。ネイティブなウィンドウではないコンポーネントというのは、すなわち独自の Grpahics 参照を保有しないということを意味しています。軽量コンポーネントは独自のウィンドウを持つのではなく、親コンポーネントの Graphics オブジェクトを用いて親コンポーネントに描画して実現されます。つまり、どのシステムでも同じ見た目と動作(ルック&フィール)を提供することができるのです。

軽量コンポーネントの実装は難しいものではありません。単純に Component クラスを継承して、paint() メソッドをオーバーライドし、コンポーネントの描画処理を実装すればよいのです。ただし、軽量コンポーネントを作成するにはいくつかの注意点があります。

まず Component クラスを継承して新しいコンポーネントを作るには、必ず getPreferredSize() メソッドをオーバーライドしなければなりません。デフォルトでこのメソッドは幅、高さともに 1 の Dimmension オブジェクトを返します。このメソッドは、コンポーネントの描画に最適な推奨サイズを返すことになっています。Java のコンテナ(他のコンポーネントを表示するコンポーネント)は、この推奨サイズに基づいてコンポーネントを配置します。

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

AWT の GUI コンポーネントはネイティブシステムに依存するため、コンポーネントの位置やサイズを定数で固定すると、異なる GUI システム間で実行された場合、コンポーネントが重なったり、レイアウトが崩れてしまう可能性があります。これを避けるため、Java の AWT ではコンポーネントの配置を行うレイアウト機構も抽象化しているのです。これをレイアウトマネージャと呼びます。

レイアウトマネージャの詳細は後ほど詳しく説明します。ここで重要なのは、コンテナに子コンポーネントを表示するとき、子コンポーネントの位置やサイズは座標などを固定して設定するのではなく、レイアウトマネージャが他のコンポーネントの配置状況にあわせて動的に行ってくれるということです。このレイアウトマネージャがコンポーネントのサイズを決定する重要な情報源が、Component クラスの getPreferredSize() メソッドなのです。

後は、必要に応じてバウンドプロパティを増やすなど、コンポーネントの性質に合わせて機能を追加します。こうして生成した子コンポーネントはアプレットやアプリケーションウィンドウなど、他のコンポーネントを抱合できるコンテナに追加して表示させることができます。AWT コンポーネントを含むことができるコンポーネントは、必ず java.awt.Container クラスを継承しています。

java.awt.Container クラス
java.lang.Object
  |
  +--java.awt.Component
        |
        +--java.awt.Container
public class Container extends Component

Applet クラスは Container クラスを継承しているため、Component オブジェクトを含むことができます。私たち開発者は、頻繁に再利用する GUI オブジェクトを軽量コンポーネントとして作成し、これをアプレットなどのウィンドウ上に配置して使いまわすことができるのです。ボタンやチェックボックス、イメージを表示する専用キャンバスなど、ユーザーとの対話に便利なコンポーネントを作ることで、アプリケーションの生産性を大きく向上させられるでしょう。コンテナにコンポーネントを追加するには add() メソッドを使います。

Container クラス add() メソッド
public Component add(Component comp)
public Component add(Component comp, int index)
public void add(Component comp, Object constraints)
public void add(Component comp, Object constraints, int index)

comp にコンテナに追加するコンポーネントを指定し index に何番目のコンポーネントとして追加するのかをインデックスで指定します。インデックスに -1 を指定した場合は、最後の位置に追加されるでしょう。index を指定しないメソッドでは常に最後に追加されます。constraints はコンポーネントを配置する情報を表す値を提供します。この値を必要とするかどうかは、レイアウトマネージャに依存します。因みに、Component オブジェクトを返す add() メソッドがありますが、これは引数に指定された comp オブジェクトをそのまま返しているだけで、意味のある戻り値ではありません。

逆に、追加したコンポーネントを解除するには remove() メソッド、または removeAll() メソッドを使います。

さらに、getComponentCount() メソッドを使えばコンテナに格納されているコンポーネントの数を取得することができ、getComponent() メソッドで指定した番号のコンポーネントを取得することもできます。getComponents() メソッドを使えば、コンテナに含まれている全てのコンポーネントを取得することもできます。

Container クラス remove() メソッド
public void remove(int index)
public void remove(Component comp)
Container クラス removeAll() メソッド
public void removeAll()
Container クラス getComponentCount() メソッド
public int getComponentCount()
Container クラス getComponent() メソッド
public Component getComponent(int n)
Container クラス getComponents() メソッド
public Component[] getComponents()

remove() メソッドで数値を指定すれば、指定されたインデックスのコンポーネントが解除されます。Component を直接指定すれば、そのコンポーネントがコンテナに含まれていれば、コンテナから外されるでしょう。全てのコンポーネントを外してしまいたいならば removeAll() を使います。

因みに、Component クラスの getParent() メソッドを使えば、コンポーネントの現在の親コンテナを取得することもできます。

Component クラス getParent() メソッド
public Container getParent()

Component クラスを継承した新しい軽量コンポーネントクラスを定義すれば、これをひとつの GUI コンポーネントとして、アプレットやアプリケーションウィンドウなどのコンテナに add() メソッドで格納することができるようになります。

AWT の Button クラスや Label クラスはシステムに依存した GUI コンポーネント、すなわち重量コンポーネントを定義しています。これらも Component クラスを継承しているので add() メソッドや remove() メソッドの操作対象となります。しかし、筆者は Swing が存在する現代では、Java でシステムに依存するコンポーネントを利用するメリットは無いことを主張します。よって、本書では重量コンポーネントは扱いません。

コード1
import java.applet.Applet;
import java.awt.*;

//<applet code="Test.class" width="400" height= "400"></applet>

public class Test extends Applet {
	public void init() {
		add(new LabelEx("Blue Blue Glass Moon"));
		add(new LabelEx("Under the Crimson Air"));
	}
}

class LabelEx extends Component {
	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 paint(Graphics g) {
		if (label != null) {
			FontMetrics fm = getFontMetrics(getFont());
			g.drawString(label , margin , fm.getAscent() + margin);
		}
		g.drawRect(0 , 0 , getWidth() -1 , getHeight() - 1);
	}
}
実行結果
コード1 実行結果

コード1は指定された文字列を表示するだけの簡単なラベルコンポーネントを作成するプログラムです。LabelEx クラスは Component クラスを継承する軽量コンポーネントです。このクラスは、コンストラクタで指定された文字列を表示する単純なプログラムです。コンポーネントが配置されている位置やサイズを確認しやすいように、コンポーネントのサイズに合わせて境界線も描画しています。

LabelEx クラスは、Component クラスの getPreferredSize() メソッドをオーバーライドしています。オーバーライドしたメソッドでは、コンポーネントに設定されているフォントを基に FontMetrics オブジェクトを生成して、表示する文字列の幅と高さを算出しています。この幅と高さにマージンを加えたサイズをこのコンポーネントの推奨サイズとして返します。コンテナのレイアウトマネージャは、コンポーネントが返した推奨サイズを参考に位置とサイズを決定します。

4.6.2 軽量と重量の違い

軽量コンポーネントは、独自の Graphics オブジェクトを持たないという点で重量コンポーネントと大きく違います。paint() メソッドに渡される Graphics オブジェクトは、親コンテナの Graphics オブジェクトが描画する子コンポーネントの座標に原点を移動し、子コンポーネントのサイズでクリッピングされている状態で渡されたものです。古い Java では軽量コンポーネントに設定されている前景色などが無視されていましたが、幸い最新の Java では子コンポーネントの前景色やフォントが Graphics オブジェクトの現在の色とフォントに設定されて渡されます。ただし、軽量コンポーネントの背景は透明なので、設定されている背景色は無視されてしまいます。背景を不透明にしたければ、軽量コンポーネントの paint() メソッドをオーバーライドするなどして塗りつぶす必要があります。

また、軽量コンポーネントの paint() メソッドを呼び出すには、親コンテナの paint() メソッドが呼び出されなければなりません。アプレットなど Container クラスを継承しているクラスで paint() メソッドをオーバーライドする場合、必ずスーパークラスの paint() メソッドを呼び出してください。そうしなければ、コンテナに含まれる軽量コンポーネントの paint() メソッドが呼び出されなくなるため、事実上軽量コンポーネントが表示されなくなってしまいます。

コンポーネントが重量コンポーネントなのか、軽量コンポーネントなのかは Component クラスの isLightweight() メソッドを使って調べることができます。

Component クラスの isLightweight() メソッド
public boolean isLightweight()

コンポーネントが軽量コンポーネントであれば isLightweight() メソッドは true を返します。

ところで、重量コンポーネントであるはずの java.awt.Button クラスや java.applet.Applet クラスも Component クラスを継承しています。コード1で作成した LabelEx クラスのように Component クラスを継承するクラスは軽量コンポーネントになるはずです。ところが、AWT の Button や Label クラスなどは重量コンポーネントです。同じクラスを継承しているにもかかわらず、このような違いが生まれるのはなぜでしょうか。同じスーパークラスを持ちながら、このような性質の分岐を Java はどのように実現しているのでしょう。

実は Componet クラスは内部にピアと呼ばれる java.awt.peer.ComponentPeer というインタフェースを持ち、このピアこそコンポーネントの実体なのです。Component クラスはピアへの中継にすぎません。

ピアが LightweightPeer インタフェースであれば軽量コンポーネントであり、そうでなければ重量コンポーネント、またはピアが null であると考えられます。ピアは Component クラスの getPeer() メソッドから取得できますが、現在ではピアを直接操作することは禁止されているため推奨されていません。LightweightPeer も含めて、全てのピアは java.awt.peer.ComponentPeer インタフェースを継承しています。ドキュメントはありませんが、興味があるのならばピアのソースファイルを調べてみると面白いでしょう。

Component クラスの内部では、コンポーネントの表示が必要になると addNotify() メソッドが呼び出されます。このとき、ピアが null であれば LightweightPeer インタフェースを実装するオブジェクトを生成して、軽量コンポーネントを構築します。もちろん、その仕組みは隠蔽されているため、アプリケーションプログラマが知る必要はありません。重量コンポーネントの場合は、独自に addNotify() メソッドをオーバーライドして、ネイティブな GUI コンポーネントを参照するピアを生成しています。ボタンならばボタンのピアを、ウィンドウならばウィンドウのピアを生成します。私たちが開発する軽量コンポーネントは addNotify() メソッドをオーバーライドしなかったため、Component クラスの addNotify() がそのまま呼び出され、その時点でピアは null を参照しているため、LightweightPeer が生成されていたのです。