Java グラフィックス

目次

もどる


グラフィックスの基本

グラフィックス用のウィンドウを用意し、Graphics クラスの描画用各種メソッドを呼び出して実行することにより、グラフを表示することが出来る。

ウィンドウを表示する

ウィンドウは、ブラウザを利用するか、 Frame クラスのサブクラスを作成する。

ウィンドウ表示その1 ブラウザを利用する:アプレット

簡易アプリケーションを意味するアプレットを作成すると、ブラウザをウィンドウ代わりにすることが出来る。アプレットビュワーと呼ばれる仕掛けを使って、ブラウザに表示させた場合の様子をプレビューすることが出来る。

アプレットは Applet クラスを継承したサブクラスを定義することで実行できる。paint() メソッドをオーバーライドして、Graphics クラスの基本図形を描画するメソッドを組み合わせて表現する。次はその作例である。

public class MyApplet extends Applet {
	public void paint(Graphics g) {
		g.drawString("Hello Java!", 150, 100);
		g.fillOval(100, 150, 200, 100);
		g.setColor(Color.WHITE);
		g.fillRect(150, 175, 100, 50);	
} }

extends Applet で Applet クラスを継承したサブクラスを定義している。drawString() は文字列を表示する、fillOval() は中を塗りつぶした楕円を描く、setColor() は描画の色を指定する、fillRect() は中を塗りつぶした長方形を描く Graphics クラスのメソッドである。

実行すると、画面左上に「アプレット」というタイトルの小さなウィンドウが表示され、左上に「Hello Java!」という文字列が表示され、その下に一部が白抜きされた黒い楕円(の一部)が表示される。アプレットビュアーのデフォルトのウィンドウサイズは 200 x 200 なので、表示が全部見えない場合は、画面右下をドラッグすれば画面の大きさを変えることが出来る。「閉じる」ボタンをクリックすると終了する。

ウィンドウ表示その2 フレームを作成する:Frame, JFrame

Frame クラスを継承したサブクラスを作り、そのインスタンスを作成して実行させることにより、自分のウィンドウを表示させることが出来る。アプレットの場合に比べて、サイズを決め、それを「見える」化する、という余計な作業(コンストラクタ)が必要になる。また、プログラムを動かすために main() メソッドの定義も必要になる。次はその作例である。

class MyFrame extends Frame {
	// コンストラクタ
MyFrame() { this.setSize(400, 300); // ウィンドウの大きさを設定(横 x 縦、ビット数) this.setVisible(true); // ウィンドウを表示する } // 最初に実行されるメソッド
public static void main(String[] args) { new MyFrame(); } // paint メソッドの書き換え public void paint(Graphics g) { g.drawString("Hello Java!", 150, 100); // 文字列表示 g.fillOval(100, 150, 200, 100); // 塗りつぶし楕円(デフォルトは黒色) g.setColor(Color.WHITE); // 表示色の設定 g.fillRect(150, 175, 100, 50); // 白い長方形 }
}

extens Frame で Frame クラスを継承したサブクラスを定義している。setSize() でウィンドウの大きさを指定し、setVisible() でウィンドウを表示している。main() メソッドでは、このクラスのインスタンスを作成している(new MyFrame())。

実行結果はアプレットの場合と同じである。このクラスにはウィンドウの「閉じる」ボタンをクリックした時の処理が定義されていないので、「閉じる」ボタンをクリックしても何も反応しない。

描画の手順は以下の通り。paint() メソッドを実行する前に repaint() メソッド(描き直し)が呼び出され、次にupdate() メソッド(初期化)、最後に paint() メソッドが呼び出されて、実際の絵を描く。また、ウィンドウサイズが変わったり、別のウィンドウの下になって、再び前面に表示されるなど、ウィンドウの再描画が必要になると、repaint() - update() - paint() に順で常に呼び出される。repaint(), update() は、通常は独自に付け加えること(オーバーライド)はないので、プログラムに書く必要はない。


描画のためのメソッド(Graphics クラス)

基本的な図形や文字列を表示するために、Graphics クラスに様々なメソッドが用意されている。drawXxx() は外形線、fillXxx() は内部を塗りつぶす、という違いがある。

メソッド名 説明    メソッド名 説明
drawLine 直線を引く drawRect, fillRect 長方形
drawPolyline 折れ線を引く drawRoundRect, fillRoundRect 角の丸い長方形
drawString 文字列を表示する drawPolygon, fillPolygon 多角形
clearRect 背景色の長方形(消去) drawOval, fillOval 円、楕円
setColor(Color)
ペンの色を設定、取得 drawArc, fillArc 円弧
setFont(Font) フォントの設定 draw3DRect, fill3DRect 立体長方形(直方体)

その他に、ある領域のビットマップ情報をコピーする copyArea()image オブジェクトを表示する drawImage() などが使われる。create() は、指定した長方形領域の描画用オブジェクトを生成する。dispose() は使用中のシステムリソースを解放する。paint() メソッドを使わない場合にゴミ処理をするためのメソッド。

もどる


座標系

図形や文字は、表示位置を指定するために、決められた座標系を用いる。表示領域(ウィンドウ)は長方形で、左上を原点、水平方向を x 軸、垂直方向を y 軸、右方、下方を正方向とし、ピクセル単位(int 型数)で指定される。
表示領域の大きさを表すために、int 型数を二つ組み合わせた Dimension 型がある。二つの数は width, height によって参照することが出来る(Dimension.width, Dimension.height)。

アプレットやフレームを定義するとき、表示領域の横幅(width)と高さ(height)をピクセル数で指定した setSize() メソッドを使う。たとえば、setSize(800,600);setSize(new Dimension(800, 600)); としても同じ)は、横方向 800 ピクセル、縦方向 600 ピクセルの画面を設定する。

