第9週:動くアプレット

今週はアプレットに表示された図形や画像などが動く仕掛けを作ります。そのためにはスレッドという考え方を理解する必要があるのですが、理屈を先に理解しようとすると少々ややこしいことになるかもしれません。まずは実際に動くアプレットをつくってみて、それから仕組みを理解してゆきましょう。

9.1 動くアプレットの基本形
9.2 スレッドの考え方とThreadクラス
9.3 動きを工夫してみよう

9.1 動くアプレットの基本形

まずは動くアプレットをつくる際の基本形を押さえましょう。たとえばアプレット上に描画された円が右に移動する、次のようなアプレットをつくってみましょう。

→ このプログラムの実行結果

このアプレットのソースコードは以下の通りです。この中にはスレッドを用いて画像を動かすために必要な最小限の要素が含まれています。これを基本形として、いろいろと動きに工夫を加えてゆきましょう。

/**
 *  第9週:動くアプレット
 * 円が右に移動する
 **/
 
import java.awt.*;
import java.applet.Applet; 
public class  MoveCircle1 extends Applet implements Runnable{

	int x, y;	// 円の表示位置
	Thread th;	// スレッド(=円の表示位置を変化させる処理の流れ)
	boolean stopFlag;	// スレッドの状態(停止/実行)を記憶する

	// initメソッド:アプレットの初期化を行う	
	public void init(){
		
		// アプレットの背景色を白にする
		setBackground(Color.white);
		
		// 円の最初の表示位置の値(=初期値)を与える
		x = 0;
		y = 50;	
	}

	// startメソッド:アプレットの処理を開始する
	public void start(){
	
		// スレッドが空(=存在しない)の時にのみ新しいスレッドを作成、開始する
		if ( th == null ){
			th = new Thread(this);	// 新しいスレッドを作成する
			stopFlag = false;	// スレッドが「実行」の状態にあることを記憶する
			th.start();		// 新しいスレッドの処理を開始する
		}
	}

	//stopメソッド:アプレットの処理を一時停止する
	public void stop(){
	
		// スレッドが存在する場合、処理を破棄する
		if( th != null ){
			stopFlag = true;	// スレッドが「停止」の状態にあることを記憶する
			th = null;		// スレッドを破棄する
		}
	}
	
	
	// runメソッド:円の表示位置を変化させる(スレッドが実行する処理)
	public void run(){
	
		// 無限ループによって、円の表示位置のx座標を変化させ続ける
		while( true ){
		
			x += 10;		// x座標を10ずつ増やす(円は右に移動する)
			if ( x >= 200 ){	// 円がアプレットの右端に来たら、左端に戻す
				x = 0;
			}
	
			repaint();		// 再描画する
			
			try{	// try~catch → 例外処理(解説を参照)
				Thread.sleep(100);	// 処理を一時停止する(時間稼ぎ)
			}
			catch(InterruptedException e){
			}

			if(stopFlag){
				break;		// もしも「停止」状態なら無限ループから抜け出す
			}
		}
	}
	
	
	// paintメソッド:アプレット本体を表示する		
	public void paint(Graphics g){
	
		// 円の描画色を緑に指定する
		g.setColor(Color.green);
		// 直径50ピクセルの円を(x,y)の指定する位置に描画する
		g.fillOval( x, y, 50, 50 );	
	}
}

まずはこのプログラムの記述を追ってみます。今回新しく登場するスレッドという考え方とThreadクラスについては、後ほど整理します。


まず最初に、スレッドの実行に必要なrunメソッドをこのアプレットに実装するために、クラス名の宣言と同時に必ずRunnableインターフェイスというものをimplementsしておきます。このimplementsという手続き自体は、イベント制御の際にやったリスナークラスのimplementsと同じ意味を持っています。今回はリスナーメソッドではなくrunメソッドを利用したいがために、そのメソッドを持っているRunnableインターフェイスをimplementsするのです。

