第25週目演習

下準備

テストドライバの導入

第25回目 (秋学期12回目) 分のテストドライバを導入する。以下の手順で行う。

  1. ダウンロード のページを開く (ここをクリック)
  2. プロジェクト「java2007」にある「test」の左側の「+」をクリック
  3. ツリーが展開されるので「install-libraries.xml」を右クリック
  4. 「実行(R)」にマウスカーソルを合わせる
  5. 「1 Ant ビルド」をクリック
  6. 「コンソール」タブに"BUILD SUCCESSFUL"と表示されれば成功
  7. eclipseの画面でプロジェクト「java2007」を右クリック
  8. メニューが表示されるので、「更新」をクリック
  9. "week25.zip" をデスクトップなどにダウンロード
  10. eclipseの画面でプロジェクト「java2007」を右クリック
  11. メニューから「インポート(I)」を選択
  12. 「インポート」ウィンドウが表示されるので、「Zip ファイル」を選択
  13. 「次へ(N)」をクリック
  14. 宛先フォルダー(L): が「java2007」になっていることを確認
  15. From zip file: の右側にある 「参照(R)...」をクリック
  16. ファイルダイアログが表示されるので、ダイアログ内に表示されたダウンロードしたファイルをダブルクリック
  17. 前の画面に戻るので、From zip file: のエリアに正しいパスが入力されていることを確認
  18. フォルダ「/」の左にチェックがついていることを確認 (ついていなければチェックボックスをクリック)
  19. 「警告を出さずに既存リソースを上書き」にチェックがついていることを確認 (上書きしたくないファイルがある場合はチェックを外す)
  20. 「終了 (F)」をクリック

第25週目テストドライバの導入に成功すると、java2007 プロジェクトの test フォルダに j2.lesson12.xml というファイルが作成される。

GUI のテスト技法はあまりよいものが確立されていない。今回のテストドライバは試験的に作成したものなので、自動テストに頼らずに各自でテストを行うこと

パッケージの作成

過去の演習を参考にして、「j2.lesson12」というパッケージを作成する。

キーボードでボールを移動させるプログラム

画面上にボールを表示させ、そのボールをキーボードによって操作できるようなプログラムを作成する。

このプログラムは、実行すると次のようなウィンドウを表示する。


ここで、キーボードの右 (→) キーを押すと、表示されている青いボールが右へ一つ移動する。


さらに、キーボードの下 (↓) キーを押すと、表示されている青いボールが下へ一つ移動する。


左キー、上キーを操作した際も、右や下キーと同様に指定した方向に移動する。

画面には、4x4 のマスが表示されていて、ボールはマスの外に出ることができない (枡の外へ出るようにキーを入力しても無視する)。


画面設計

まずは、GUI 部分を表示するクラス MovingBall を作成する。このクラスは下記のようなウィンドウを表示するだけのプログラムである。


レイアウトの決定

コンポーネント内のレイアウトを決定する。ウィンドウのレイアウトと違い、「どの座標に何を表示させるか」ということを細かく決定しなければならない。


紙に上記のような図を書いて、描画の方針を決めればよい。


上図の px は pixel (ピクセル) の略で、画面上の一つ分の点を表す画像を構成する最小の単位であり、多くのデジタル画像はこの点に色をつけて規則正しく敷き詰めることによって一枚の絵を表現している。これまでに「ウィンドウのサイズは 300x200 とする」などといった表現をしてきたが、このサイズの単位は全てピクセルである (つまりそのようなウィンドウは、300x200 個の点で表現される)。

上記のような設計図に従い、擬似コードを書く。

擬似コードの作成

設計図を見ながら、このレイアウトを実現するために必要な擬似コードを書く。


コンポーネントを描画する
    罫線を引く (灰色)
    ボールを描く (青)

「罫線を引く」という部分をもう少し分解してみると、次のように書ける。


以下、灰色で描画
for x = 0 から 4 まで
    (10 + 40 * x, 10) -> (10 + 40 * x, 170) に線を引く
for y = 0 から 4 まで
    (10, 10 + 40 * y) -> (170, 10 + 40 * y) に線を引く

上記をまとめて、次のようにも書ける (好きなほうでよい)。

