GO BACK

Java教室 i-mode篇その4


「Canvasとグラフィック」


 

■Canvasを利用する

 ここまで作成したiアプリは、全てPanelを継承して作られたものでした。Panelは、コンポーネントを組み込んでiアプリを設計する場合にはいいのですが、自分でグラフィックなどを表示するようなことはできません。このような場合には、「Canvas」と呼ばれるコンポーネントを使います。

  このCanvasは、コンテナではなくコンポーネントです。つまり、これにコンポーネントを組み込んだりはできません。503iでは、コンポーネントを使うものとグラフィックを使うものをきっちり分けて考えているようですね。実用的なものならばPanelのほうが便利ですが、ゲームなどを作りたいならCanvasをベースにiアプリを作った方がよいでしょう。

 このCanvasは、もちろんawtではなくcom.nttdocomoのものです。一応、お馴染みのpaintメソッドを持ち、Graphicsインスタンスを使って描画などを行えます。このあたりは感覚的にawtと近いものがありますね。ただし従来使っていたPanelなどとは根本的に異なっている部分が1つあります。それはイベント処理です。Canvasでは、イベントリスナーが使えないのです。従って、イベント関係の処理を根本的に設計しなおす必要があります。まあ、おそらくPanelにCanvasを組み込むなどすればリスナーを利用できるとは思いますが、iアプリは非常にメモリの制約が厳しいですから、不要なクラスは作らない方がいいでしょう。そこで、「Canvasをベースにしたiアプリ」の基本について考えてみることにしましょう。

 とりあえず、実際にCanvas利用のiアプリを作ってしまいましょう。グラフィックを表示する、ごく簡単なiアプリを作ってみることにします。

 

import com.nttdocomo.ui.*;
public class Test4 extends IApplication {
public void start() { Display.setCurrent(new Test4Frame(this)); }
}
class Test4Frame extends Canvas { IApplication app;
public Test4Frame(IApplication a){ app = a; setSoftLabel(Frame.SOFT_KEY_1, "Exit"); }
public void paint(Graphics g) { g.lock(); g.clearRect(0,0,Display.getWidth(),Display.getHeight()); g.setColor(g.getColorOfRGB(255,0,0)); g.drawString("Hello !",20,30); g.setColor(g.getColorOfRGB(0,255,0)); g.drawRect(20,50,30,30); g.setColor(g.getColorOfRGB(0,0,255)); g.fillRect(70,70,30,30); g.unlock(true); }
public void processEvent(int type,int param) { super.processEvent(type, param); if (type == Display.KEY_RELEASED_EVENT){ switch(param){ case Display.KEY_SOFT1: app.terminate(); break; } repaint(); } }
}

 

 これがそのソースコードです。「Test4.java」というファイル名で保存し、コンパイルして下さい。できたら、i-JADEエミュレータで実行してみましょう。――これは、テキストと四角い図形を表示するだけのシンプルなプログラムです。機能と呼べるものは、「Exit」のソフトキーでプログラムを終了することだけしかありません。

 ここでは、2つの新しい概念が登場します。1つはGraphicsオブジェクトによる描画、もう1つはprocessEventによるイベント処理です。Graphicsについてはなじみがありますが、processEventは「なんかわからないぞ」と思う人は多いでしょう。実はJava2にもこういうのあるんですけど、普段はリスナー使ってるから知らない人もいるかも知れませんね。

 

