WisdomSoft - for your serial experiences.

5.6 描画メカニズム

Swing コンポーネントの描画処理について解説します。通常、Swing コンポーネントの描画を拡張する場合は paint() メソッドでは無く paintComponent() メソッドをオーバーライドします。

5.6.1 描画処理の再定義

AWT コンポーネントに何らかの図やイメージを描画するには paint() メソッドをオーバーライドしました。しかし、JComponent を継承する Swing コンポーネントの場合、paint() メソッドはコンポーネント全体の描画を総括するトップメソッドとして定義されています。

Swing コンポーネントは、コンポーネントの描画以外に、子コンポーネントの描画やコンポーネントの境界線となる枠を部品化しています。子コンポーネントの描画は paintChildren() メソッドで、境界線は paintBorder() メソッドで行われます。paint() メソッドはこれらのメソッドを呼び出して、コンポーネントの外観を整える役割を持っているため、サブクラスでこれをオーバーライドしてしまうと、このルールが破綻してしまう可能性があるため、推奨できません。paint() メソッドをオーバーライドする場合は、Swing コンポーネントの内部動作を理解したうえで行う必要があります。

このような事情があるため、Swing コンポーネントの描画処理を再定義する場合は paintComponent() メソッドをオーバーライドします。

JComponent コンポーネント paintComponent() メソッド
protected void paintComponent(Graphics g)

5.5 Swingの軽量コンポーネント コード1」では、paint() メソッドをオーバーライドしてコンポーネントに描画していますが適切ではありません。JComponent を継承する独自のコンポーネントを作成し、コンポーネントに図を描画するには paintComponet() メソッドをオーバーライドしてください。

class LabelEx extends JComponent {
	public void paintComponent(Graphics g) {
		//描画処理...
	}
}

JComponent を描画する前に何らかの処理を追加したい場合は paint() メソッドをオーバーライドすることも手段ですが、その場合はスーパークラスの paint() メソッドを呼び出すなどして、Swing コンポーネントの機能を果たす必要があります。

5.6.2 イベントディスパッチスレッド

Java アプリケーションが実行されると main() 関数が実行され、main() 関数内でウィンドウを生成しウィンドウを画面に表示しても、コードは非同期に処理され main() 関数は処理を終了します。通常はこの時点でアプリケーションが終了しますが、ウィンドウが表示されている場合はウィンドウが破棄されるまでアプリケーションは実行し続けます。

これは、ウィンドウを制御しているスレッドがアプリケーションを起動したスレッドとは異なるスレッドだからなのです。このウィンドウを制御する専用のスレッドをイベントディスパッチスレッドと呼び、paint() メソッドや update() メソッド、そしてリスナインタフェースの各イベント処理メソッドを呼び出していたのはこのスレッドです。

Swing コンポーネントを操作するメソッドは、一部を除いて必ずイベントディスパッチメソッドから呼び出さなければならないとされています。他のスレッドから呼び出した場合の動作は保証されません。システムによっては例外が発生する可能性があります。

そこで、Swing コンポーネントの制御は、イベントディスパッチスレッドのみで行うように設計しなければなりません。多くのプログラムでは、意識しなくてもシングルスレッド設計で開発できると思われますが、ゲームやネットワーク関連のプログラムはマルチスレッドになるため Swing コンポーネントにアクセスできなくなってしまいます。

そこで、マルチスレッドの場合は、イベントディスパッチスレッドに対して特定のメソッドを呼び出してほしいということを要求します。イベントディスパッチスレッドは、コンポーネントにイベントが発生していないかを常に監視しているため、これに要求すれば、他に処理するべきイベントがなくなった時点で指定したメソッドを呼び出してくれるという仕組みなのです。メソッドの登録は Thread クラスの仕組みと同様に Runnable インタフェースを実装するオブジェクトから参照することで実現されます。

イベントディスパッチスレッドに Runnable オブジェクトを呼び出してもらうには javax.swing.SwingUtilities クラスを用いて要求しなければなりません。

javax.swing.SwingUtilities クラス
java.lang.Object
  |
  +--javax.swing.SwingUtilities
public class SwingUtilities extends Object implements SwingConstants

このクラスは、Swing に関連する機能を単独で動作するツール的な static メソッドのコレクションを提供しています。例えば、Swing の JWindow や JDialog クラスは、親フレームを null に指定すると Swing 共通の隠しフレームが割り当てられるという仕様になっていました。この隠しフレームを提供するのは SwingUtilities クラスの getSharedOwnerFrame() という非公開のメソッドです。