for i = 0 から 4 まで
    (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
    (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く


また、「ボールを描く」という部分も、次のように書ける。

x = ボールの現在の位置 (0 <= x <= 3)
y = ボールの現在の位置 (0 <= y <= 3)
以下、青色で描画
左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす

たいした長さではないので、一つの擬似コードにまとめる。

コンポーネントを描画する
    以下、灰色で描画
    for x = 0 から 4 まで
        (10 + 40 * x, 10) -> (10 + 40 * x, 170) に線を引く
    for y = 0 から 4 まで
        (10, 10 + 40 * y) -> (170, 10 + 40 * y) に線を引く
    x = ボールの現在の位置 (0 <= x <= 3)
    y = ボールの現在の位置 (0 <= y <= 3)
    以下、青色で描画
    左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす

また、全体のサイズ指定もあるので、そのサイズを返す擬似コードも作成しておく。


コンポーネントのサイズを取得する
    return 180x180

クラスの設計

親クラスの抽出

今回のコンポーネントは Swing のコンポーネントとして使用したいため、javax.swing.JComponent を継承する必要がある。

フィールドの抽出

擬似コードで、次のような表現があった。

x = ボールの現在の位置 (0 <= x <= 3)
y = ボールの現在の位置 (0 <= y <= 3)

これは、コンポーネントの描画で使用する情報であるので、インスタンスフィールドとして作成する。

コンストラクタの抽出

特に必要なコンストラクタはないが、フィールドの初期化のために引数なしのコンストラクタを用意する。

メソッドの抽出

javax.swing.JComponent を継承するクラスで独自の描画を行う場合、次の2つのメソッドをオーバーライドする必要がある。

骨格の作成

クラスの作成

以下の手順で、パッケージ「j2.lesson12」に「MovingBall」クラスを作成する。

  1. 先ほど作成したパッケージ 「j2.lesson12」の上で右クリック
  2. マウスカーソルを「新規」に合わせる
  3. 「クラス」をクリック
  4. クラス名は MovingBall とする

擬似コードの貼り付け (骨格のみ)

作成したクラスに、各擬似コードの名前を貼り付ける。

このプログラムでは Swing の API をいくつか使用しているため、次の import 文を書いておく。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall {
    // コンストラクタ
    // コンポーネントを描画する
    // コンポーネントのサイズを取得する
}

親クラスの指定

親クラスに JComponent を指定する。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall extends JComponent {
    // コンストラクタ
    // コンポーネントを描画する
    // コンポーネントのサイズを取得する
}

インスタンスフィールドの作成

抽出したインスタンスフィールドをクラス内に用意する。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall extends JComponent {
    
    private int x;
    private int y;
    
    // コンストラクタ
    // コンポーネントを描画する
    // コンポーネントのサイズを取得する
}

コンストラクタの作成

抽出したコンストラクタをクラス内に用意する。コンストラクタではインスタンス (フィールド) を初期化しておく。

ボールの初期位置は左上なので、x, y 共に 0 に設定する。

// コンストラクタ
public MovingBall() {
    this.x = 0;
    this.y = 0;
}

paintComponent メソッドの作成 (骨格のみ)

擬似コード「コンポーネントを描画する」にあわせて、クラス「MovingBall」内に protected void paintComponent(Graphics g) から始まるメソッドを作成する。

骨格のみなので、前回同様に例外 UnsupportedOperationException をスローしておく。

// コンポーネントを描画する
protected void paintComponent(Graphics g) {
    throw new UnsupportedOperationException("paintComponent");
}

getPreferredSize メソッドの作成 (骨格のみ)

擬似コード「コンポーネントのサイズを取得する」にあわせて、クラス「MovingBall」内に public Dimension getPreferredSize() から始まるメソッドを作成する。

骨格のみなので、前回同様に例外 UnsupportedOperationException をスローしておく。

// コンポーネントのサイズを取得する
public Dimension getPreferredSize() {
    throw new UnsupportedOperationException("getPreferredSize");
}

全体の骨格

ここまでのプログラムの骨格は以下のようになる。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall extends JComponent {
    
    private int x;
    private int y;
    
    // コンストラクタ
    public MovingBall() {
        this.x = 0;
        this.y = 0;
    }
    
    // コンポーネントを描画する
    protected void paintComponent(Graphics g) {
        throw new UnsupportedOperationException("paintComponent");
    }
    
    // コンポーネントのサイズを取得する
    public Dimension getPreferredSize() {
        throw new UnsupportedOperationException("getPreferredSize");
    }
}

骨格テスト

ここまでの作業をCtrl+Sを押して保存し、コンパイルを行う (保存時に自動で行われる)。ここでエラーが発生していたら文法エラーなので見直す。

「MovingBallに対する骨格テスト」を実行する。

骨格テストを行った際に緑のバーが表示されれば、外側から見たプログラムの骨格は正しくなっている。

赤いバーが表示された場合、メッセージを元にプログラムを見直すこと。修正を行い、Ctrl+Sで保存した後に「Run」ボタンをクリックする。

メッセージ 詳細
(クラス名), existence j2.lesson12 に対象のクラスが存在していない。パッケージやクラス名を確認
(メソッド名), existence 指定されたメソッドが存在しない
(メソッド名), public メソッドを作る際に public が抜けている
(メソッド名), protected メソッドを作る際に protected が抜けている
(メソッド名), static メソッドを作る際に static が抜けている
(メソッド名), not static メソッドを作る際に static が余計についている
(メソッド名), type <T> メソッドを作る際に戻り値の型を間違えている (正しくは <T>)

プログラムへの変換 (paintComponent)

先ほど作成した MovingBall クラスの paintComponent メソッドの中身を記述する。

まずは擬似コードをコメントとして貼り付ける。

// コンポーネントを描画する
protected void paintComponent(Graphics g) {
    // 以下、灰色で描画
    // for i = 0 から 4 まで
        // (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
        // (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く
    // 以下、青色で描画
    // 左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす
    throw new UnsupportedOperationException("paintComponent");
}

描画をクリアする

擬似コードには出現していないが、コンポーネント上のごみを消すために、以下の一行をメソッドの先頭に加える。

g.clearRect(0, 0, getWidth(), getHeight());

罫線を引く

擬似コードの最初の部分では、罫線を描画している。

以下、灰色で描画
for i = 0 から 4 まで
    (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
    (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く

灰色を表す色は、Color.GRAY である。それを setColor の引数に渡すことにより、以降の描画色を灰色に設定することができる。

また、擬似コードに従って drawLine メソッドを使用することにより、罫線を引くことができる。

// 以下、灰色で描画
g.setColor(Color.GRAY);
// for i = 0 から 4 まで
for (int i = 0; i <= 4; i++) {
    // (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
    g.drawLine(10 + 40 * i, 10, 10 + 40 * i, 170);
    // (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く
    g.drawLine(10, 10 + 40 * i, 170, 10 + 40 * i);
}

ボールを表示する

擬似コードの次の部分では、ボールを描画している。

以下、青色で描画
左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす

同様に、以下のようなプログラムが書ける。

// 以下、青色で描画
g.setColor(Color.BLUE);
// 左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす
g.fillOval(15 + this.x * 40, 15 + this.y * 40, 30, 30);

メソッドが完成したので、骨格作成時に書いた例外のスローは消しておく。

プログラムへの変換 (getPreferredSize)

次に、MovingBall クラスの getPreferredSize メソッドの中身を記述する。

まずは擬似コードをコメントとして貼り付ける。

// コンポーネントのサイズを取得する
public Dimension getPreferredSize() {
    // return 180x180
    throw new UnsupportedOperationException("getPreferredSize");
}

java.awt.Dimension は、幅と高さを表すためのクラスである。これを返せばよいので、下記のようになる。

// コンポーネントのサイズを取得する
public Dimension getPreferredSize() {
    // return 180x180
    return new Dimension(180, 180);
}

メソッドが完成したので、骨格作成時に書いた例外のスローは消しておく。

単純な main メソッドの用意

テストの意味を兼ねて、単純な main メソッドを用意する。

// コンポーネントを表示してみる
public static void main(String[] args) {
    JFrame frame = new JFrame();
    frame.setTitle("moving ball");
    frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
    // スクロール可能にして貼り付ける
    frame.getContentPane().add(new JScrollPane(new MovingBall()));
    // setPreferredSize() が定義されていれば、pack メソッドで自動的にサイズを調整
    frame.pack();
    frame.setVisible(true);
}

プログラムの実行

プログラムを実行すると、下記のようなウィンドウが表示される。


キーボードのイベントを受信していないので、まだボールを操作することはできない。

プログラム全体

MovingBallクラス全体を掲載しておく。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall extends JComponent {
    
    private int x;
    private int y;
    
    // コンストラクタ
    public MovingBall() {
        this.x = 0;
        this.y = 0;
    }
    
    // コンポーネントを描画する
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // 以下、灰色で描画
        g.setColor(Color.GRAY);
        // for i = 0 から 4 まで
        for (int i = 0; i <= 4; i++) {
            // (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
            g.drawLine(10 + 40 * i, 10, 10 + 40 * i, 170);
            // (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く
            g.drawLine(10, 10 + 40 * i, 170, 10 + 40 * i);
        }
        
        // 以下、青色で描画
        g.setColor(Color.BLUE);
        // 左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす
        g.fillOval(15 + this.x * 40, 15 + this.y * 40, 30, 30);
    }
    
    // コンポーネントのサイズを取得する
    public Dimension getPreferredSize() {
        // return 180x180
        return new Dimension(180, 180);
    }
    
    // コンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setTitle("moving ball");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        // スクロール可能にして貼り付ける
        frame.getContentPane().add(new JScrollPane(new MovingBall()));
        // setPreferredSize() が定義されていれば、pack メソッドで自動的にサイズを調整
        frame.pack();
        frame.setVisible(true);
    }
}

イベントリスナの設計

コンポーネント設計の部分が終わったので、次はこのコンポーネントにキーボードイベントの受信機能を追加する。

このプログラムのキーボードイベントリスナを表すクラスとして BallController を作成する。

イベントリスナの確認

このプログラムでは、「上下左右のキーを押した際に、指定した方向にボールを移動させる」といった処理を行う。

前回の演習同様に、イベントハンドラ内で実際の処理を行うのではなく、「イベントが発生したらコンポーネントにボールを移動してもらう」という考え方でプログラムを書く。


このように考えると、イベントハンドラは次のような擬似コードになる。

キーが押された際に呼び出される
    if 入力されたキーが 上キー
        MovingBall にボールを (0, -1) 方向に移動してもらう
    else if 入力されたキーが 下キー
        MovingBall にボールを (0, +1) 方向に移動してもらう
    else if 入力されたキーが 左キー
        MovingBall にボールを (-1, 0) 方向に移動してもらう
    else if 入力されたキーが 右キー
        MovingBall にボールを (+1, 0) 方向に移動してもらう

そして、MovingBall には、次の擬似コードで表されるメソッドが必要になる。

ボールを移動する (dx, dy)
    x = x + dx
    y = y + dy
    ボールが範囲外
        ボールを範囲内へ戻す
    ボールを再描画してもらう

MovingBall の変更

MovingBall クラスには、上記の「ボールを移動する」処理を追加する必要がある。

「ボールを移動する」メソッドの追加

擬似コード「ボールを移動する」が表すメソッドを MovingBall クラスに追加する。

ボールを移動する (dx, dy)
    x = x + dx
    y = y + dy
    if ボールが範囲外へ行ってしまった
        ボールを範囲内へ戻す
    ボールを再描画する

「ボールを移動する」メソッドとして、protected void shiftBall(int dx, int dy) を MovingBall クラスに追加し、上記の擬似コードを貼り付ける。

// ボールを移動する
protected void shiftBall(int dx, int dy) {
    // x = x + dx
    // y = y + dy
    // if ボールが範囲外へ行ってしまった
        // ボールを範囲内へ戻す
    // ボールを再描画する
}

これを元に、プログラムを書く。

// ボールを移動する
protected void shiftBall(int dx, int dy) {
    // x = x + dx
    this.x += dx;
    // y = y + dy
    this.y += dy;
    
    // if ボールが範囲外へ行ってしまった
        // ボールを範囲内へ戻す
    if (this.x < 0) {
        this.x = 0;
    }
    else if (this.x > 3) {
        this.x = 3;
    }
    if (this.y < 0) {
        this.y = 0;
    }
    else if (this.y > 3) {
        this.y = 3;
    }
    
    // ボールを再描画する
    repaint();
}

「x = x + dx」「y = y + dy」の x, y は、それぞれインスタンスフィールドで定義しているものを使用する。それぞれ範囲は 0..3 なので、その範囲外へボールが行ってしまった場合には、0 または 3 に戻すことによって範囲内に収めることができる。

また、ボールを再描画するには、paintComponent(Graphics) メソッドを呼び出すのではなく、repaint() メソッドによって自動的に再描画してもらうのが普通である。

BallController クラスの作成

次に、下記の動作をするイベントリスナを作成する。

キーが押された際に呼び出される
    if 入力されたキーが 上キー
        MovingBall にボールを (0, -1) 方向に移動してもらう
    else if 入力されたキーが 下キー
        MovingBall にボールを (0, +1) 方向に移動してもらう
    else if 入力されたキーが 左キー
        MovingBall にボールを (-1, 0) 方向に移動してもらう
    else if 入力されたキーが 右キー
        MovingBall にボールを (+1, 0) 方向に移動してもらう

親クラス/インターフェースの抽出

キーボードの情報を受信するリスナは java.awt.event.KeyListener であり、このインターフェースは次のメソッドを持っている。

今回は、「キーが押された」という情報のみを扱いたいため、keyPressed メソッドのみを使用することになる。そこで、リスナではなくこのリスナを実装した空のクラス java.awt.event.KeyAdapter というアダプタクラスを使用する。

フィールドの抽出

イベントハンドラを記述するクラスに必要なインスタンスフィールドを抽出する。

キーが押された際に呼び出される
    if 入力されたキーが 上キー
        MovingBall にボールを (0, -1) 方向に移動してもらう
    else if 入力されたキーが 下キー
        MovingBall にボールを (0, +1) 方向に移動してもらう
    else if 入力されたキーが 左キー
        MovingBall にボールを (-1, 0) 方向に移動してもらう
    else if 入力されたキーが 右キー
        MovingBall にボールを (+1, 0) 方向に移動してもらう

このアクションで、使用するデータは MovingBall である。そこで、これらをインスタンスフィールドとして作成する。

コンストラクタの抽出

KeyAdapter クラスは特別なコンストラクタを持たないので、コンストラクタはフィールドを初期化するためだけに作ればよい。

メソッドの抽出

java.awt.event.KeyListener を実装するクラス (KeyAdapter を親に持っているので間接的に実装している) で「キーボードが押された際の処理」を書くイベントハンドラを記述するには、次のメソッドを用意すればよい。

java.awt.event.KeyListener を直接実装している場合はこのほかの2つのハンドラも用意しなければならないが、今回は java.awt.event.KeyAdapter を継承しているため、必要な公開メソッドは上記のものだけである。

骨格の作成

クラスの作成

以下の手順で、パッケージ「j2.lesson12」に「BallController」クラスを作成する。

  1. 先ほど作成したパッケージ 「j2.lesson12」の上で右クリック
  2. マウスカーソルを「新規」に合わせる
  3. 「クラス」をクリック
  4. クラス名は BallController とする。

また、このクラスでは KeyAdapter クラスと KeyEvent クラスを使用するため、次の import 文を書いておく。

package j2.lesson12;

import java.awt.event.*;

public class BallController {
}

親クラスの設定

キーイベントリスナを作成するため、java.awt.event.KeyAdapter を継承する。

package j2.lesson12;

import java.awt.event.*;

public class BallController extends KeyAdapter {
}

インスタンスフィールドの作成

「フィールドの抽出」の部分で決めた内容に従って、インスタンスフィールドをクラス内に作成する。

package j2.lesson12;

import java.awt.event.*;

public class BallController extends KeyAdapter {
    
    private final MovingBall ball;
}

コンストラクタの作成

「コンストラクタの抽出」の部分で決めた内容に従って、コンストラクタをクラス内に作成する。

この際に、インスタンスフィールドに引数の値を代入しておく。

// コンストラクタ
public BallController(MovingBall ball) {
    this.ball = ball;
}

メソッドの作成

「メソッドの抽出」の部分で決めた内容に従って、メソッドをクラス内に作成する。

// キーが押された際に呼び出される
public void keyPressed(KeyEvent e) {
}

全体の骨格

ここまでのプログラムの骨格は以下のようになる。

package j2.lesson12;

import java.awt.event.*;

public class BallController extends KeyAdapter {
    
    private final MovingBall ball;

    // コンストラクタ
    public BallController(MovingBall ball) {
        this.ball = ball;
    }
    
    // キーが押された際に呼び出される
    public void keyPressed(KeyEvent e) {
    }
}

骨格テスト

ここまでの作業をCtrl+Sを押して保存し、コンパイルを行う (保存時に自動で行われる)。ここでエラーが発生していたら文法エラーなので見直す。

「BallControllerに対する骨格テスト」を実行する。

骨格テストを行った際に緑のバーが表示されれば、外側から見たプログラムの骨格は正しくなっている。

赤いバーが表示された場合、メッセージを元にプログラムを見直すこと。修正を行い、Ctrl+Sで保存した後に「Run」ボタンをクリックする。

メッセージ 詳細
(クラス名), existence j2.lesson12 に対象のクラスが存在していない。パッケージやクラス名を確認
(メソッド名), existence 指定されたメソッドが存在しない
(メソッド名), public メソッドを作る際に public が抜けている
(メソッド名), not static メソッドを作る際に static を余計につけている
(メソッド名), type <T> メソッドを作る際に戻り値の型を間違えている (正しくは <T>)

プログラムへの変換

まずは擬似コードをメソッド内に貼り付ける。

// キーが押された際に呼び出される
public void keyPressed(KeyEvent e) {
    // if 入力されたキーが「上」
        // ボールを上 (-y) 方向へ 1 マス移動
    // else if 入力されたキーが「下」
        // ボールを下 (+y) 方向へ 1 マス移動
    // else if 入力されたキーが「左」
        // ボールを左 (-x) 方向へ 1 マス移動
    // else if 入力されたキーが「右」
        // ボールを右 (+x) 方向へ 1 マス移動
}

入力されたキーを判定するには、KeyEvent オブジェクトの getKeyCode() メソッドで押されたキーの情報を取得し、その値を調べればよい。

上下左右のキーを表す定数は、KeyEvent のクラスフィールドに定数として次のようなものが用意されている。

この値を使用し、次のようなプログラムを書けばよい。

// キーが押された際に呼び出される
public void keyPressed(KeyEvent e) {
    // if 入力されたキーが「上」
    if (e.getKeyCode() == KeyEvent.VK_UP) {
        // ボールを上 (-y) 方向へ 1 マス移動
        this.ball.shiftBall(0, -1);
    }
    // else if 入力されたキーが「下」
    else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
        // ボールを下 (+y) 方向へ 1 マス移動
        this.ball.shiftBall(0, +1);
    }
    // else if 入力されたキーが「左」
    else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
        // ボールを左 (-x) 方向へ 1 マス移動
        this.ball.shiftBall(-1, 0);
    }
    // else if 入力されたキーが「右」
    else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
        // ボールを右 (+x) 方向へ 1 マス移動
        this.ball.shiftBall(+1, 0);
    }
}