現在使っている画面の横、縦の大きさ(ピクセル数)は this.getWidth(), this.getHeight() によって知ることが出来る。

Frame を継承したクラスを定義した場合、表示されるウィンドウはタイトルバーとして上から30ピクセル、周囲の枠として 8 ピクセル使われているので、実際に使える画面は、左上が (8, 30)、右下が (this.getWidth()-8, this.getHeight()-8) の範囲である。

長方形や円などの図形を描く場合、左上隅が起点となる。左上の座標((x,y))と、図形の大きさ(Dimension(width,height))を指定する。文字列を表示する場合は、左下隅を起点とする長方形領域に描かれる。

数学関数を使うとき、y 軸方向が反転しているので、注意が必要である。例えば、点 (Math.cos(t), Math.sin(t)) は、t を増加させたとき、(反時計回りではなく)時計回りに動く。

もどる


アニメーション

アニメーションは、一定間隔で画面を少しずつ描き変えることによって動きを表現する。無限ループを作って、paint() を繰り返し呼べば良い。

次のプログラムは、ボールが転がる様子を実現したものである。

public class TestAnim extends Frame {
	int x = 0;

	TestAnim() {
		this.setSize(400, 300);
		setVisible(true);
		run();				// アニメーション開始
	}
	public static void main(String[] a) {
		new TestAnim();
	}
	public void run() {
		while (true) {			// 無限ループ
			x = x + 1;
			if (x > 400) x = 0;
			repaint();		// 画面更新
			try {
				Thread.sleep(10); // 画面更新間隔指定
			} catch (Exception e) { }
		}
	}
	public void paint(Graphics g) {
		g.drawOval(x, 100, 50, 50);	// 実際の画像
	}
}

このプログラムを実行すると、円盤が左から右へ転がっているように見える。

プログラムの解説:

プログラムだけ見ると、paint() メソッドによって円が重なって描画されるように見えるが、paint() メソッドは実行する前に repaint(), update() メソッドを実行していったん画面をクリアしてから、そこに描かれているメソッドを実行するので、直前まで描かれていた円は消去され、新たに描かれた円だけが表示される。
その結果として、前の位置から少しだけ動いた円が表示されるので、動いているかのように見える。例えば、 paint() メソッドの中に g.drawOval(x, x, 30, 30); を追加すると、左上から右下へ動く小振りの円も同時に表示される。paint() メソッドの工夫で尤もらしい動きを見せることも可能だが、動きを止めたり、途中から追加したりするという複雑なことを実現しようとすると、別の仕掛けが必要である。