このクラスの invokeLater() メソッドから Runnable インタフェースを実装するオブジェクトを渡せば、イベントディスパッチスレッドの手が空いた時点で呼び出してくれます。invokeLater() メソッド自体は、イベントディスパッチスレッドとは非同期なので即座に制御を返します。

SwingUtilities クラス invokeLater() メソッド
public static void invokeLater(Runnable doRun)

このメソッドの引数に Runnable オブジェクトを渡します。イベントディスパッチスレッドは、他に処理するべきイベントがあればそちらを優先して処理するため、このメソッドを呼び出して即座に doRun.run() が呼び出されるわけではありません。この処理は非同期となるのです。invokeLater() メソッドを別のスレッドから呼び出してdoRun.run() 内で目的のコンポーネント制御すれば、マルチスレッド環境でもイベントディスパッチスレッドのみで Swing コンポーネントにアクセスすることができるのです。

ただし、JComponent の repaint()、revalidate()、invalidate() メソッドには他のスレッドからでもアクセスすることができます。これらのメソッドは invokeLater() メソッドと同様に、イベントディスパッチスレッドに要求することが目的のメソッドだからです。

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

public class Test extends JFrame implements Runnable {	
	private JComponent label;

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

	public Test() { 
		label = new LabelEx("Kitty on your lap");
		getContentPane().add(label);
		new Thread(this).start();
	}

	private int color = 0;
	private boolean increase = true;
	public void run() {
		while(true) {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					label.setFont(new Font("Serif" , Font.PLAIN , 20));
					label.setForeground(new Color(color));
					label.repaint();
				}
			});
			if (increase) color++;
			else color--;

			if (color == 255) increase = false;
			else if (color == 0) increase  = true;

			try { Thread.sleep(10); }
			catch(InterruptedException err) {
				//例外処理をここに記述します。
			}
		}
	}
}

class LabelEx extends JComponent {
	private String label;
	public LabelEx(String label) { this.label = label; }

	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は、main() メソッドで JFrame を拡張する Test クラスのインスタンスを生成するために、Test() コンストラクタを呼び出しています。Test クラスではウィンドウに新しく LabelEx クラスのオブジェクトを追加し、スレッドを開始させています。

別のスレッドで動作している Test クラスの run() メソッドからでは、Swing コンポーネントにアクセスするのは好ましくありません。そこで、SwingUtilities クラスの invokeLater() メソッドを呼び出し、Runnable インタフェースを実装する匿名クラスのオブジェクトを渡しています。匿名クラスの run() メソッドはイベントディスパッチメソッドに呼び出されるため、Swing コンポーネントにアクセスして問題はありません。

具体的には Swing コンポーネントは画面に描画されるまでは他のスレッドからも制御可能とされていますが、可能ならばシングルスレッドで設計するべきでしょう。

5.6.3 ダブルバッファリング

ダブルバッファリングとは、直接コンポーネントに描画するのではなく、メモリ上のイメージに描画し、このイメージをコンポーネントに上書きすることで、コンポーネントをクリアしたときに生じる画面のちらつきを防止する技術です。アニメーションなど、頻繁に画面を更新する処理では重要になるでしょう。この、メモリ上のイメージをオフスクリーンと呼び、実際の物理デバイスをオンスクリーンと呼ぶこともあります。

Swing では、このダブルバッファリングをデフォルトでサポートしています。そのため、開発者はダブルバッファリングの仕組みを知らなくても、Swing コンポーネントで画面のちらつきが発生することはありませんが、その原理を知らなければ高度な描画処理ができないので、知っておくべきでしょう。

この場では、まず AWT コンポーネントでちらつきを防止するため、ダブルバッファリングの具体的な実装方法を説明します。ダブルバッファリングを行うには、まずメモリ上のイメージが必要です。これは、コンポーネントが最終的に描画する画像ファイルのようなものだと考えてください。オフするリーんのためのイメージは Component クラスの createImage() メソッドを使います。

Component クラス createImage() メソッド
public Image createImage(int width, int height)

width パラメータと height パラメータにはイメージの幅と高さを指定します。通常は、コンポーネントと同じサイズのイメージを生成して描画に備えます。

ダブルバッファリングのための Image オブジェクトが構築できれば、後は、このイメージを参照する Graphics オブジェクトさえあれば、コンポーネントに対する描画と同じコードを用いてオフスクリーンに描画することができます。Image オブジェクトを参照する Graphics オブジェクトは Image クラスの getGraphics() メソッドから取得します。

Image クラス getGraphics() メソッド
public abstract Graphics getGraphics()

このメソッドは Image オブジェクトを参照する Graphics オブジェクトを返します。この Graphics オブジェクトの描画メソッドを使えば参照するイメージに対して図やイメージが描画されます。paint() メソッドで描画していたコードを全て Image に対して描画させ、このダブルバッファリング用のイメージを最終的にコンポーネントに描画すれば、その間の処理は実際の画面からは見えないため、ちらつきがなくなるというわけです。

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

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

public class Test extends Applet implements Runnable {
	private boolean isStop;
	private Image offScreen;