MovingBall クラスの変更

イベントリスナを作成したら、コンポーネントにこのリスナを登録する必要がある。

インスタンスを生成する際、つまり MovingBall のコンストラクタに、自分自身に対してこのリスナを登録するコードを追加する。

// コンストラクタ
public MovingBall() {
    // インスタンスフィールドの初期化
    this.x = 0;
    this.y = 0;
    
    // キーボードの設定を行う
    this.setFocusable(true);
    this.addKeyListener(new BallController(this));
}

ここで、setFocusable(true) を呼び出さないとキーボードを入力を無視してしまうので注意すること。

プログラムの実行

プログラムを実行する。今回作成したアクションは MovingBall クラスに組み込んだため、MovingBall クラスを起動する

適当に上下左右キーを押して動作を確認する。

機能テスト

ここまでの作業をCtrl+Sを押して保存し、コンパイルを行う (保存時に自動で行われる)。ここでエラーが発生していたら文法エラーなので見直す。

「BallControllerに対する単体テスト」を実行する。

すると、MovingBall クラスが起動されて、ウィンドウが表示される。しばらく待つとウィンドウが自動で操作され、いくつかのテストケースが実行される (自動で操作されている間はキーボードやマウスを操作しないこと)。

メッセージ 詳細
<機能テストの項目名> <機能テストの項目名> で、FAIL ボタンを押した

