アットウィキロゴ

Java独学ノート > その8?:GUIを作ってみよう

GUIの勉強を始めたのでちょっとずつ書いていきます...

GUIとは

今まで、コマンドプロンプトで色々やってきました。
コマンドプロンプトの特徴として、文字だけで構成されている事が挙げられます。
このように文字だけで人に認識できるようにしたユーザーインターフェース、つまり人とコンピュータがやり取りする画面をCharacter User Interface、略してCUIと言います。
Characterは文字を意味します。基本型のcharはCharacterのCharから来ています。

一方、皆さんが使うウィンドウ、例えばインターネットのページを見るブラウザ、ゲーム、メッセージダイアログその他諸々は、文字だけではなく図形や絵で構成されています。
このように、文字に限らず色々な見た目のものが自由自在に配置してあるユーザーインターフェイスをGraphical User Interface、略してGUIと言います。

昔、PCが誕生した頃はCUIが基本でした。
今でも、起動したときに環境ややりたい事によって(PCの復元とか)は文字だけが表示される画面が出てきたり、あのブルースクリーンなんかも文字だけで構成されていたりとCUIで作られているシステムは色々存在しますし、
コマンドプロンプトは、言ってしまえばPCが誕生した頃のPCを再現したようなものです。

昔はGUIなんてやれるほどのコンピュータの能力は無いですし、ファミコンだって、あれは文字の集まりが動いているようなものです*1
それが今はコンピュータの進化や効率の良い描画の処理方法が開発されていって、一般人が触るPCのアプリはほぼ全てGUIになり、知識の浅い人でもPCを扱いやすくなった、といった具合でしょうか。

Javaでは、初代から存在するAbstract Windowing Tools、略してAWTと呼ばれる、GUIを作るAPIや、その上位互換(パフォーマンス以外は)であるSwingなどが存在します。
基本はAWTを学んで、そこでGUI作りのいろはを覚え、そこに付け加える形でSwingを覚えるというのが一般だと思われます。
自分の場合は結構貧乏性なので、極力AWTで作りたいってのもあってAWTの限界はよく知っておきたい所です。

GUIの構成

コンポーネント(Component)とコンテナ(Container)

JavaのGUIとして基本となるAPIは、主にjava.awtというパッケージに入っています。

GUIを構成するもの、例えばGUIに表示される文章や入力スペース、画像、チェックボックスなどなど、それらは原則java.awt.Componentというクラスを継承しており、コンポーネントと呼ばれています。
ComponentのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Component.html

しかしコンポーネントだけではまとまりがありません。
これらをまとめるもの、逆を言えば、これらを中に入れて管理できるものは原則java.awt.Containerというクラスを継承しており、コンテナと呼ばれています。
ContainerのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Container.html
主なものにウィンドウがあります。
ウィンドウを人がカーソルで移動させれば、その中の文章や画像は置いてけぼりにされずに動きますよね。それはウィンドウというコンテナが管理していて一緒に動かされるからと考えてください。

java.awt.Containerはjava.awt.Componentを継承しています。つまり、コンテナはコンポーネントの一種でもあるのです。
絵を描くアプリケーションで例えるなら、ベースとなるウィンドウ(コンテナ)の中に、具体的に絵を描く部分と、ツールを切り替えたり色を変えたりする部分があって、ツール画面自体はコンテナ、そこにあるツールを選択するボタンはコンポーネントといった具合です。
こういうようにコンテナとして内包する事により、ツール画面自体を動かす(ようにプログラムする)だけで、ボタンも一緒に移動してくれます。

フレーム(Frame)

まずは、GUIのベース、すなわちウィンドウを発生させてみましょう。

ウィンドウは元を辿るとContainerを継承しています。
つまりウィンドウはコンテナであり、この中にコンポーネントを入れていっていくというのがGUI作りの基本です。