■paintとGraphicsオブジェクト

 このCanvasでは、paintメソッドで画面の描画を行ないます。注意して欲しいのは、Canvasの場合、paintは「使える」のではなく「必須」である、ということです。paintメソッドを書いてないと、コンパイル時にエラーが起きます。――では、paintメソッドで行なっていることを見てみましょう。

	public void paint(Graphics g) {
		g.lock();
		g.clearRect(0,0,Display.getWidth(),Display.getHeight());
		g.setColor(g.getColorOfRGB(255,0,0));
		g.drawString("Hello !",20,30);
		g.setColor(g.getColorOfRGB(0,255,0));
		g.drawRect(20,50,30,30);
		g.setColor(g.getColorOfRGB(0,0,255));
		g.fillRect(70,70,30,30);
		g.unlock(true);
	}

 このCanvasのpaintメソッドでも、パラメータにGraphicsインスタンスが渡され、これを使って描画を行ないます。が、このGraphicsは、通常のawtのGrahicsとはかなり違います。まず、用意されているメソッドがかなり違う(少ない)ということ、そして「ダブルバッファリング」を標準でサポートしているということです。

 ここで最初に行なっている「g.lock()」というのがそのためのメソッドです。lockを実行すると、以後の描画処理は全てオフスクリーンのバッファに行なわれます。そして最後に「g.unlock(true)」でバッファが画面に描画され、表示が更新されます。

 unlockにはbooleanのパラメータが1つありますが、これは全バッファを強制的にフラッシュするかどうかを示すものです。実はこのlockは、複数回呼び出せるのです。lockするとバッファが作られ、そこで更にlockすると更にバッファが作られ…という具合に作成され、unlockすると最後のものから順に解除していきます。unlock(true)とすると、全バッファを強制的に全て書き出します。まあ、複数バッファを使わない限り、これはtrueでもfalseでも動作に違いはありません。

 このlock/unlockを使わなくとも、もちろん描画は行えます。が、この例のような簡単なものならばいいんですが、ある程度複雑なものになると、描いている過程がちらちらと見えてしまうんですね。なので、「描画の際は、特に理由がない限りlock/unlockする」と考えたほうがよいでしょう。

 さて、実際の描画処理を見てみます。まず、「g.clearRect」で全体をクリアしていますね。――このCanvasがawtなどと違う点の1つに「再描画の際に全体がクリアされない」ということがあります。一応、このCanvasでもrepaint()で画面を再描画したりできるんですが、これを実行しても前に描いたものは消えないのです。ですから、paintの際にまずclearRectしておかないと、前の表示に新たに描き足されるようになってしまいます。

 このclearRectは、クリアする領域を横位置,縦位置,横幅,縦幅の順で指定します。ここでは、横幅と縦幅を指定するのに「Display.getWidth()」「Display.getHeight()」というものを使っていますね。Displayは先に触れましたが、デバイスのスクリーンやキーパッドに関する情報を得るためのものです。ここにあるgetWidth()とgetHeight()で、液晶スクリーンの横幅と縦幅が得られるのです。

 503iでは、液晶スクリーンのサイズは横120×縦130ドットです。が、これは「この大きさ固定」と決まっているわけではないようです。ので、将来的にもっと液晶サイズが大きくなったりした場合、これらで大きさを調べながら、それに合わせて表示をするような必要が生じるかも知れませんね。

 さて、その後で行なっている処理は、描画と色の設定のメソッドを交互に呼び出しているだけです。まず、awtとほとんど変わらない描画メソッドから触れておきましょう。今回使っているのは、以下のようなものですね。

g.drawRect(《横位置》,《縦位置》,《横幅》,《縦幅》);
g.fillRect(《横位置》,《縦位置》,《横幅》,《縦幅》);
g.drawString(《描画するテキスト》,《横位置》,《縦位置》);

 いずれもawtのGraphicsでお馴染みのものですからすぐわかるでしょう。この他、 drawLineやdrawPolyline、fillPolygonといったメソッドも用意されています。ただし多角形はint配列をパラメータに指定するもののみで、Polygonインスタンスは使えません。

 ここまで読んで、「あれ? 円を描くdrawOvalは?」と思った人もいることでしょう。実は、このGraphicsには円を描くメソッドはないのです。そう、503iでは円は描けないのです!

 その理由を理解するには、携帯Javaの環境について理解しておく必要があります。503iや、Java2 Micro Editionで規定されている携帯Java用の仕様では、まずベースにKVMという仮想マシンがあり、その上にCLDCというライブラリがあります。そして、503iならばその上にDoCoMoの専用ライブラリが、J2MEの標準仕様ではMIDPが乗せられているわけです。――重要なのは、この土台となっているCLDCでは、浮動小数がサポートされておらず、整数しか使えない、ということです。いえ、そもそも仮想マシンであるKVM自体に浮動小数を処理する機能がないのです。

 円の描画を行なうには小数の演算が必要なはずです。整数だけを使って円を描画するアルゴリズムもあるのですが、おそらく円の描画メソッドがない理由はこのあたりにあるのではないでしょうか。三角関数も対数も平方根も、小数を必要とするものは全て使えないのですから…。

 残る色の設定に進みましょう。――色については、awtと同様、setColorで設定して描画を行なえば、その色で図形が描かれるようになっています。が、重要なのは「503iでは、new Colorでインスタンスを作成できない」ということです。Colorなどは非常に頻繁に利用されますから、その度にインスタンスをnewされていたのではあっという間にメモリ不足に陥りそうです。そのあたりを考え、禁じてあるのでしょうね。

 ではどうするのかというと、Graphicsの「getColorOfRGB」というメソッドを使います。これで赤・緑・青の各色の輝度を0〜255の整数で指定すると、それにもっとも近い色を、利用可能な色の中から選んで返します。――503iはフルカラーではありません。それに機種によって表示できる色数などが違ってくる可能性もあります。それで、このような形をしてあるのでしょうね。

 なお、Graphicsには、多用されるColorの値があらかじめ用意されています。私たちがawtで「Color.red」などとやって色を設定できるように、503iでも「Graphics.RED」というようにして色が取りだせます。――が、どうもこれらを使って色設定をすると、思うような色になってくれないという現象が確認できました。もし、このあたり原因がわかる方がいましたら教えて下さいませ。

 