機能テストの項目

テストの概要 意味
(1) 起動直後のウィンドウ コンポーネントを貼り付けたウィンドウを表示した直後
(2) 1 から 上、左 (1) の状態からキーボードの 上、左 を順に押して離した直後の状態
(3) 2 から 右、右、右、右 (2) の状態からキーボードの 右、右、右、右 を順に押して離した直後の状態
(4) 3 から 右、上 (3) の状態からキーボードの 右、上 を順に押して離した直後の状態
(5) 4 から 左、下、左、下、左、下 (4) の状態からキーボードの 左、下、左、下、左、下 を順に押して離した直後の状態
(6) 5 から 左、下 (5) の状態からキーボードの 左、下 を順に押して離した直後の状態
(7) 6 から 右、右、右、右 (6) の状態からキーボードの 右、右、右、右 を順に押して離した直後の状態
(8) 7 から 右、下 (7) の状態からキーボードの 右、下 を順に押して離した直後の状態

プログラム全体

BallController 全体を掲載しておく。

package j2.lesson12;

import java.awt.event.*;

public class BallController extends KeyAdapter {
    
    private final MovingBall ball;

    // コンストラクタ
    public BallController(MovingBall ball) {
        this.ball = ball;
    }
    
    // キーが押された際に呼び出される
    public void keyPressed(KeyEvent e) {
        // if 入力されたキーが「上」
        if (e.getKeyCode() == KeyEvent.VK_UP) {
            // ボールを上 (-y) 方向へ 1 マス移動
            this.ball.shiftBall(0, -1);
        }
        // else if 入力されたキーが「下」
        else if (e.getKeyCode() == KeyEvent.VK_DOWN) {
            // ボールを下 (+y) 方向へ 1 マス移動
            this.ball.shiftBall(0, +1);
        }
        // else if 入力されたキーが「左」
        else if (e.getKeyCode() == KeyEvent.VK_LEFT) {
            // ボールを左 (-x) 方向へ 1 マス移動
            this.ball.shiftBall(-1, 0);
        }
        // else if 入力されたキーが「右」
        else if (e.getKeyCode() == KeyEvent.VK_RIGHT) {
            // ボールを右 (+x) 方向へ 1 マス移動
            this.ball.shiftBall(+1, 0);
        }
    }
}