ウィンドウとして扱えるコンテナはjava.awt.Windowがありますが、これだけでは普通のウィンドウとしては足りないので、これを継承しているjava.awt.Frameを基本的に使っていきます。
WindowのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Window.html
FrameのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Frame.html

今回は2つのファイルに分けて作っていきます。
以降独自の方法で要素を追加していきますが、どう組み込むかは人によってまちまちだと思います。
無理に分けようとすると色々呼び出したりする手間があるので、基礎が分かったら独自にまとめた方が良いと思われます。メモ書きに一例を書いておきます。

  • Runtime.java
    • 起動時にどうするかのプログラムです。
    • 新しくオブジェクトとしてフレーム(を継承したもの)をnew演算子で発生させます、するとウィンドウが発生します。
public class Runtime
{
 public static void main(String[] args)
 {
  new Base("Test", 400, 300);//引数はウィンドウの名前と大きさ、変数を作らなくても良い
 }
}

  • Base.java
    • java.awt.Frameを継承したものです。
    • これからこれをいじってGUIを作りたいものに変えていきます。
import java.awt.*;

public class Base extends Frame
{
 public Base(String name, int width, int height)//コンストラクタ、ウィンドウの名前と大きさを引数で貰った
 {
  super(name);//Frameのコンストラクタ、String型の引数を入れると、ウィンドウの名前に反映されます。
  setSize(width,height);//Frame、すなわちウィンドウの初期サイズです。それぞれ横と縦の長さ
  setVisible(true);//Frameが見えるかどうか、初期状態では見えなくなっています。
 }
}
さて、これをjavac Runtime.java Base.javaとコマンドプロンプトに入力してコンパイルし、java Runtimeで起動してみましょう。
すると、Testという名前の真っ白なウィンドウが表示されました。

ところで、×ボタンを押しても消えません。なぜなら、×ボタンを押したときどうするか、
詳しく言えば、×ボタンを押されたというイベントが発生して、そのイベントをキャッチするまでは良いのですが、キャッチされたらどうするかが定義されていないからです。
現状ウィンドウを消す場合はJava自体を終了させなければなりません。コマンドプロンプトでCtrl+Cキーで強制終了させるか、コマンドプロンプト自体を閉じましょう。

ところで、Frameはnew演算子でオブジェクトとして発生するたびにウィンドウが出来ていきます。
Runtime.javaを以下のようにしてみましょう。
public class Runtime
{
 public static void main(String[] args)
 {
  if(args.length > 0)//コマンドライン引数がある
  {
   for(String name : args)
   {
    new Base(name, 400, 300);//コマンドライン引数の内容をそのまま入れる
   }
  }
  else System.out.println("No Args");//コマンドライン引数が無い場合コマンドプロンプトに引数が無い事を表示
 }
}
そして同じようにコンパイルし、java Runtime あ い う え おと入力して起動すると・・・?
あ、い、う、え、おとそれぞれ名前の付いたウィンドウが合計5つ表示されるはずです。

5つあるのはFor文でコマンドライン引数の数だけnew演算子を使ったからです。
試しに3つ、6つ、とにかく好きな数言葉を入れてみましょう。その数だけ、それぞれの名前のウィンドウが表示されるはずです。

イベントリスナー(EventListener)

上記のプログラムで作ったウィンドウは、×ボタンを押しても消えません。
先ほども言った通り、×ボタンを押したらどうするかが定義されてないのが原因です。
逆にこれによって、途中で止めてはいけない作業中に強制停止されない、あるいは作業が終わるまで閉じるのを待つという制御が出来たりと、×ボタンでどうするかが定義できるのは必要な事でもあったりします。

さて、JavaのGUIでイベントを処理するには、コンポーネントにjava.util.EventListenerを継承したイベントリスナーを追加して、対応するメソッドを実装する事で、その操作がされた時の処理を定義することができます。
試しにComponentのページのメソッドの部分を見てください、add〇〇Listenerというメソッドがいくつかあると思います。
また、ContainerやWindow、Frameと見ていくと、その先々で更に発生するであろうイベントが増える分add〇〇Listenerが増えていくことが分かります。