■processEventによるイベント処理

 さて、Canvasで登場するもう1つの新しい概念、それが「processEventによるイベント処理」です。サンプルのソースコードを見ると、このようなメソッドでイベント処理を行なっていました。

	public void processEvent(int type,int param) {
		super.processEvent(type, param);
		if (type == Display.KEY_RELEASED_EVENT){
			switch(param){
			case Display.KEY_SOFT1:
				app.terminate();
				break;
			}
			repaint();
		}
	}

 このprocessEventは、全てのイベントが発生した時に呼び出される、いわばイベント処理のもっともベースとなるものです。このメソッドでは2つのパラメータがあります。1つ目は、イベントの種類を示す値、2つ目はイベントが必要とするパラメータなどが、発生したイベントに応じて送られます。

 イベントの種類は、Displayクラスに用意されている値を使って調べます。主な値としては、以下のようなものがあります。

KEY_RELEASED_EVENT キーパッドを離した
KEY_PRESSSED_EVENT キーーパッドを押した
RESUME_VM_EVENT プログラムがリジュームされた
RESET_VM_EVENT プログラムがリセットされた
UPDATE_EVENT 画面がアップデートされた
TIMER_EXPIRE_EVENT タイマー関係のイベント

  2番目のパラメータは、イベントの種類によって違います。ここではKEY_RELEASED_EVENTイベントの際の処理をしていますが、キーパッド関係ではこの第2パラメータに、イベントが発生したキーを示す値が収められます。これも、Displayクラスに用意されている値で調べることができます。用意されているものとしては以下のようなものがあります。

KEY_SOFT1
KEY_SOFT2
KEY_UP
KEY_DOWN
KEY_RIGHT
KEY_LEFT
KEY_SELECT
KEY_0
KEY_1
KEY_2
KEY_3
KEY_4
KEY_5
KEY_6
KEY_7
KEY_8
KEY_9
KEY_ASTERISK
KEY_POUND

 まあ、これは見ればわかりますよね? processEventのパラメータがこれらの値かどうかを調べれば、どのキーが押されたのかわかるというわけです。ここではswitch構文で「case Display.KEY_SOFT1:」として左のソフトキーが押されていた場合にはterminateで終了するようにしてあったのですね。そして最後にrepaintを呼び出して画面を更新しておしまいです。まあ、ここでは特に何も処理らしいことはしてないんでなくてもいいんですが、何か表示の処理をするような場合は、こうして最後にrepaintしておきます。

 この他に、もう1点、重要なことがあります。メソッドの一番最初を見て下さい。「super.processEvent(type, param);」というので、スーパークラスにprocessEventを送っていますね? これは実は重要です。これを忘れると、使っていないイベントも全て503iが認識できなくなってしまいます。processEventは全てのイベントを受けとることを忘れないで下さい。

 このprocessEventを使ったイベント処理は、使うイベントが多くなるとかなりわかりにくくなりますが、リスナーなど余計なオブジェクトを組み込まなくて済むので、慣れてしまえば割とわかりやすいものです。ゲームなどでは「イベント処理=キーパッドの処理」といっていいでしょうから、上のif (type == Display.KEY_RELEASED_EVENT)でイベントの種類を調べ、switchで各キーごとに分岐する、というやり方が基本だ、と考えてしまっていいでしょう。

 

■「8パズル」ゲームを作る!

 それでは応用問題として、Canvasを使った簡単なiアプリを作ってみることにしましょう。やっぱり、グラフィックが使えるとなればゲームですよね。そこで「8パズル」というのを作ってみます。――皆さん、「15パズル」というのは知ってますよね? 4×4のマス目に1〜15の数字のコマが並んでて、1つずつ動かしてきれいに番号順にするという、アレです。

 「8パズル」は、その縮小版。3×3のマス目に1〜8の数字がランダムに配置されます。これを、上下左右のキーパッドで動かして順番に並べるというものです。なぜ、4×4でなく、わざわざ3×3にしたのか。理由は、503iの画面が小さいからではありません。私が生まれてこの方、15パズルを解けたことが1度もないからであります。自分で解けないものを作っても面白くないでしょ?

 