Thread.sleep() メソッドで使われたスレッド Thread の考え方がそれである。

スレッド

コンピュータで、ある仕事を実行するための一連の処理単位はスレッド(thread of execution)と呼ばれる。単一目的のプログラムではスレッドを特に意識することはないが、並行処理する必要がある場合は、複数のスレッドを同時に実行させる場合がある。計算を実行するCPUは一つしか無いので、CPU時間を時分割して各スレッドを順番に処理することになる。

プログラム例:次のプログラムは三つのスレッドを同時に動かした例である。

public class MyThread {
	public static void main(String[] args) {
		new SecondThread().start();
		new ThirdThread().start();
		for(int i=0; i<10; i++) {
			System.out.println("スレッド1:" + i);
			try {
				Thread.sleep(500);
			} catch(Exception e){};
		}
	}
}
class SecondThread extends Thread {
	public void run() {
		for(int i=0; ii<10; i++) {
			System.out.println("     スレッド2:" + i);
			try {
				Thread.sleep(500);
			} catch(Exception e){};
		}
	}
}
class ThirdThread extends Thread {
	public void run() {
		for(int i=0; ii<10; i++) {
			System.out.println("           スレッド3:" + i);
			try {
				Thread.sleep(500);
			} catch(Exception e){};
		}
	}
}

プログラムの解説:

プログラムの実行例は、例えば次のようになる。
     スレッド2:0
スレッド1:0
           スレッド3:0
     スレッド2:1
スレッド1:1
           スレッド3:1
           スレッド3:2
スレッド1:2
(以下 省略)
この例では、3つのスレッドが、2, 1, 3, 1, 2, 3, 3, 2, 1, 3, 1, 2, 1, 2, 3, 2, 1, 3, 1, 3, 2, 2, 1, 3, 2, 3, 1, 2, 3, 1 の順に実行されたが、この順序は実行するたびに違う可能性がある。

Runnable を実装したスレッドの実行

スレッドを停止したり、新規に開始させたりするためには、Thread クラスを継承したサブクラスを作り、run() メソッドをオーバーライドし、wait(), sleep() メソッドを組み合わせて制御の仕組みを作る。あるいは、Thread クラスのインターフェース版の Runnable を実装する。

アニメーションそのものは、上のプログラム例同様、run() メソッドの中に無限ループを作り、その中で repaint() メソッドを実行する。

Runnable インターフェースを使ったプログラムを作るための基本手順は次の通り。

  1. Runnable インターフェースを実装し(implements)
  2. run メソッドを組み込み、
  3. run メソッドの中で、画面の描き変え処理ルーチンと、一時停止用の Thread.sleep() を組み込み、
  4. Thread(this).start() メソッドを実行する

プログラム例

public class TestThread extends Frame implements Runnable {
	int x = 0;

	TextThread() {
		this.setSize(400, 300);
		setVisible(true);
		// このプログラムをスレッドとして実行開始する
		new Thread(this).start();
	}
	public static void main(String[] a) {
		new TestThread();
	}
	public void run() {
		while (true) {
			x = x + 1;
			if (x > 400) x = 0;
			repaint();
			try {
				Thread.sleep(10);
			} catch (Exception e) {
			}
		}
	}
	public void paint(Graphics g) {
		g.drawOval(x, 100, 50, 50);
	}
} 

このプログラムを実行すると、円が左から右へ転がり、あるところまで行ったら左端に戻る、という簡単なアニメーションが表示される。この例のように、1回性のものであれば、Runnable を実装しない最初のプログラム例とほとんど変わらない。実際、変更したのは、Runnabale を実装して、コンストラクタで run() の代わりに new Thread(this).start(); と書き換えただけである。run()Runnable インターフェースに含まれるメソッドをオーバーライドしたものとみなされ、start() によって開始されるから、直接呼び出す必要はない。

プログラムの説明:


Thread クラスを継承したスレッドの実行

Thread クラスを継承したサブクラスを作り、そのインスタンスを組み込むことによってスレッドを実行することも出来る。

基本手順は次の通り。

  1. Thread を継承したサブクラスを作り(run()メソッドを組み込む)、
  2. メインプログラムで、そのサブクラスのインスタンスを作ることにより、スレッドを実行する