まず、×ボタンを押すというイベントはFrameまで継承されていく過程のどの時点で発生する可能性があるでしょうか。
Windowは×ボタンが存在しないしFrame...と見せかけて、実はWindowなんです。
WindowにはaddWindowListenerというメソッドがあります。ここに引数としてWindowListenerがありますね。クリックしてメソッドを見てみましょう。
windowClosing、これが×ボタンを押した時実行されるメソッドです。「ユーザーがウィンドウのシステム・メニューでウィンドウを閉じようとしたときに呼び出されます。」だそうで...紛らわしいですね。

どれが×ボタンを押した事を検出してくれるか分かった所で、とりあえずリスナーを作ってみましょう。
Listener系はインターフェースなので、一つのクラスに複数入れる事もできます。便利ですね。

  • Listeners.java
import java.awt.event.*;

public class Listeners implements WindowListener
{
 public void windowClosing(WindowEvent e)
 {
  System.exit(0);
 }
 //以下特にそれが起きても何もしないけどインターフェースなので一応実装
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
 }
}
そしてBaseをこう変えてください。
import java.awt.*;

public class Base extends Frame
{
 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width,height);
  addWindowListener(new Listeners());//これを追加
  setVisible(true);
 }
}
そして変更したものをまとめてコンパイルし、起動して出てきたウィンドウの×ボタンを押すと消えるはずです。

ところで、現状この方法ではJavaを直接終了する方法でやっているので、複数ウィンドウが存在する場合は一つ×ボタンを押すだけですべて消えてしまいます。
ウィンドウを直接消したい時はWindow.dispose()を使う事でリソースを解放(RAMの容量を開ける)できます。
これで消したウィンドウはWindowの特定のメソッドで復活可能ですが、逆を言えばどこかで変数に入れて置かなければ完全に抹消されます。

dispose()を使う上で...
リスナーのメソッドにはWindowEventという型の引数がありますね。
この引数にはイベントが起きた時の情報があります。
リファレンスを見ると、情報を引き出せるメソッドがありますね。
また、継承元のEvent系クラスを辿ると、その時点で必ず存在するであろう情報を出せるメソッドが存在します。

って事で、このWindowEventから、×ボタンが押されたウィンドウが(RAM上の)どれかを見つけ出し、それをdispose()で消すようにしてみましょう。
という事でListeners.javaを以下のように変えてみましょう。
import java.awt.event.*;

public class Listeners implements WindowListener
{
 public void windowClosing(WindowEvent e)//ここを変更
 {
  e.getWindow()//ウィンドウを呼び出す、型はWindowに
   .dispose();//ウィンドウを消す
 }
 //以下特にそれが起きても何もしないけどインターフェースなので一応実装
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
 }
}
そしてコンパイルして起動、複数のウィンドウを出して×ボタンを押すと...
ちゃんと押したウィンドウだけ消えるはずです。

ちなみにWindowListenerのリファレンスを見ると、WindowListenerを実装したWindowAdapterというクラスがあります。
これはリスナー・オブジェクトの作成を容易にするため(リファレンスの説明より引用)にある"アダプタ"と呼ばれるクラスです。
既に一通り実装してある(メソッドの中身はカラ)ので、これを使えばListeners.javaの内容はこれで済みます。
一方インターフェースではないのでアダプタを一まとめにすることはできません。
ただし後術の性質の関係上、リスナーはまとめて定義するよりイベントの種類によって個別に定義するのが一般的なので、やり方によりますが気にならない場合が多いと思います。
また、匿名クラスとして使う際にも便利です。メモ書きに一例を書いておきます。
import java.awt.event.*;

public class Listeners extends WindowAdapter
{
 public void windowClosing(WindowEvent e)
 {
  e.getWindow().dispose();
 }
}