	public void init() { offScreen = createImage(getWidth() , getHeight()); }
	public void start() {
		isStop = false;
		new Thread(this).start();
	}
	public void stop() { isStop = true; }

	private Point pt = new Point(0 , 0);
	private Dimension size = new Dimension(20 , 20);

	private boolean isXInc = true , isYInc = true;

	public void run() {
		while(!isStop) {
			pt.x += isXInc ? 1 : -1;
			pt.y += isYInc ? 1 : -1;

			if (pt.x < 0) isXInc = true;
			else if (pt.x > getWidth() - size.width) isXInc = false;

			if (pt.y < 0) isYInc = true;
			else if (pt.y > getHeight() - size.height) isYInc = false;

			repaint();
			try { Thread.sleep(10); }
			catch(InterruptedException err) { System.out.println(err); }
		}
	}

	public void update(Graphics g) { paint(g); }
	public void paint(Graphics g) {		
		Graphics offg = offScreen.getGraphics();

		offg.setColor(getBackground());
		offg.fillRect(0 , 0 , offScreen.getWidth(this) , offScreen.getHeight(this));
		offg.setColor(getForeground());
		offg.fillOval(pt.x , pt.y , size.width , size.height);

		g.drawImage(offScreen , 0 , 0 , this);
	}
}
実行結果
コード1 実行結果

コード2は AWT のアプレットでアニメーションを行っています。通常であれば、再描画を行うたびにアプレットが背景色でクリアされるため画面がちらつきます。しかし、このプログラムでは update() メソッドをオーバーライドしアプレットをクリアしないように paint() メソッドを直接呼び出しています。そして paint() メソッドでは Image オブジェクトを参照する Graphics オブジェクトを取得し、Image オブジェクトに対して描画処理を行っています。

まず fillRect() メソッドを使って、イメージを背景色で塗りつぶして、前に描画した内容をクリアしています。そして、fillOval() メソッドで丸いボールを指定位置に描画しています。これらの処理を繰り返すことによって、アニメーションが実現されています。

生成したイメージを最後にオンスクリーンに描画することで、イメージに描画した内容が画面に反映されます。最終的に、アプレットはイメージを上書きし続けているだけなので、このような設計は描画処理を一ヶ所に集中させたい場合にも便利でしょう。

Swing コンポーネントは、このダブルバッファリングをデフォルトで搭載しています。つまり paint() メソッドに渡される Graphics オブジェクトは、コンテナを参照しているのではなく、コンテナが保有しているオフスクリーンイメージを参照していると考えられます。

JComponent クラスは、ダブルバッファリングを行うかどうかを設定する setDoubleBuffered() メソッドと、ダブルバッファリングの状態を取得する isDoubleBuffered() メソッドを提供しています。

JComponent クラス setDoubleBuffered() メソッド
public void setDoubleBuffered(boolean aFlag)
JComponent クラス isDoubleBuffered() メソッド
public boolean isDoubleBuffered()

デフォルトではダブルバッファリングは false に設定されていますが、コンテンツペインなど、他のコンポーネントを含むことを前提としたコンポーネントでは、デフォルトでダブルバッファリングが採用されています。全てのコンポーネントが独自にダブルバッファリングを行う必要はありません。Swing は軽量コンポーネントなので、Graphics オブジェクトを提供するコンテナがダブルバッファリングを行っていれば、その子コンポーネントの描画もイメージに対して書き込まれるためです。