プログラム例

public class TestAnim extends Frame {
	int x = 0;

	TestAnim() {
		this.setSize(400, 300);
		setVisible(true);
		new MyThread(this, 0);
		new MyThread(this, 200);
	}
	public static void main(String[] a) {
		new TestAnim();
	}
	public void redraw(int x) {
		this.x = x;
		repaint();
	}
	public void paint(Graphics g) {
		g.drawOval(x, 100, 50, 50);
	}
}
class MyThread extends Thread {
	int x;
	TestAnim ta;
	MyThread(TestAnim ta, int x) {
		this.ta = ta;
		this.x = x;
		new Thread(this).start();
	}
	public void run() {
		while (true) {
			x = x + 1;
			if (x > 400) x = 0;
			ta.redraw(x);
			try {
				Thread.sleep(10);
			} catch (Exception e) { }
		}
	}	
}

プログラムを実行すると、二つの円が左から右に(回転しながら)移動する様子が観察できる。

プログラムの説明:

new MyThread を実行すると、指定された位置から円が動きだすアニメーションが(一つのスレッドとして)表示される。別の初期座標を与えてさらに new MyThread を実行すると、新たな円のアニメーションが別のスレッドとしてスタートする。スレッドスタートのタイミングを制御する仕組み(例えば、一時停止したり、マウスをクリックした位置からアニメーションを始めるなど)を取り込めば、より複雑なアニメーションができる。

スレッドの一時停止、再開

スレッドを一時中断したばあい、wait() を実行する。再開したい場合は、その実行を停止させるために notify() というメソッドを実行する。これらの間には同期がとれていることが必要なので、synchronized 属性を与える必要がある。

プログラム例

public class ThreadPauseResume extends Frame implements Runnable, ActionListener {
	int x = 100;
	boolean pause = false; // 休止状態か否かを記憶する論理変数

	ThreadPauseResume() {
		this.setSize(400, 300);
		// 一時停止、再開を使えるための伝達手段(ボタンクリックで操作)
		Button bpause = new Button("pause");
		bpause.addActionListener(this);
		this.add(bpause, BorderLayout.NORTH);
		Button bresume = new Button("resume");
		bresume.addActionListener(this);
		this.add(bresume, BorderLayout.SOUTH);
		this.setVisible(true);
		new Thread(this).start();
	}
	public static void main(String[] a) {
		new ThreadPauseResume();
	}
	public void run() {
		while(true) {
			// 一時停止状態でない場合のみ、再描画実行
			if (!pause) {
				x = x + 1;
				if(x > 400) x = 0;
				repaint();
			}
			try {
				Thread.sleep(10);
				// 休止状態(pause=true)ならば、何もしないで「待つ」
				synchronized(this) {
					while(pause) wait();
				}
			} catch(Exception e) { }
		}
	}
	// 再開ボタンがクリックされたことを知らせる
	public synchronized void myResume() {
		notify();
	}
	public void paint(Graphics g) {
		g.drawOval(x, 100, 50, 50);
	}
	// ボタンがクリックされたときの処理を定義する
	public void actionPerformed(ActionEvent ae) {
		pause = !pause;
		if (ae.getActionCommand() == "resume") {
			myResume();
		}
	}
}

プログラムを実行すると、ボタンをクリックすることで玉の動きが止まったり再開したり、という動きを実現していることが確かめられる。

プログラムの説明:

スレッドの別の例:流れるコメント文字列表示

動画のコメントのように、文字列を左から右へ移動しながら表示するプログラムの例である。
LabelRunnable インターフェースを実装し、その表示位置を少しずつ右にずらすことにより、表示されるラベルの文字列が右に移動していくアニメーションを実現している。一つの Label がランダムに生成されてから右端に消えていくまでを一つのスレッドとしている。右端に消えたとき、スレッドは停止する。

public class MovingComment extends Frame implements Runnable {

	int id = 0;
	Dimension dim = new Dimension(800, 600);