リスナーを追加する事に関してですが、これは言い換えると、それ関係のイベントが起きたとき、このクラスを参照してねっていうのをコンポーネントにメモしている感じです。
それを踏まえて、Listeners.javaに、キーボードのEscキーを押すとウィンドウを消すように定義してみましょう。
KeyListenerはコンポーネントの時点で追加できます。Componentのリファレンスを参照してください。
import java.awt.event.*;
import java.awt.*;//今回はComponent型とWindow型を使う上でインポートします

public class Listeners implements WindowListener, KeyListener//KeyListenerを追加
{
 public void windowClosing(WindowEvent e)
 {
  e.getWindow().dispose();
 }
 public void keyPressed(KeyEvent e)
 {
  if(e.getKeyCode() != KeyEvent.VK_ESCAPE) return;//KeyEventから押されたキーの情報を読み込んでEscキーではなかったら何もしない
  Component c = e.getComponent();//継承元のComponentEventのメソッド
  if(c instanceof Window)
   ((Window)c).dispose();//Window型にキャストして消す
 }
 public void keyReleased(KeyEvent e){}//キーが離された時
 public void keyTyped(KeyEvent e){}//文字を入力した時、大げさに言えば文字を入力するキーが押された場合
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
}
そしてそのままコンパイルして起動してEscキーを押しても...何も起きません。
何故ならBase.java、WindowListenerを追加して、Windowに関するイベントが起きた時に処理をどのクラスに任せれば良いかは知っていますが、KeyListenerは追加されてないため、キーが押された時どうすれば良いかは分からないからです。
なのでBase.javaをこのように変えてみましょう。今までaddWindowListenerに直接new演算子を使ってリスナーを作っていましたが、今回は変数に入れてみましょう。
import java.awt.*;

public class Base extends Frame
{
 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width, height);
  Listeners l = new Listeners();
  addWindowListener(l);
  addKeyListener(l);
  setVisible(true);
 }
}
そしてコンパイルすれば、Escキーを押した際にアクティブにしているウィンドウが消えるはずです。

以下、Runtime.javaは戻した状態で話を進めます、戻さなくても一応やれなくはありません。

グラフィックス(Graphics)

Graphicsは全てのコンポーネントに存在する要素で、描画、つまり画面への表示に影響する部分です。
このGraphicsを弄る事で絵を表示したり、キャラクターや障害物をゲーム状況に応じたように表示する事でゲーム画面を作る事ができます。
Graphicsですが、ホワイトボードで例えると...
この位置にプレイヤーがいて、この位置に敵である円がいて...みたいにマグネットが張り付いている感じではなく、
この位置にプレイヤーの画像と同じ色と形で色塗りして、この位置に敵がいるので円を描いて...みたいに直接ペンやスタンプで描いている感じです。
上記のようにマグネット何かを動かしているようにしたい場合は、基本的にGraphicsをリセットして描き直すのを繰り返す感じでやります。
GraphicsのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Graphics.html

Graphicsを弄る方法は、Component.getGraphics()で呼び出して直接命令する方法と、Componentに存在する、paint(Graphics g)、repaint()およびupdate(Graphics g)をオーバーライドして使用する方法があります。

まずGraphicsを呼び出して直接描画する方法です。
Graphicsには、簡単な図形を描画するメソッドが標準で搭載されているので、それを使用して、マウスの位置を中心とした丸を描くプログラムを組んでみましょう。
まずListeners.javaを以下のように変えます。
import java.awt.event.*;
import java.awt.*;