次に円の座標を表すint型のx,yと、今回初登場のThreadクラスのth、さらにスレッドの状態を記憶するboolean型のstopFlagを用意しておきます。x,yを用意するのは円の表示位置がタイムリーに変化してゆくからです。またstopFlagが記憶する「スレッドの状態(停止/実行)」とは、このプログラムで言えば、すなわち「円が移動する」という処理が行われていないかいるか、という2通りの状態を意味しています。stopFlagがtrueすなわちスレッドが「停止」ならば円の移動はストップしていることを、stopFlagがfalseすなわちスレッドが「実行」ならば円の移動が行われていることを意味します。


initメソッドでは、アプレットの初期化作業として背景色の設定と円の最初の表示位置の決定だけを行います。アプレットの背景色はAppletクラスのsetBackgroundメソッドを用いて設定します。このメソッドの引数は、設定したい色に応じて次のように与えます。

Color.色名

色名としてはblack (黒)、blue(青)、cyan(シアン)、darkGray (ダークグレイ)、gray(灰色)、green(緑)、lightGray(ライトグレイ)、magenta(マジェンタ)、orange(オレンジ)、pink(ピンク)、red(赤)、white(白)、yellow(黄色)が選べます。


startメソッドは今回の講座では初登場です。これはinitメソッドが実行された直後に実行されます。また、ブラウザのウインドウがスクロールされたり、他のウインドウの下に隠れるなるなどの事情でアプレットが一旦見えなくなった後、再び画面上に現れる場合も呼び出されます。

このときに行う処理は、ひとつはThreadクラスのオブジェクトの作成です。但し、万が一すでにThreadオブジェクトが存在すると困る(複数のThreadオブジェクトが作成されるとプログラムが正しく動かない)ので、必ずif文で囲み、thの中身がnull(=空っぽ)であることを確かめてから行います。つぎに、stopFlagをfalseにし、スレッドが「実行」状態にあることを記憶します。最後に、作成したThreadオブジェクト(このプログラムではth)を利用して、Threadクラスのstartメソッドを実行します。引数にはアプレット自身、つまりthisを与えます。そうすると、Threadオブジェクトはアプレット自身のrunメソッドを探しだし、そこに書かれている処理を実行します。ここが今回のポイントです。


stopメソッドも初登場です。役割はstartメソッドの逆で、完全に破棄される以外の理由でアプレットが隠れてしまった場合、一旦アプレットの処理を中止します。

ここではスレッドが「停止」状態にあると記憶したのち、Threadオブジェクトを破棄します。まずstopFlagに「停止」を意味するtrueを代入し、その次にthにnullを代入してThreadオブジェクトを破棄します。


runメソッドには、startメソッドで作成されたThreadオブジェクトによって実行される処理を書き込みます。ここに書き込まれた処理は、他のAppletクラスのメソッドのようにブラウザによって直接実行されるのではなく、Threadオブジェクトによって(勝手に)実行されます。

このプログラムでは円の表示位置のx座標を連続的に変化させています。無限ループといって、終了条件が成立する事のないwhile文(注)を用意し、ひたすらxに10を加え続けます。ただしxが200になったらこれを右の端として、xの値を0(すなわち左の端)に戻します。値を変更したらその都度repaintで円を再描画します。

その後にある「try〜catch」の文は、例外処理といって、エラー(例外的な事態)を起こす可能性のある命令に対してあらかじめそのときの処置を定義するやり方です。いくつか(といってもたくさんありますが、)の特定のメソッドの実行の際に、Javaはこの例外処理を併せて記述することを推奨しています。まず、エラーを起こす可能性のある処理(この場合は「Thread.sleep(100);」)を「try{ ... }」のカッコの中に記述します。そして「catch(InterruptedException e){ ... }」のカッコの中に、エラーが起きたときの対処を書いておきます。但し今回のケースでは、特に必要がないので対処するための命令は記述していません。