また、修正を行った後の MovingBall クラスも掲載しておく。

package j2.lesson12;

import java.awt.*;
import javax.swing.*;

public class MovingBall extends JComponent {
    
    private int x;
    private int y;
    
    // コンストラクタ
    public MovingBall() {
        // インスタンスフィールドの初期化
        this.x = 0;
        this.y = 0;
        
        // キーボードの設定を行う
        this.setFocusable(true);
        this.addKeyListener(new BallController(this));
    }
    
    // ボールを移動する
    protected void shiftBall(int dx, int dy) {
        // x = x + dx
        this.x += dx;
        // y = y + dy
        this.y += dy;
        
        // if ボールが範囲外へ行ってしまった
            // ボールを範囲内へ戻す
        if (this.x < 0) {
            this.x = 0;
        }
        else if (this.x > 3) {
            this.x = 3;
        }
        if (this.y < 0) {
            this.y = 0;
        }
        else if (this.y > 3) {
            this.y = 3;
        }
        
        // ボールを再描画する
        repaint();
    }

    // コンポーネントを描画する
    protected void paintComponent(Graphics g) {
        g.clearRect(0, 0, getWidth(), getHeight());
        
        // 以下、灰色で描画
        g.setColor(Color.GRAY);
        // for i = 0 から 4 まで
        for (int i = 0; i <= 4; i++) {
            // (10 + 40 * i, 10) -> (10 + 40 * i, 170) に線を引く
            g.drawLine(10 + 40 * i, 10, 10 + 40 * i, 170);
            // (10, 10 + 40 * i) -> (170, 10 + 40 * i) に線を引く
            g.drawLine(10, 10 + 40 * i, 170, 10 + 40 * i);
        }

        // 以下、青色で描画
        g.setColor(Color.BLUE);
        // 左上 (15 + x * 40, 15 + y * 40) から 幅、高さ30 の長方形に内接する円を塗りつぶす
        g.fillOval(15 + this.x * 40, 15 + this.y * 40, 30, 30);
    }
    
    // コンポーネントのサイズを取得する
    public Dimension getPreferredSize() {
        // return 180x180
        return new Dimension(180, 180);
    }
    
    // コンポーネントを表示してみる
    public static void main(String[] args) {
        JFrame frame = new JFrame();
        frame.setTitle("moving ball");
        frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        // スクロール可能にして貼り付ける
        frame.getContentPane().add(new JScrollPane(new MovingBall()));
        // setPreferredSize() が定義されていれば、pack メソッドで自動的にサイズを調整
        frame.pack();
        frame.setVisible(true);
    }
}