public class Listeners implements WindowListener, KeyListener, MouseMotionListener//MouseMotionListenerを追加
{
 public void windowClosing(WindowEvent e)
 {
  e.getWindow()
   .dispose();
 }
 public void keyPressed(KeyEvent e)
 {
  if(e.getKeyCode() != KeyEvent.VK_ESCAPE) return;
  Component c = e.getComponent();
  if(c instanceof Window)
   ((Window)c).dispose();
 }
 //ここから
 public void mouseMoved(MouseEvent e)//左クリックせずにマウスを動かした時
 {
  Component c = e.getComponent();//イベントの発生したコンポーネント(フレーム)呼び出し
  Graphics g = c.getGraphics();//コンポーネントからグラフィック呼び出し

  g.fillOval//塗りつぶされた楕円を描く
  (
   e.getX() - 15,//左右の位置、イベントが発生した場所(今回の場合はポインターの位置)を取得して半径分引く(座標を左上の頂点とした長方形にすっぽり入るように描画するため)
   e.getY() - 15,//上下の位置
   30,//左右の大きさ、上記の括弧書きした内容の架空の長方形の大きさでもある
   30//上下の大きさ、大きさが違うと楕円、同じだと正円になる
  );
 }
 public void mouseDragged(MouseEvent e)//左クリックしたまま動かした場合
 {
  mouseMoved(e);//クリックしてない時と同じ事をさせるため直接呼び出す
 }
 //ここまで
 public void keyReleased(KeyEvent e){}
 public void keyTyped(KeyEvent e){}
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
}
さて、新しいタイプのイベントリスナーが追加されたのと、ちょっと下準備が必要なものなので、Base.javaを以下のように変更します。
都合上paint(Graphics g)を先に使ってますが、後で詳しく解説します。
import java.awt.*;

public class Base extends Frame//GUI本体
{
 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width, height);
  Listeners l = new Listeners();
  addWindowListener(l);
  addKeyListener(l);
  addMouseMotionListener(l);//マウスポインターの動きに応じた処理があるので追加
  setVisible(true);
 }

 //Componentから存在し継承されるにつれオーバーライドされてるメソッド。最初にする描画でコンポーネント(今回はフレーム)、ただし今回は描く図形の色指定のみ
 public void paint(Graphics g)
 {
  super.paint(g);//それぞれのコンポーネントによって違うするべきことがあるので必ず親のpaintを呼び出すこと
  g.setColor(new Color(0,0,0));//これから描くものの色を指定する、赤、緑、青の色の強さを0~255で指定
 }
}
そしてコンパイルして起動してみてください。
マウスポインターを画面の中で動かすと、その軌跡に応じて黒い線が描かれると思います。

所で、現状ポインターがある位置だけ黒い丸を表示したい時は、画面をリセットしてまた描画するようにしないといけません。
Listeners.javaの途中を以下のように変えてください。
import java.awt.event.*;
import java.awt.*;

public class Listeners implements WindowListener, KeyListener, MouseMotionListener//MouseMotionListenerを追加
{
 public void windowClosing(WindowEvent e)
 {
  e.getWindow()
   .dispose();
 }
 public void keyPressed(KeyEvent e)
 {
  if(e.getKeyCode() != KeyEvent.VK_ESCAPE) return;
  Component c = e.getComponent();
  if(c instanceof Window)
   ((Window)c).dispose();
 }
 //ここから
 public void mouseMoved(MouseEvent e)
 {
  Component c = e.getComponent();
  Graphics g = c.getGraphics();
  c.update(g);//ここを追加、何もオーバーライドしない場合は単に描画がリセットされる
  //ちなみに背景色で全体を塗り広げてしまうっていう手もある
  //参考までにGraphics.fillRect(int x, int y, int width, int height)を使えば塗りつぶした長方形を描けるので、g.setColor(Color c)で背景色を指定してから引数(0, 0, 画面の左右の大きさ, 上下の大きさ)でやれる
  //その際はg.setColor(Color c)で後に描画したいものの色を設定しなおす事を忘れずに

  g.fillOval
  (
   e.getX() - 15,
   e.getY() - 15,
   30,
   30
  );
 }
 public void mouseDragged(MouseEvent e)
 {
  mouseMoved(e);
 }
 public void keyReleased(KeyEvent e){}
 public void keyTyped(KeyEvent e){}
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
}
すると画面がリセット(真っ白になる)された後円が表示されるため、マウスの位置だけ黒い丸が表示されるようになります。
ちなみに、画面外にポインターが出ると最後にポインターがあった場所に丸が残されます。
これはマウスが画面に出たというイベントを拾ってその時の処理をすれば消せます。対応するリスナーはMouseListenerです(今回は割愛)。