ここでtry〜catchの中で実行されている「Thread.sleep(100);」という命令は、スレッドの実行を100ミリ秒間停止させるという意味です。これで円の移動する速度を調節します。このThreadクラスのsleepメソッドは特殊なメソッドで、オブジェクト名ではなく、クラス名(つまりThread)のあとにピリオドで記述します(注)

また最後にif文で、stopFlagがtrue(すなわちスレッドは「停止」)の時には無限ループから抜けるという記述をしておきます。if文の( )内にはstopFlagとだけ書かれています。これはstopFlag==trueと全く同じ意味を持ち、stopFlagの値がtrueの時には条件成立で{ }内の処理が行われます。処理として書かれているbreak;は、「ループから抜ける」という命令です。


paintメソッドではGraphicsクラスのsetColorメソッドを用いて円の描画色を緑にしています。先ほどAppletクラスのsetBackgroundメソッドの箇所で紹介した「Color.色名」を、全く同じように引数として与えます。そして、fillOvalメソッドで円を描画します。その際のx、y座標は当然変数になっており、その都度の値に応じた場所へ円を描きます。


練習問題 9-1

上記のアプレットColorCircle1.javaを実際につくってみなさい。また、円が全く逆に右から左へ向かって移動するようにするにはどうしたらよいでしょうか。

9.2 スレッドの考え方とThreadクラス

ここでは今回新しく学ぶスレッドという考え方とThreadクラスについて整理してみましょう。

スレッドとはコンピュータのプログラムにおける「処理の流れ」のことをさします。通常のプログラムでは処理の流れは一つだけしかありません。たとえば今まで作成してきたアプレットでは、ブラウザがあらかじめ決まった順番でアプレットの持っている特定のメソッドを呼び出して、ひとつの処理の流れをつくっていました(この順番については第6週を参照)。このように処理の流れが一つだけであるプログラムのことをシングルスレッドプログラムと呼びます。

ところで、アプレットで動画を実現しようとすると、どうしても「ひたすら再描画を繰り返す」というやり方をとることになります。シングルスレッドでそれを実現しようとすると、処理の流れが再描画の繰り返しに割かれてしまいます。たとえばその間にマウスからの入力を受け付けるなどといった別の処理をしようとすると、非常に複雑なプログラムを書かなければなりません。つまりシングルスレッドでは二つのことを同時にこなすのは非常に困難なのです。

そこで、JavaではThreadクラスを利用することにより、複数の処理の流れを持つプログラム(マルチスレッドプログラム)つくることができるようになっています。先ほど例に挙げたアプレットでは、ブラウザがつくる一般的なアプレットの処理の流れの他に、Threadオブジェクトによる「runメソッドの呼び出し」というもう一つの流れを利用し、「x座標の増加→再描画」のプロセスを繰り返させています。

ここで、もう一度Threadクラスのメソッドについてまとめておきます。

というわけで、動きのあるアプレットを実現する際の基本として、以上の考え方を理解しておきましょう。

9.3 動きを工夫してみよう

先ほどのMoveCircle1.javaに少しだけ手を加えることで、円の動きにもっと面白い変化を加えることができます。例えばpaintメソッドの中にあるfillOvalメソッドの4つの引数のうち、x,y座標は変数でしたが「外接する四角形の幅・高さ」を意味する(すなわち円の直径を決定する)後半の二つには定数100が与えられていました。この二つの値も変数にしてしまえば、円の位置を動かすと同時に、円の直径も動かせそうな気がしますね。

この考え方を応用すれば、次のような動きを表現することができます。

→ プログラムの実行結果

このアプレットでは、円が再描画されるたびにその半径が10ずつ小さくなってゆきます。ただし、半径0になったらもとの大きさに戻り、再び縮小を続けます。これを実現するには、先ほどのプログラムのどこに手を加えたらよいのでしょうか。

/**
 *  第9週:動くアプレット
 * 円が右に移動しながら縮小→復活を繰り返す
 **/
 
import java.awt.*;
import java.applet.Applet; 
public class  MoveCircle2 extends Applet implements Runnable{