import com.nttdocomo.ui.*;
import java.util.*;
public class MiniPuzzle extends IApplication {
public void start() { Display.setCurrent(new MiniPuzzleFrame(this)); }
}
class MiniPuzzleFrame extends Canvas { IApplication app; Random rnd; int[][] data; int zeroX,zeroY; boolean flg;
public MiniPuzzleFrame(IApplication a){ app = a; rnd = new Random(); data = new int[3][3]; flg = false; setSoftLabel(Frame.SOFT_KEY_1, "Exit"); setSoftLabel(Frame.SOFT_KEY_2, "PLAY"); }
public void paint(Graphics g) { g.lock(); g.clearRect(0,0,Display.getWidth(),Display.getHeight()); if (!flg){ g.drawString("8 Puzzle Game",20,40); g.drawString("by Tuyano.",30,60); } else{ for(int i=0;i<3;i++){ for(int j=0;j<3;j++){ g.drawRect(i*30+15,j*30+15,28,28); if (data[i][j]!=0){ g.drawRect(i*30+17,j*30+17,24,24); g.drawString(Integer.toString(data[i][j]),i*30+27,j*30+35); } else { g.fillRect(i*30+17,j*30+17,25,25); } } } } g.unlock(true); }
public void move(int x,int y){ int x1 = x + zeroX; int y1 = y + zeroY; if (x1 < 0) x1 = 0; if (y1 < 0) y1 = 0; if (x1 > 2) x1 = 2; if (y1 > 2) y1 = 2; int n = data[zeroX][zeroY]; data[zeroX][zeroY] = data[x1][y1]; data[x1][y1] = n; zeroX = x1; zeroY = y1; }
public void processEvent(int type,int param) { super.processEvent(type, param); if (type == Display.KEY_RELEASED_EVENT){ switch(param){ case Display.KEY_SOFT1: app.terminate(); break; case Display.KEY_SOFT2: int n = 0; for(int i=0;i<3;i++){ for(int j=0;j<3;j++){ data[i][j] = n; n++; } } int a1,b1,a2,b2; for(int i=0;i<25;i++){ a1 = Math.abs(rnd.nextInt() % 3); b1 = Math.abs(rnd.nextInt() % 3); a2 = Math.abs(rnd.nextInt() % 3); b2 = Math.abs(rnd.nextInt() % 3); n = data[a1][b1]; data[a1][b1] = data[a2][b2]; data[a2][b2] = n; } for(int i=0;i<3;i++){ for(int j=0;j<3;j++){ if (data[i][j]==0){ zeroX = i; zeroY = j; } } flg = true; } break; case Display.KEY_UP: move(0,1); break; case Display.KEY_DOWN: move(0,-1); break; case Display.KEY_RIGHT: move(-1,0); break; case Display.KEY_LEFT: move(1,0); break; } repaint(); } }
}

 

 これが、ソースコードです。「MiniPuzzle.java」として保存し、コンパイルして下さい。i-JADEエミュレータで実行しているところが以下の図です。「PLAY」を押すと、ランダムに数字が並べられます。後は、上下左右のキーを押して数字を動かしていきます。

 ゲームの基本的な部分だけしか作ってませんので、カラーなども全てモノクロのまま、解けた時のクリアのチェックなども全くしてません。単に「スタートするとランダムに数字を並べ、キーでそれを動かす」というだけのものです。

 それなりに長くなってしまいましたが、新しい要素は特にありませんので詳しい説明は行ないません。それぞれでどんなことをやっているか考えてみるのもよいでしょう。とはいえ、ランダムにコマを初期化するところなど、かなりベタなやり方をしてますので、あんまりいい見本ではないですが。

 1つ補足しておくと、「java.util」というパッケージをimportしていますが、これはRandomクラスで乱数を使うためです。このjava.utilパッケージは、実は皆さんが普段使っているjava.utilとは違います。いえ、作る時は同じなんですが、503iで動かす時には、CLDCにあるjava.utilが使われるのです。

 CLDCには、このようにJava SEと同名のパッケージが用意されています。同じ名前でも、中身はサブセットになっていて、通常のパッケージのごく一部しか実装されていません。ですから使えないものもたくさんあります。このへん、混乱しないように注意しましょう。携帯Javaで使えるライブラリは、あくまでCLDCのライブラリで、Java2 SEのものではありません。

 上記のソースコードの概略がわかったら、これを元に、もっとカラフルにしたり、クリアしたら何か表示するようにしたり、いろいろと改良して、もっとちゃんとしたゲームらしいものにしてみましょう!

※今回作成した「8パズル」のiアプリは、以下のURLにアップしてあります。
 動作はN503iで確認してあります。503iをお持ちの方、アクセスしてみて下さい。

http://www.h5.dion.ne.jp/~tuyano/i_test/



GO NEXT


GO HOME