さて、先ほど使っていたpaint(g)とupdate(g)、ついでにrepaint()についてやって行きましょう。

paint(g)はコンポーネントが作られて最初に必ず実行されるのと、後はコンポーネントのサイズが変わった時(フレームなら端をドラッグして大きさを変えた時など)などに自動的に実行されます。デフォルトでは特に何もしません。
update(g)は、repaint()をした際、軽量コンポーネントで無ければ呼び出されます。デフォルトでは描画をリセットします。
謎のワードが出ましたが...ともかく、どちらも直接使っても問題ありません。gには何かをしたいGraphics、基本的にはそのコンポーネントのGraphics(Component.getGraphics()などで呼び出す)を入れて使います。
そしてrepaint()、軽量コンポーネントであればpaint(g)を、そうでなければupdate(g)を呼び出します。それも瞬時ではなく出来るだけ早く(文として読まれた後、呼び出した処理が終わるのを待たずに次の文を読んでしまう)呼び出します。
こちらは他と違いGraphicsの引数が要らない(gには)ので他のコードなどから描画の更新を行わせるのに便利です。

軽量コンポーネント(勉強中)

さて、とりあえずFrameは軽量コンポーネントではないので、Listeners.javaのc.update(g)をc.repaint()に変えても同じように機能するはずです。cのupdate(g)が呼び出されるはずですから。
そしてやってみると...黒丸が表示されません。
沢山動かすと一瞬見えるときもありますが、すぐに消えてしまいます。
何故なら、repaint()は先ほども言った通り出来るだけ早く呼び出して、その間はその後の文を実行しているからです。
これがrepaint()の悩ましい部分です。

ならばどうすれば良いか。
というかこれが、ゲームなど描画が常に変化するGraphicsの正しい使い方なのですが、
任意の方法でrepaint()を繰り返し呼び出し、update(g)にその時描画したい事を全て詰め込む事で、画面の描画をリセットして描画しなおすを繰り返す事が出来ます。

これを元に、先ほどまで作った、カーソルを黒丸が追いかけるプログラムを改造していきましょう。
Runtime.java
public class Runtime
{
 public static void main(String[] args)
 {
  Base b = new Base("Test", 400, 300);
  while(b.isDisplayable())//Componentから継承、そのコンポーネントが表示可能かを真偽値に。Window.disposeで消えたら表示不可になりfalseに
  {
   b.repaint();
  }
 }
}
Base.java
import java.awt.*;

public class Base extends Frame
{
 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width, height);
  Listeners l = new Listeners();
  addWindowListener(l);
  addKeyListener(l);
  addMouseMotionListener(l);
  setVisible(true);
 }

 public void paint(Graphics g)
 {
  super.paint(g);
  g.setColor(new Color(0,0,0));
 }
 //これを追加
 public void update(Graphics g)
 {
  super.update(g);//それぞれのコンポーネントによって違うするべきことがあるので必ず親のupdateを呼び出すこと
  g.fillOval(DeltaData.getMousePositionX() - 15, DeltaData.getMousePositionY() - 15, 30, 30);//今までリスナー側でやってた事
 }
}
Listeners.java
import java.awt.event.*;
import java.awt.*;