	MovingComment(String title) {
		this.setSize(dim);
		this.setTitle(title);
		setLocationRelativeTo(null);
		this.addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent we) { System.exit(0); }
		});
		this.setVisible(true);
		new Thread(this).start();
	}

	public static void main(String[] a) {
		new MovingComment("動くコメント");
	}

	// 新規コメントの生成(1秒間に平均1個生成)
	public void run() {
		while (true) {
			if (Math.random() < 0.1) {
				id++;
				int y = id * 40 % (dim.height - 100) + 40;
				Comments com = new Comments(id, y, dim.width);
				add(com);
			}
			try {
				Thread.sleep(100);
			} catch (Exception e) {}
		}
	}

	public void update(Graphics g) {
		paint(g);
	}
}
// Label クラスの拡張、新規ラベルの生成
class Comments extends Label implements Runnable {
	boolean runn = true;
	int id;
	int x, y, width = 100, height = 30, WD;
	String[] comm = {"excellent", "good", "enough", "passably", "miserable", 
		"stupid", "no comment", "amazing", "fantastic", "funny", "OK", 
		"not too bad", "boring", "trash"};
	int NC = comm.length;

	Comments(int id, int y, int WD) {
		this.id = id;
		this.WD = WD;
		this.y = y;
		x = 0;
		// コメントリストから一つをランダムに選択
		this.setText(comm[(int)(Math.random()*NC)]);
		this.setBounds(x, y, width, height);
		new Thread(this).start();
	}

	// 画面から外れるまで(横幅 WD)右へずらしながら表示する
	public void run() {
		while (runn) {
			this.setBounds(x++, y, width, height);
			if (x > WD)
				runn = false;
			try {
				Thread.sleep(10);
			} catch (Exception e) {}
		}
	}
}

もどる


ちらつき防止

アニメーションのように頻繁に描き変える(paint() を呼ぶ)と、画面がちらついて、見にくい。これは、paint() メソッドを実行する場合に、(update() メソッドを実行して)画面をいったん消去してから新たなグラフィックスを描き込む仕組みになっているからである。それを回避する二つの方法を示す。

update() を書き変える

paint() メソッドを呼ぶと、その前処理として update() を実行することになっており、そこで画面の消去処理が行われる。画面消去処理をさせないためには、update() をバイパスするように書き変えてしまえばよい。

public void paint(Graphics g) {
	...
}

public void update(Graphics g) {
	paint(g);
}

ただし、こうすることによって、直前に実行された paint() の内容は画面に残っているので、アニメーションのように画像を少しずつ動かすような描画を繰り返す場合は、軌跡が全部残ってしまう。直前に描いた画像を残すか消すかは作者の責任になる。前の画像を消したければ clearRect() を実行すれば良い。

ダブルバッファリング

paint() を使うと、描画過程がすべて表示されているスクリーン上で実行されるため、ちらつきの原因になる。目に見えない、いわば仮想スクリーンで描画し、完成図だけを表示させるようにすれば、実スクリーンの描き換えは一回ですむことから、ちらつきは少なくなることが期待できる。

目に見えない仮想スクリーンはイメージファイルを利用することで実現できる。Image クラスのインスタンスに Graphics のメソッドを使って描画し、drawImage() メソッドを使ってその仮想スクリーンを一度に描く。

	// イメージファイルを用意する
	Image img = null;
	Graphics g;

	// グラフィックス
	public void paint(Graphics gr) {
		super.paint(gr);
		if (img == null) {					// イメージファイルの初期化
			D = new Dimension(this.getSize());
			img = this.createImage(D.width, D.height);	// 描画用仮想スクリーンを確保
			g = img.getGraphics();				// 実際の描画用 Graphics 
		}
		gr.drawImage(img, 0, 0, this); 				// 仮想スクリーンを表に表示
	}
	// ちらつき防止
	public void update(Graphics g) {
		paint(g);
	}
  // 実際の使用例 public void draw() { g.drawOval(100, 100, 200, 100); repaint(); }

ダブルバッファリングで描画するプログラムを作る手順をまとめると、以下のようになる。

  1. Image のインスタンスを作り、createImage() メソッドで仮想ウィンドウの大きさを設定する
  2. Graphics のインスタンスを作り、描画するウィンドウを仮想ウィンドウに設定する
  3. その仮想ウィンドウに必要な図形を描き込む
  4. paint() メソッドに drawImage() メソッドを組み込む
  5. 再描画させたい場合に repaint() 命令文を書く

