WisdomSoft - for your serial experiences.

4.2 再描画

コンポーネントの内部を repaint() メソッドで再描画します。アニメーションなどのために、頻繁に画面を処理する必要がある場合、プログラムから明示的にコンポーネントを再描画し、画面を再構成できます。

4.2.1 コンポーネントを更新する

アニメーション処理を実現したり、何らかの理由でコンポーネントの描画をやり直したい場合は、特定のタイミングでコンポーネントを再描画する必要があります。コンポーネントを再描画するということは、最終的に paint() メソッドを呼び出すということです。しかし、paint() メソッドを直接呼び出すことはできません。なぜならば、このコンポーネントを参照する Graphics クラスの実体を生成することができないためです。では、どうやって paint() メソッドを呼び出すのでしょうか。

コンポーネントを再描画したい場合は、まずその Component オブジェクトの repaint() メソッドを呼び出します。

Component クラス repaint() メソッド
public void repaint()
public void repaint(long tm)
public void repaint(int x, int y, int width, int height)
public void repaint(long tm, int x, int y, int width, int height)

通常は、パラメータを受け取らない repaint() メソッドを呼び出します。何も指定しない場合は可能な限り速やかに再描画処理が行われますが、tm パラメータを指定した場合は指定したミリ秒以内に再描画することを表します。つまり、システムに対して再描画処理を行うまでの猶予を与えるということです。

x パラメータ、y パラメータ、width パラメータ、height パラメータは、このコンポーネントの再描画する矩形領域を指定します。この値を指定すると paint() メソッドに渡される Graphics オブジェクトのデフォルトの現在のクリップに影響されます。つまり、指定した矩形領域だけが再描画され、そのほかの部分は再描画されません。近年のコンピュータは確かに目覚しい処理速度の向上がありましたが、それでもグラフィカルな処理は負担になります。再描画が必要な部分を特定できるのであれば、コンポーネント全体を再描画するよりも速度の向上が期待できます。

repaint() メソッドが呼び出されると、コンポーネントは自分自身の update() メソッドを呼び出します。update() メソッドはコンポーネントを背景色でクリアし、paint() メソッドを呼び出します。

Component クラス update() メソッド
public void update(Graphics g)

update() メソッドはコンポーネントを更新します。paint() メソッド同様に Graphics オブジェクトを引数に受けるため、直接呼び出すことはできません。repaint() メソッドから update() メソッドを呼び出すのです。

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

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

public class Test extends Applet implements Runnable {
	//true ならスレッドを停止する
	private boolean isStop;

	public void start() {
		isStop = false;

		Thread thread = new Thread(this);
		thread.start();
	}
	public void stop() {
		isStop = true;
	}

	//ボールの位置とサイズ
	private Point pt = new Point(0 , 0);
	private Dimension size = new Dimension(20 , 20);

	//true なら位置をインクリメント、そうでなければデクリメント
	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 paint(Graphics g) {
		g.fillOval(pt.x , pt.y , size.width , size.height);
	}
}
実行結果
コード1 実行結果

コード1はアニメーションを AWT のアプレットを用いて実現したものです。repaint() メソッドでコンポーネントを一定時間間隔で更新し、描画する図形を少しずつ変化させれば、ユーザーの目にはそれが動いているかのように見えるはずです。ただし、低速なコンピュータと高速なコンピュータでの実行を比較するとアニメーション速度は変化するので注意してください。

このプログラムは Runnable インタフェースの run() メソッドを実装しています。そのため Thread コンストラクタに Test クラスの参照を渡して別のスレッドとして run() メソッドを起動することができます。このプログラムのアニメーションは長期的なループとなるため、主スレッドではなく新しいスレッドを使って処理しています。短い処理ならば問題ありませんが、制御を返さない無限ループを主スレッドで作ってしまうと、入力などの処理に応答できなくなってしまうでしょう。

paint() メソッドでは fillOval() メソッドを使って pt と size オブジェクトが提供する座標とサイズで楕円を描画するだけです。アニメーション用のスレッドで楕円の座標を操作し、reapint() メソッドでコンポーネントを更新しています。低速なコンピュータでも快適に動作させたいのであれば、repaint() メソッドの呼び出しで再描画する矩形を最小の範囲で指定することで更なる最適化を行うこともできます。

また、アプレットが停止したときは無駄にスレッドを動かさないように stop() メソッドが呼び出された時点でスレッドを停止し、再びアプレットがスタートして start() メソッドが時点でもう一度スレッドを構築するような仕掛けを採用しています。アプレットを起動しているブラウザや、アプレットビューワを最小化するなど、アプレットの機能が停止したときにはスレッドを停止して、再び表示したときにスレッドが走り出します。

4.2.2 画面のクリア

ところで、コード1のような方法でアニメーションを行うと、repaint() メソッドを呼び出すたびに画面がちらつくことに気づいたでしょうか。これは、repaint() メソッドの呼び出しによってコンポーネントが再描画されるときに update() メソッドが Component オブジェクトに設定されている現在の背景色で全体を塗りつぶしているためです。画面を塗りつぶしたその瞬間から、楕円を描画するまでの一瞬が表示されることによって、私たちの目には画面がちらつくように見えたのです。

これを根本的に回避するには、メモリ上のイメージに図を描画して全ての描画処理が終了した時点でメモリイメージをコンポーネントに表示するダブルバッファリングと呼ばれる手法が一般的です。ただし、ダブルバッファリングはメモリ上にコンポーネントと同じサイズのイメージを生成するため、主記憶装置の記憶領域を消費します。ダブルバッファリングの詳細は Swing で紹介します。

update() メソッドをオーバーライドすれば、描画時にコンポーネントが背景色でクリアされる処理を省略することができます。update() メソッドをオーバーライドする場合、paint() メソッドを呼び出すことを忘れないでください。paint() メソッドを呼び出すのは常に update() メソッドからです。

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

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

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

	public void start() {
		isStop = false;

		Thread thread = new Thread(this);
		thread.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) {
		g.fillOval(pt.x , pt.y , size.width , size.height);
	}
}
実行結果
コード2 実行結果

コード2は、コード1に update() メソッドのオーバーライドを追加しただけのものです。update() メソッドは本来、コンポーネントの背景色でコンポーネント全体を塗りつぶしてクリアしますが、オーバーライドした update() メソッドでは直接 paint() メソッドを呼び出し、update() メソッドでは何もしていません。その結果、repaint() メソッドを呼び出してコンポーネントを再描画しても、以前に表示されていたコンポーネントの図形が消されないまま新しい図形が書き加えられます。