public class Listeners implements WindowListener, KeyListener, MouseMotionListener
{
 public void windowClosing(WindowEvent e)
 {
  e.getWindow()
   .dispose();
 }
 public void keyPressed(KeyEvent e)
 {
  if(e.getKeyCode() != KeyEvent.VK_ESCAPE) return;
  Component c = e.getComponent();
  if(c instanceof Window)
   ((Window)c).dispose();
 }
 public void mouseMoved(MouseEvent e)//ここを変更
 {
  DeltaData.setMousePosition(e.getX(), e.getY());//ポインターが動くごとにイベントオブジェクトからポインターの座標を取得してDeltaDataに送る
 }
 public void mouseDragged(MouseEvent e)
 {
  mouseMoved(e);
 }
 public void keyReleased(KeyEvent e){}
 public void keyTyped(KeyEvent e){}
 public void windowActivated(WindowEvent e){}
 public void windowClosed(WindowEvent e){}
 public void windowDeactivated(WindowEvent e){}
 public void windowDeiconified(WindowEvent e){}
 public void windowIconified(WindowEvent e){}
 public void windowOpened(WindowEvent e){}
}
DeltaData.java
  • 新しくその時その時の情報を保存するクラスを追加
    • 典型的なカプセル化もやってるので参考になれば...必要ではありません
public class DeltaData
{
 private static int mousePositionX;//マウスポインターのウィンドウ内のx座標
 private static int mousePositionY;//マウスポインターのウィンドウ内のy座標

 public static int getMousePositionX(){return mousePositionX;}
 public static int getMousePositionY(){return mousePositionY;}
 public static void setMousePosition(int x, int y)
 {
  mousePositionX = x;
  mousePositionY = y;
 }
}
これらをまとめてコンパイルして実行しましょう。すると...
ちょっと描画がおかしくなると思います。
具体的には、黒丸が上側ちょっとだけ表示されたり、一瞬だけそれ以外が見えたりといった感じになると思います。
一言で言えばちらつきというもので、これは対策しないと起こりうるよくある現象です。

というのも、Base.javaのpublic void update(Graphics g)の中では、フレームのGraphicsを直接弄ってます。
Javaの仕様上上から文を実行するのはすでに承知の上だと思いますが、Graphicsの変化はリアルタイムで行われています。
一方上記のメソッドでは、Graphicsをリセット→Graphicsに、取得してあるマウスポインターの位置情報を元にした位置に黒丸を描画という2つの処理を交互に繰り返しています。
つまり、画面に表示されているのはメソッドでGraphicsを処理している過程だったのです。黒丸が欠けるのは、リセットして黒丸を描画する途中だったり、その逆だったりしているからです。

前までちらつきが起きなかったのは、マウスが動いたというイベントが起きるごとだったので、一度に沢山ポインターの座標が変わらなければ(素早く動かさなければ)限りは過程を無限に見させられる事は無かったのですが、
今回はrepaint()を出来るだけ早く繰り返しているので、常に過程を見させられている状態だったという訳です。

これを解消するために、下記のダブルバッファリングという技法を使います。

AWTをさらに使いこなす

ダブルバッファリングとイメージ(Image)

これは、次に表示したい描画を別のものに描き、それを表示させたいコンポーネント(ウィンドウなど)のGraphicsに描画させるという方法で、Graphicsに1つの描画処理だけを行わせる事によりちらつきを無くすという方法です。
Graphicsにはcreate()というコピーを作るメソッドがありますが、Graphicsには、何かしらのコンポーネントに繋がりを必ず持っているので、update(Graphics g)でgのコピーを作ってそれを弄っても画面の変化が起きてしまいます。
例えば、Base.javaをこう変えても同じような感じの実行結果になります。
import java.awt.*;

public class Base extends Frame
{
 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width, height);
  Listeners l = new Listeners();
  addWindowListener(l);
  addKeyListener(l);
  addMouseMotionListener(l);
  setVisible(true);
 }

 public void paint(Graphics g)
 {
  super.paint(g);
  g.setColor(new Color(0,0,0));
 }
 //これを変更
 public void update(Graphics g)
 {
  newG = g.create();//Graphics.create()はクローンを作るメソッド
  super.update(newG);
  newG.fillOval(DeltaData.getMousePositionX() - 15, DeltaData.getMousePositionY() - 15, 30, 30);//クローンを弄っただけなのにフレームに描画処理がされる、クローンもこのフレームを参照しているため
 }
}