	int x, y;	// 円の表示位置
	int w,h;	// 円に外接する四角形の幅、高さ(すなわち円の直径を決定する)
	Thread th;	// スレッド(=円の表示位置を変化させる処理の流れ)
	boolean stopFlag;	// スレッドの状態(停止/実行)を記憶する

	// initメソッド:アプレットの初期化を行う	
	public void init(){
		
		// アプレットの背景色を白にする
		setBackground(Color.white);
		
		// 円の最初の表示位置の値(=初期値)を与える
		x = 0;
		y = 50;
		
		// 円に外接する四角形の幅、高さの初期値を与える
		w = 100;
		h = 100;	
	}


	// startメソッド:アプレットの処理を開始する
	public void start(){
	
		// スレッドが空(=存在しない)の時にのみ新しいスレッドを作成、開始する
		if ( th == null ){
			th = new Thread(this);	// 新しいスレッドを作成する
			stopFlag = false;	// スレッドが「実行」の状態にあることを記憶する
			th.start();		// 新しいスレッドの処理を開始する
		}
	}


	//stopメソッド:アプレットの処理を一時停止する
	public void stop(){
	
		// スレッドが存在する場合、処理を停止し破棄する
		if( th != null ){
			stopFlag = true;	// スレッドが「停止」の状態にあることを記憶する
			th = null;		// スレッドを破棄する
		}
	}
	
	
	// runメソッド:円の表示位置を変化させる(スレッドが実行する処理)
	public void run(){
	
		// 無限ループによって、円の表示位置の直径とx座標を変化させ続ける
		while( true ){
			
			w-=10;		// 円の幅を10ずつ小さくする
			h-=10;		// 円の高さを10ずつ小さくする
			if( w< 10 && h <10 ){	// 円の幅・高さが10より小さくなったら
				w = 100;		// 幅と高さを元の値(100)に戻す
				h = 100;
			}
				
			x += 10;		// x座標を10ずつ増やす(円は右に移動する)
			if ( x >= 300 ){	// 円がアプレットの右端に来たら、左端に戻す
				x = 0;
			}
	
			repaint();		// 再描画する
			
			try{	// try~catch → 例外処理(解説を参照)
				Thread.sleep(100);	// 処理を一時停止する(時間稼ぎ)
			}
			catch(InterruptedException e){
			}
			
			if(stopFlag){
				break;		// もしも「停止」状態なら無限ループから抜け出す
			}
		}
	}
	
	
	// paintメソッド:アプレット本体を表示する		
	public void paint(Graphics g){
	
		// 円の描画色を緑に指定する
		g.setColor(Color.green);
		// 直径50ピクセルの円を(x,y)の指定する位置に描画する
		g.fillOval( x, y, w, h );	
	}
}

そうですね。paintメソッド中にある無限ループの処理内容に、円の幅・高さを減少させる処理を加えればよいわけです。さらにif文を用いれば、幅・高さがともにある値よりも小さくなったときに、元の大きさに戻す仕組みも記述できます。

それでは、次のアプレットはどのようにして実現すればよいでしょうか。

練習問題 9-2

画像が動くアプレットを作成しなさい。ただし動き方は次のようなものとします。

→ プログラムの実行結果

画像はまず右斜め下へと動いてゆき、画面から消えてしまうと再び最初の地点に戻り、右斜め下へと動いてゆきます。無限ループの中で、うまく制御構造を駆使して実現してください。

ヒントとしては、まず第一に、x座標の増加量よりもy座標の増加量を小さくすることです。そうしないとあっという間に画像はブラウザの下方へと消えてしまいます。先ほど見たプログラムの場合、xは10ずつ増加するのに対し、yは2ずつ増加します。もう一つは、if文のネストを用い、画像が右端に来たときのケースをうまく二通りに分けて考えましょう。具体的には以下のように考えます。

if (  ){   // 画像が右端に来た場合で、
        if(  ){    // なおかつ下端に来た場合
             ..............................
         }
         else{    // それ以外(つまり単なる右端)の場合
              ............................
         }
}


>> 目次ページへ