画面のスクロール表示

イメージファイルを使うと、copyArea メソッドを利用して画面表示を平行移動(スクロール)させることが出来る。「copyArea(x,y,w,h,xx,yy)」は、左上座標が (x, y) の大きさ w x h の画面を、左上座標が (xx, yy) となる位置にコピーペーストするメソッドである。動いた部分にある残像を消去すればスクロールしたように見える。
次のプログラムは、マウスホイールの動きに応じて画面が上下にスクロールする機能を取り入れたグラフィック用のパネルの例である。

プログラム例

public class MyMouseEvent3 extends Frame {
	Graphics g;
	Dimension dim;						// ウィンドウの大きさ指定
	Point prev, now = new Point(0,0);			// マウス位置を記憶
	
	MyMouseEvent3() {
		this.setSize(400, 400);
		this.addMouseListener(new MyMouseAdapter());
		this.addMouseMotionListener(new MyMouseMotionAdapter());
		this.addMouseWheelListener(new MyMouseWheelListener());
		this.setVisible(true);
		g = this.getGraphics();
		dim = this.getSize();
	}
	public static void main(String[] args) {
		new MyMouseEvent3();
	}
	// マウスクリック時の処理
	class MyMouseAdapter extends MouseAdapter {
		public void mouseClicked(MouseEvent me) {
			Point p = me.getPoint();
			g.drawLine(p.x - 5, p.y, p.x + 5, p.y);
			g.drawLine(p.x, p.y - 5, p.x, p.y + 5);
		}
	}

	// マウスドラッグ時の処理
	class MyMouseMotionAdapter extends MouseMotionAdapter {
		public void mouseDragged(MouseEvent me) {
			prev = now;
			now = me.getPoint();
			if(Math.abs(prev.x-now.x) + Math.abs(prev.y-now.y) > 20) prev = now;
			g.drawLine(prev.x, prev.y, now.x, now.y);
		}
	}
	// マウスホイール回転時の処理
	class MyMouseWheelListener implements MouseWheelListener {
		public void mouseWheelMoved(MouseWheelEvent me) {
			int rt = me.getWheelRotation();
			if(rt > 0) {			// スクロールアップ
				g.copyArea(0, rt, dim.width, dim.height, 0, -rt);
				g.clearRect(0, dim.height-rt, dim.width, rt);
			} else {			// スクロールダウン
				g.copyArea(0, 0, dim.width, dim.height, 0, -rt);
				g.clearRect(0, 0, dim.width, -rt);	
			}
		}			
	}
}

プログラムの説明:

もどる

paint メソッドを使わない描写

簡易的な描画法として、paint() メソッドを使わずに、直接 Graphics クラスのメソッドを実行することも可能である。

class MyMouseAdapter extends MouseAdapter {
public void mouseClicked(MouseEvent me) {
Graphics gr = getGraphics();
x = me.getX();
y = me.getY();
gr.drawString("クリックされました", x, y);
gr.dispose();
}
}

プログラムの説明

3行目の getGraphics()メソッドは this.getGraphics() と書くところを省略したもので、「この」ウィンドウのグラフィックスを描き込む変数(インスタンス)を指定している。

6行目で、そのインスタンスの drawString() メソッドを読んで文字列をウィンドウに表示する。

7行目は、「ゴミをかたづける」という仕事をするメソッド。一般にグラフィックスを利用すると作業用に多くのメモリを使用するために、不要となったメモリを解放してプログラムの負担を軽くする習慣がある。

結局、以下の手順で paint() を使わない描画を実現することが可能となる。

  1. Graphics」のインスタンスを生成して(getGraphics())
  2. そこに描き込み
  3. 最後には解放する(dispose()

このプログラムは paint() メソッドと見かけ上全く同じ動きをするが、ウィンドウサイズを変えると、書いてあるものが消えてしまう。グラフィックスは、環境が変わるとその都度、repaint() - update() - paint() が実行される、という説明をしたが、このプログラムには paint() メソッドがないので、update() で画面を消去したままになってしまうからである。



もどる