そこで使うのがイメージ(Image)です。Graphicsとの違いを言えば、Imageは絵そのもの(の内容)で、Graphicsは紙(コンポーネント)に描かれて展示されている(画面に表示している)絵といった所でしょうか。
Component.createImage(int width, int height)の説明にも、ダブル・バッファリングのためにイメージを作成するって感じで書いてあります。
公式が言うのですからこちらも使っていきましょう。
ImageのURL:https://docs.oracle.com/javase/jp/8/docs/api/java/awt/Image.html

では、実際に使ってみましょう。Base.javaを以下のように変更してください。
import java.awt.*;

public class Base extends Frame
{
 private Image tempImage;//ダブルバッファリング用の一時的イメージ、フィールドにする事でメモリ消費などを節約
 private Graphics temp;//イメージをGraphics型にしたもの、イメージに対しGraphicsの描画メソッドを行うため

 public Base(String name, int width, int height)
 {
  super(name);
  setSize(width, height);
  Listeners l = new Listeners();
  addWindowListener(l);
  addKeyListener(l);
  addMouseMotionListener(l);
  setVisible(true);
  //tempImage = createImage(width, height);
  //temp = tempImage.getGraphics();
  //Component.createImage
 }

 public void paint(Graphics g)//画面のサイズが変更された場合にも読み込まれるのでその都度Imageの大きさが変わる、コンストラクタなどで一度きりだと画面を大きくした際元の画面の大きさ分しか描画されない
 {
  Dimension size = getSize();//フレームのサイズをjava.awt.Demention型で取得
  tempImage = createImage((int)size.getWidth(), (int)size.getHeight());//Double値で取得してしまうのでキャスト
  temp = tempImage.getGraphics();//一時的イメージをGraphicsの描画メソッドで描画するためImageをGraphicsに変化させる(取得するってよりそんな感じ)
  super.paint(g);
  g.setColor(new Color(0,0,0));
 }
 public void update(Graphics g)
 {
  super.update(temp);//コンポーネントから継承されるたびに追加された描画更新の内容を一時的イメージに適応
  temp.fillOval(DeltaData.getMousePositionX() - 15, DeltaData.getMousePositionY() - 15, 30, 30);//一時的イメージに描画
  g.drawImage(tempImage, 0, 0, this);//一時的イメージをフレームのGraphicsに描画させることで画面を更新
 }
}
そしてコンパイルして実行すると...やっと思い通りにポインターの位置に黒丸が常に表示されるようになったと思います。

所で、repaint()を無限に呼び出すだけでもメモリを多く使用します(この項の前までのプログラムでも、こちらの環境では100MBぐらい使う)。
そしてダブルバッファリングを行う事で、こちらはもっと多く使います(このプログラムで、こちらの環境では400MB以上)。
Runtime.javaの繰り返しにの部分に、java.lang.Thread.sleep(long millis)*2を入れてみると良いでしょう。そもそも描画を繰り返す方法は他にもありますが...
数字を大きくするとメモリの消費が抑えられますが、画面の更新頻度が減るのでカク付きます。

さて、Imageには他にも別の使い方が存在します。
(編集中)

コンポーネントをコンテナに追加、そしてレイアウト(Layout)

最終更新:2021年06月02日 19:57

*1 文字ごとに主人公の左上、その右隣、そのさらに右隣といった感じでフォントの感覚で文字と絵、というかパズルのピースを当てはめていた感じです。チートバグ動画なんかだと、文字のデータとそれに当てはまる絵をずらしてしまい文字の規則正しく並んだ物体が動いてるなんて場面をよく見ると思います。

*2 InterruptedExceptionを投げるので注意、catchの内容は空でOK