目次
グラフィックス用のウィンドウを用意し、Graphics
クラスの描画用各種メソッドを呼び出して実行することにより、グラフを表示することが出来る。
ウィンドウは、ブラウザを利用するか、 Frame
クラスのサブクラスを作成する。
簡易アプリケーションを意味するアプレットを作成すると、ブラウザをウィンドウ代わりにすることが出来る。アプレットビュワーと呼ばれる仕掛けを使って、ブラウザに表示させた場合の様子をプレビューすることが出来る。
アプレットは 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 なので、表示が全部見えない場合は、画面右下をドラッグすれば画面の大きさを変えることが出来る。「閉じる」ボタンをクリックすると終了する。
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
クラスに様々なメソッドが用意されている。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); // 実際の画像 } }
このプログラムを実行すると、円盤が左から右へ転がっているように見える。
プログラムの解説:
run()
メソッドを呼んでいる。run()
は repaint()
と Thread.sleep(10)
を無限ループで実行し続ける。repaint()
は画面を描き換えるメソッド、Thread.sliip(10)
は実行を 10 ミリ秒停止するメソッドである。paint()
メソッドでは、x の値が変更され、新しい x の値を使って円を描いている。その結果、1秒間に100 回の描き換えが行われ、その都度円の左上座標が変化するので、円が左から右へ動いているように見える。try ... catch
は sleep()
メソッドで生じる割込みを無視するために使われている。プログラムだけ見ると、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){}; } } }
プログラムの解説:
main
メソッドに書かれたfor文を実行するのが1本目のスレッドで、この場合は普通に書けば良い。2本目(以降)のスレッドを走らせるには、Thread
クラスを継承したサブクラスを作り、run
メソッドでプログラム内容を記述する。for
ループに含まれる「Thread.sleep(10)
」は、500ミリ秒間の実行停止命令を表す。try ... catch
は Thread.sleep
メソッドで生じる割り込みをスルーするための構文である。Thread.sleep(500)
も、開始から500ミリ秒だけ実行される別のスレッドである。スレッド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
インターフェースを使ったプログラムを作るための基本手順は次の通り。
Runnable
インターフェースを実装し(implements
)run
メソッドを組み込み、run
メソッドの中で、画面の描き変え処理ルーチンと、一時停止用の Thread.sleep()
を組み込み、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()
によって開始されるから、直接呼び出す必要はない。
プログラムの説明:
Runnable
を実装(implements
)する。new Thread(this).start()
を実行することでスレッドが開始され、run()
メソッドが呼び出される。この run()
メソッドは(前のプログラムと違い) Runnable
インターフェースのそれをオーバーライドしたものとみなされる。run()
メソッド、paint()
メソッドは前のプログラムをそのまま使っている。
Thread
クラスを継承したスレッドの実行 Thread
クラスを継承したサブクラスを作り、そのインスタンスを組み込むことによってスレッドを実行することも出来る。
基本手順は次の通り。
Thread
を継承したサブクラスを作り(run()
メソッドを組み込む)、プログラム例
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) { } } } }
プログラムを実行すると、二つの円が左から右に(回転しながら)移動する様子が観察できる。
プログラムの説明:
MyThread
は Thread
クラスを継承したサブクラスで、コンストラクタで呼び出し元のクラス名を記憶し、初期座標 x を設定し、Thread(this).start()
メソッドを呼ぶことで、新たなスレッドを開始させる。run()
メソッドは Runnable
実装の場合と同じで無限ループを作るが、ここでは座標を更新するだけで、描画用の命令は直接書かず、更新された座業を引数として元のクラスのメソッド redraw()
を呼び出している。そのため、redraw() をアクセスできるように元のクラス名を記憶しておくことが必要である。run()
の代わりに、この redraw()
メソッドを定義すれば良い。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(); } } }
プログラムを実行すると、ボタンをクリックすることで玉の動きが止まったり再開したり、という動きを実現していることが確かめられる。
プログラムの説明:
pause
を使って、それが false
の間だけ(if(!pause)
)描画する、としておけばよい。 停止している間 wait()
メソッドを実行する。notify()
メソッドを実行するが、必要な処理を間違いなく実行するために、他のスレッドの実行を一時停止させる必要がある。特定のプログラムを他に優先して処理することを排他制御といい、synchonized
属性を指定する。myResume
メソッドがそれである。synchronized(this)
はそれに続く命令文群を排他的に処理することを意味するメソッドであるが、wait()
はこれを指定しなくても正常に働くように見える。 動画のコメントのように、文字列を左から右へ移動しながら表示するプログラムの例である。
Label
に Runnable
インターフェースを実装し、その表示位置を少しずつ右にずらすことにより、表示されるラベルの文字列が右に移動していくアニメーションを実現している。一つの 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(); }
ダブルバッファリングで描画するプログラムを作る手順をまとめると、以下のようになる。
Image
のインスタンスを作り、createImage()
メソッドで仮想ウィンドウの大きさを設定するGraphics
のインスタンスを作り、描画するウィンドウを仮想ウィンドウに設定するpaint()
メソッドに drawImage()
メソッドを組み込む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); } } } }
プログラムの説明:
MouseWheelListener
は、マウスホイールが動いたとき割り込み処理を行うリスナーで、MouseWheelEvent
イベントを生成し、mouseWheelMoved
メソッドで処理する。ホイールの回転量を getWheelRotation
で知ることが出来る。なぜか、無名クラスで定義しないとエラーになる。
簡易的な描画法として、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()
を使わない描画を実現することが可能となる。
Graphics
」のインスタンスを生成して(getGraphics())dispose()
)このプログラムは paint()
メソッドと見かけ上全く同じ動きをするが、ウィンドウサイズを変えると、書いてあるものが消えてしまう。グラフィックスは、環境が変わるとその都度、repaint() - update() - paint()
が実行される、という説明をしたが、このプログラムには paint()
メソッドがないので、update()
で画面を消去したままになってしまうからである。