第24週目演習

下準備

テストドライバの導入

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

  1. ダウンロード のページを開く (ここをクリック)
  2. プロジェクト「java2007」にある「test」の左側の「+」をクリック
  3. ツリーが展開されるので「install-libraries.xml」を右クリック
  4. 「実行(R)」にマウスカーソルを合わせる
  5. 「1 Ant ビルド」をクリック
  6. 「コンソール」タブに"BUILD SUCCESSFUL"と表示されれば成功
  7. eclipseの画面でプロジェクト「java2007」を右クリック
  8. メニューが表示されるので、「更新」をクリック
  9. "week24.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)」をクリック

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

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

パッケージの作成

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

コンピュータ内のテキストを表示するプログラム

今回の演習では、コンピュータ内に保存されているテキストファイルを表示するプログラムを作成する。

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


メニューバーにある「ファイル」を選択すると、「開く」というメニューが表示される。


さらに、「開く」というメニューを選択すると、ファイル選択画面が表示される。


ファイルを選択すると、そのファイル内のテキストデータをテキストエリアに表示する。


画面設計

まずは、GUI 部分を表示するクラス TextViewer を作成する。このクラスはいくつかのコンポーネントを持つ、下記のようなウィンドウを表示するだけのクラスである。


レイアウトの決定

レイアウトを決定する。非常に単純なレイアウトなので、それほど難しくはない。


紙に上記のようなレイアウトを書いて、コンポーネントを配置する方法を決めればよい。また、ここで書いた紙は後ほどアクションを書く部分でも使用するため、捨てずにとっておくとよい。


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

擬似コードの作成

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


これまでは GUI のプログラムを全て main メソッド内に書いてきたが、そのように開発すると後ほどプログラムを変更しにくくなる。そこで、今回は一度「テキストビューアのインスタンス」を作ってからそれを操作するようにプログラムを書くことにする。

まず、プログラム全体としては次のようになる。

プログラム全体
    viewer = テキストビューア
    viewer のウィンドウを初期化する

「ウィンドウを初期化する」というのは、画面を表示する部分なので、さらに詳しい擬似コードに分解する。

ウィンドウを初期化する
    window = タイトルが "text viewer" の新しいウィンドウ
    menubar = メニューバーを作成する
    window に menubar を登録
    textarea = テキストエリアを作成する
    textarea をスクロール可能にする
    window に textarea を登録
    window の初期サイズを 400x400 に設定
    window を表示

擬似コード「ウィンドウを初期化する」内の「メニューバーを作成する」を、さらに詳しい擬似コードに分解する。

メニューバーを作成する
    bar = 新しいメニューバー
    file = "ファイル" メニュー
    open = "ファイル" メニュー内の "開く" メニュー
    barにメニューを追加
    return bar

骨格の作成

クラスの作成

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

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

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

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

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

package j2.lesson11;

import javax.swing.*;

public class TextViewer {
    // ウィンドウを初期化する
    // メニューバーを作成する
    // プログラム全体
}

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

擬似コード「ウィンドウを初期化する」にあわせて、クラス「TextViewer」内に protected void init() から始まるメソッドを作成する。

後の演習で使用するため、protected として宣言しておくことにする。

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

擬似コード「メニューバーを作成する」にあわせて、クラス「TextViewer」内に protected JMenuBar createMenuBar() から始まるメソッドを作成する。

Swing のメニューバーには、通常 javax.swing.JMenuBar が用いられる。そのため、戻り値の型を JMenuBar としている。

これまでは、戻り値の型が void ではない場合には、コンパイルエラーを回避するために何らかの無意味な値を返していた。しかし、無意味な値を返すメソッドの中身を書き忘れてしまうと、意外と発見しにくいバグになることが多い。

そこで、中身を書き忘れていたらすぐに気が付くように、例外をスローすることにする。

Java には java.lang.UnsupportedOperationException という、使用できない操作を実行しようとしたことを表すチェックされない (RuntimeException を継承した)例外がある。これをスローすれば、プログラムを実行した瞬間に実装していないことに気が付くので便利である。

// メニューバーを作成する
protected JMenuBar createMenuBar() {
    throw new UnsupportedOperationException("createMenuBar");
}

先ほどの init() メソッドもこのようにしておいてもよい。

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

擬似コード「プログラム全体」にあわせて、クラス「TextViewer」内にpublic static void main(String[] args) から始まるメソッドを作成する。

全体の骨格

ここまでのプログラムの骨格は以下のようになる (init, main は例外をスローしなくてもよい)。

package j2.lesson11;

import javax.swing.*;

public class TextViewer {

    // ウィンドウを初期化する
    protected void init() {
        throw new UnsupportedOperationException("init");
    }
    
    // メニューバーを作成する
    protected JMenuBar createMenuBar() {
        throw new UnsupportedOperationException("createMenuBar");
    }

    // プログラム全体
    public static void main(String[] args) {
        throw new UnsupportedOperationException("main");
    }
}

骨格テスト

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

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

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

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

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

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

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

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

// ウィンドウを初期化する
protected void init() {
    // window = タイトルが "text viewer" の新しいウィンドウ
    // menubar = メニューバーを作成する
    // window に menubar を登録
    // textarea = テキストエリアを作成する
    // textarea をスクロール可能にする
    // window に textarea を登録
    // window の初期サイズを 400x400 に設定
    // window を表示
    throw new UnsupportedOperationException("init");
}

ウィンドウを作成する

擬似コードの最初の部分では、ウィンドウを作成している。

window = タイトルが "text viewer" の新しいウィンドウ

これまでと同じようにウィンドウを作成し、同時に終了時に自動的にウィンドウを処分するようにする。

// window = タイトルが "text viewer" の新しいウィンドウ
JFrame window = new JFrame("text viewer");
window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

メニューバーを作成する

擬似コードの次の部分では、メニューバーを作成してウィンドウに登録している。

menubar = メニューバーを作成する
window に menubar を登録

メニューバーを作成するプログラムは、「createMenuBar()」という別のメソッドに追い出したので、ここではそのメソッドを呼び出すだけでよい。

作成したメニューバーは、「JFrame.setJMenuBar」メソッドで登録する。

// menubar = メニューバーを作成する
JMenuBar menubar = createMenuBar();
// window に menubar を登録
window.setJMenuBar(menubar);

テキストエリアを作成する

擬似コードの次の部分では、テキストを表示するためのテキストエリアを作成してウィンドウに貼り付けている。

textarea = テキストエリアを作成する
textarea をスクロール可能にする
window に textarea を登録

テキストエリアは、javax.swing.JTextArea クラスを使えばよい。ただし、このコンポーネントにはスクロール機能が付いていないので、スクロール機能を持つコンテナ javax.swing.JScrollPane に貼り付けることによってスクロール機能を与える。

// textarea = テキストエリアを作成する
JTextArea textarea = new JTextArea();
// textarea をスクロール可能にする
// window に textarea を登録
window.add(new JScrollPane(textarea));

ウィンドウを表示する

最後に、ウィンドウを表示する。

window の初期サイズを 400 x 400 に設定
window を表示

これはそのまま、setSize メソッドと setVisible メソッドによって表示の設定を行えばよい。

// window の初期サイズを 400x400 に設定
window.setSize(400, 400);
// window を表示
window.setVisible(true);

最後まで書き終わったので、例外をスローする部分は消してかまわない。

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

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

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

// メニューバーを作成する
protected JMenuBar createMenuBar() {
    // bar = 新しいメニューバー
    // file = "ファイル" メニュー
    // open = "ファイル" メニュー内の "開く" メニュー
    // barにメニューを追加
    // return bar
    throw new UnsupportedOperationException("createMenuBar");
}

メニューバーに使われる Swing のコンポーネントは、次の3種類がある。

下のレイアウト設計図を元に、順にaddしていくだけなので問題ない。


// メニューバーを作成する
protected JMenuBar createMenuBar() {
    // bar = 新しいメニューバー
    JMenuBar bar = new JMenuBar();
    
    // file = "ファイル" メニュー
    JMenu file = new JMenu("ファイル");
    
    // open = "ファイル" メニュー内の "開く" メニュー
    JMenuItem open = new JMenuItem("開く");
    file.add(open);
    
    // barにメニューを追加
    bar.add(file);
    
    // return bar
    return bar;
}

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

main メソッドは TextViewer のインスタンスを生成して、init メソッドを呼び出すだけである。

// プログラム全体
public static void main(String[] args) {
    // viewer = テキストビューア
    TextViewer viewer = new TextViewer();
    
    // viewer のウィンドウを初期化する
    viewer.init();
}

プログラムの実行

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


また、機能は持たないがメニューも正しく動作していることが確認できる。


プログラム全体

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

package j2.lesson11;

import javax.swing.*;

public class TextViewer {

    // ウィンドウを初期化する
    protected void init() {
        // window = タイトルが "text viewer" の新しいウィンドウ
        JFrame window = new JFrame("text viewer");
        window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        
        // menubar = メニューバーを作成する
        JMenuBar menubar = createMenuBar();
        
        // window に menubar を登録
        window.setJMenuBar(menubar);
        
        // textarea = テキストエリアを作成する
        JTextArea textarea = new JTextArea();
        
        // textarea をスクロール可能にする
        // window に textarea を登録
        window.add(new JScrollPane(textarea));
        
        // window の初期サイズを 400x400 に設定
        window.setSize(400, 400);
        
        // window を表示
        window.setVisible(true);
    }
    
    // メニューバーを作成する
    protected JMenuBar createMenuBar() {
        // bar = 新しいメニューバー
        JMenuBar bar = new JMenuBar();
        
        // file = "ファイル" メニュー
        JMenu file = new JMenu("ファイル");
        
        // open = "ファイル" メニュー内の "開く" メニュー
        JMenuItem open = new JMenuItem("開く");
        file.add(open);
        
        // barにメニューを追加
        bar.add(file);
        
        // return bar
        return bar;
    }

    // プログラム全体
    public static void main(String[] args) {
        // viewer = テキストビューア
        TextViewer viewer = new TextViewer();
        
        // viewer のウィンドウを初期化する
        viewer.init();
    }
}

アクションの設計

画面設計の部分が終わったので、次はこの画面にアクションを追加する。

このプログラムのアクションを表すクラスとして FileOpenAction を作成する。

アクションの確認

このプログラムでは、「"ファイル > 開く" というメニューが選択された際に、ファイル選択画面を出して選択されたファイルをテキストエリアに表示する」といった処理を行う。

このアクションを実現するため、先ほど紙に書いたレイアウトのうち、貼り付けられているコンポーネントに識別のための記号 (a, b, c など) を割り当てる。


そして、「"ファイル > 開く" というメニューが選択された際に、ファイル選択画面を出して選択されたファイルをテキストエリアに表示する」という表現を、先ほど割り当てた記号を使って正確に言葉で表現する

前回の演習では、アクションにコンポーネントを渡して処理を行っていたが、今回は少し考え方を変えて、下図のような流れで行うことにする。


「イベントが発生したらファイルを開く」のではなく「イベントが発生したらテキストビューアにファイルを開いてもらう」というという考え方になる。

このように考えると、先ほどのアクションは次のような単純なものに置き換わる。

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

ファイルを開く
    1. ファイル選択ダイアログを開く
    (1 でファイルが選択された場合)
    2. 選択されたファイルを読み込む
    3. ファイルの内容をテキストエリア (b) に表示させる
    4. アクションを終了
    (1 でファイルが選択されなかった場合)
    2a. テキストエリア (b) に "ファイルが選択されませんでした" と表示させる
    3a. アクションを終了
    (2 でファイル読み込めなかった場合)
    3b. テキストエリア (b) に "ファイルが読み込めませんでした" と表示させる
    4b. アクションを終了

このままでは読みにくいので、これまでと同様の擬似コードの形に変換する。

ファイルを開く
    ファイル選択ダイアログを開く
    selected = 選択されたファイル
    if ファイルが選択された
        text = selected の内容
        if ファイルが読み込めなかった
            テキストエリア (b) に "ファイルが読み込めませんでした" と表示
        else
            テキストエリア (b) に text の内容を表示
    else (ファイルが選択されなかった)
        テキストエリア (b) に "ファイルが選択されませんでした" と表示

TextViewer の変更

TextViewer クラスには、上記の「ファイルを開く」処理を追加する必要がある。

インスタンスフィールドの抽出

まず、「ファイルを開く」処理で必要となるコンポーネントを探す。

ファイルを開く
    ファイル選択ダイアログを開く
    selected = 選択されたファイル
    if ファイルが選択された
        text = selected の内容
        if ファイルが読み込めなかった
            テキストエリア (b) に "ファイルが読み込めませんでした" と表示
        else
            テキストエリア (b) に text の内容を表示
    else (ファイルが選択されなかった)
        テキストエリア (b) に "ファイルが選択されませんでした" と表示

まず、「ファイル選択ダイアログを開く」という処理には、JFrame が必要であった。また、「テキストエリア (b)」を操作しているので、次の2つのインスタンスフィールドが必要となる。

protected JFrame window;
protected JTextArea textarea;

後の演習で使用するため、private とすべきところを protected で宣言している。

init メソッドの変更

先ほど作成した2つのインスタンスフィールドは、宣言しただけでは意味がないため、適切なオブジェクトを代入する必要がある。

そこで、これまでローカル変数として宣言していた下記の2つを、

// window = タイトルが "text viewer" の新しいウィンドウ
JFrame window = new JFrame("text viewer");
// textarea = テキストエリアを作成する
JTextArea textarea = new JTextArea();

次のように、インスタンスフィールドを使用するように変更すればよい。

// window = タイトルが "text viewer" の新しいウィンドウ
this.window = new JFrame("text viewer");
// textarea = テキストエリアを作成する
this.textarea = new JTextArea();

ローカル変数として宣言しなければ、自動的にインスタンスフィールドとして扱われる。区別をはっきりとつけるために他に window や textarea を使っている場面でも this をつけて参照するとよい。

「ファイルを開く」メソッドの追加

準備が整ったので、擬似コード「ファイルを開く」が表すメソッドを TextViewer クラスに追加する。

ファイルを開く
    ファイル選択ダイアログを開く
    selected = 選択されたファイル
    if ファイルが選択された
        text = selected の内容
        if ファイルが読み込めなかった
            テキストエリア (b) に "ファイルが読み込めませんでした" と表示
        else
            テキストエリア (b) に text の内容を表示
    else (ファイルが選択されなかった)
        テキストエリア (b) に "ファイルが選択されませんでした" と表示

ファイル選択ダイアログは javax.swing.JFileChooser クラスを使うことによって表示できる。使い方を簡単に書くと、下記のようになる。

// ファイル選択ダイアログを開く
JFileChooser chooser = new JFileChooser();
chooser.showOpenDialog(this.window);
// selected = 選択されたファイル
File selected = chooser.getSelectedFile();
// if ファイルが選択された
if (selected != null) {
    // ファイルが選択された場合の処理
}
// else (ファイルが選択されなかった)
else {
    // ファイルが選択されなかった場合の処理
}

ただし、ここで java.io.File を使用するため、import java.io.*; が必要となるため、宣言しておく。

「ファイルを開く」メソッドとして、public void fileOpen() というメソッドを作成し、上記の JFileChooser のサンプルを貼り付ける。

// ファイルを開く
public void fileOpen() {
    // ファイル選択ダイアログを開く
    JFileChooser chooser = new JFileChooser();
    chooser.showOpenDialog(this.window);
    // selected = 選択されたファイル
    File selected = chooser.getSelectedFile();
    // if ファイルが選択された
    if (selected != null) {
        // ファイルが選択された場合の処理
    }
    // else (ファイルが選択されなかった)
    else {
        // ファイルが選択されなかった場合の処理
    }
}

そこに、足りない部分の擬似コードをコメントとして貼り付ける。

// if ファイルが選択された
if (selected != null) {
    // text = selected の内容
    // if ファイルが読み込めなかった
        // テキストエリア (b) に "ファイルが読み込めませんでした" と表示
    // else
        // テキストエリア (b) に text の内容を表示
}
// else (ファイルが選択されなかった)
else {
    // ファイルが選択されなかった場合の処理
    // テキストエリア (b) に "ファイルが選択されませんでした" と表示
}

javax.swing.JTextArea にテキストを書き込む場合は、次のメソッドが便利である。

上記2つのメソッドを使用すると、これまでの知識でテキストエリアにファイルの内容が表示できる。

// ファイルを開く
public void fileOpen() {
    // ファイル選択ダイアログを開く
    JFileChooser chooser = new JFileChooser();
    chooser.showOpenDialog(this.window);
    // selected = 選択されたファイル
    File selected = chooser.getSelectedFile();
    // if ファイルが選択された
    if (selected != null) {
        // # getPath() をしないでそのまま渡してもよい
        BufferedReader reader;
        try {
            reader = new BufferedReader(new FileReader(selected.getPath()));
        }
        // if ファイルが読み込めなかった
        catch (FileNotFoundException e) {
            // テキストエリア (b) に "ファイルが読み込めませんでした" と表示
            this.textarea.setText("ファイルが読み込めませんでした");
            return;
        }
        try {
            // text = selected の内容
            // else (if-else の順番が逆転している)
            // テキストエリア (b) に text の内容を表示
            // 空に設定
            this.textarea.setText("");
            while (true) {
                String line = reader.readLine();
                if (line == null) {
                    break;
                }
                this.textarea.append(line);
                // # 改行も追加
                this.textarea.append("\n");
            }
        }
        // if ファイルが読み込めなかった
        catch (IOException e) {
            // テキストエリア (b) に "ファイルが読み込めませんでした" と表示
            this.textarea.setText("ファイルが読み込めませんでした");
        }
        finally {
            try {
                reader.close();
            }
            catch (IOException e) {
                // # 閉じる際の例外は無視
            }
        }
    }
    // else (ファイルが選択されなかった)
    else {
        // テキストエリア (b) に "ファイルが選択されませんでした" と表示
        this.textarea.setText("ファイルが選択されませんでした");
    }
}

理解できない場合は、第21回 - Exception and I/O (1) -などを参考にすること。

早期テスト

上記のような設計を行うことによって、メニューにアクションを登録する前にテストを行うことができる。

TextViewer クラスの main メソッドを、テスト用に次のように変更する。

// プログラム全体
public static void main(String[] args) {
    // viewer = テキストビューア
    TextViewer viewer = new TextViewer();
    
    // viewer のウィンドウを初期化する
    viewer.init();
    
    // テスト用にファイルを開いてみる
    viewer.fileOpen();
}

これでプログラムを起動すると、ウィンドウが表示された直後にファイル選択画面が表示される。様々なファイルを選択したり、選択をキャンセルしたりして試してみよ。

動作を確認したら、下記はコメントアウトするか消しておくこと。

// テスト用にファイルを開いてみる
viewer.fileOpen();

FileOpenAction クラスの作成

次に、下記の動作をするアクションを作成する。

インターフェースの抽出

今回は javax.swing.AbstractAction ではなく、java.awt.event.ActionListener を使用してイベント処理を記述する。

フィールドの抽出

アクションを記述するクラスに必要なインスタンスフィールドを抽出する。

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

コンストラクタの抽出

今回は javax.swing.AbstractAction ではなく、java.awt.event.ActionListener を使用してイベント処理を記述するため、コンストラクタはフィールドを初期化するためだけに作ればよい。

メソッドの抽出

java.awt.event.ActionListener を実装するクラスでイベントハンドラ (アクション) を記述するには、次のメソッドを用意する必要がある。

アクションを記述するには上記のメソッドだけ公開すればよいので、必要な公開メソッドは上記のものだけである。

骨格の作成

クラスの作成

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

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

また、このクラスでは ActionListener インターフェースと ActionEvent クラスを使用するため、次の import 文を書いておく。

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction {
}

実装するインターフェースの設定

アクションイベントリスナを作成するため、java.awt.event.ActionListener を実装する。

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {
}

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

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

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {

    // テキストビューアのインスタンス
    private final TextViewer textViewer;
}

コンストラクタの作成

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

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

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {

    // テキストビューアのインスタンス
    private final TextViewer textViewer;
    
    // コンストラクタ
    //   textViewer - テキストビューア 
    public FileOpenAction(TextViewer textViewer) {
        this.textViewer = textViewer;
    }
}

メソッドの作成

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

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {

    // テキストビューアのインスタンス
    private final TextViewer textViewer;
    
    // コンストラクタ
    //   textViewer - テキストビューア 
    public FileOpenAction(TextViewer textViewer) {
        this.textViewer = textViewer;
    }

    // メニューの File > Open が選択された際のアクション
    public void actionPerformed(ActionEvent e) {
    }
}

全体の骨格

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

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {

    // テキストビューアのインスタンス
    private final TextViewer textViewer;
    
    // コンストラクタ
    //   textViewer - テキストビューア 
    public FileOpenAction(TextViewer textViewer) {
        this.textViewer = textViewer;
    }

    // メニューの File > Open が選択された際のアクション
    public void actionPerformed(ActionEvent e) {
    }
}

骨格テスト

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

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

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

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

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

プログラムへの変換

「テキストビューアにファイルを開いてもらう」だけなので、そのまま書けばよい。

// メニューの File > Open が選択された際のアクション
public void actionPerformed(ActionEvent e) {
    // 1. テキストビューアにファイルを開いてもらう
    this.textViewer.fileOpen();
}

TextViewer クラスの変更

アクションを作成したら、先ほど作成した画面に統合する必要がある。

このアクションのトリガ (発生条件) は、次のようなものであった。

TextViewer クラス内の、該当するメニューにこのアクションを設定する。

具体的には、変更すべき箇所は下記の部分である。

// open = "ファイル" メニュー内の "開く" メニュー
JMenuItem open = new JMenuItem("開く");

このメニューに作成したアクションリスナを渡し、メニューが選択された際に処理を委譲させる

// open = "ファイル" メニュー内の "開く" メニュー
JMenuItem open = new JMenuItem("開く");
open.addActionListener(new FileOpenAction(this));

引数に TextViewer が必要だが、自分自身が TextViewer なので this を渡せばよい。

プログラムの実行

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

ファイル > 開く を選択するとファイル選択ダイアログが表示されるので、適当なテキストファイル (ソースコードなどでもよい) を開いて表示してみる。

機能テスト

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

「FileOpenActionに対する機能テスト」を実行する。

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

メッセージ 詳細
フレーム <F> が見つかりません <F> の名前を持つフレームが見つからない。JFrame のコンストラクタに指定している文字列を確認
フレーム <F> は javax.swing.JFrame のインスタンスではありません <F> の名前を持つフレームが JFrame のインスタンスでない。java.awt.Frame を誤って使用していないか確認
ダイアログが表示されませんでした ファイルを操作するダイアログが表示されるべき場面で、ダイアログが表示されなかった
フレームへの遷移に失敗しました ダイアログが閉じられるべき場面で、フレームをアクティブにできなかった
<機能テストの項目名> <機能テストの項目名> で、FAIL ボタンを押した

機能テストの項目

テストの概要 意味
起動直後のウィンドウ プログラムを起動した直後に表示されるウィンドウの状態
ファイルメニューを選択 メニューバーの "ファイル" を選択した場合のウィンドウの状態
開くメニューを選択 メニューバーの "ファイル" > "開く" を選択した場合のウィンドウの状態
pr2402sample1.txt を開いた ファイル選択時に <現在のディレクトリ>/.temp/(左のファイル名) を開いた場合のウィンドウの状態
pr2402sample2.txt を開いた ファイル選択時に <現在のディレクトリ>/.temp/(左のファイル名) を開いた場合のウィンドウの状態
ファイルが開けなかった場合 開けないファイルを選択した場合のウィンドウの状態
ファイルの選択をキャンセル ファイル選択画面でキャンセルを押した場合のウィンドウの状態

プログラム全体

FileOpenAction 全体を掲載しておく。

package j2.lesson11;

import java.awt.event.*;

public class FileOpenAction implements ActionListener {

    // テキストビューアのインスタンス
    private final TextViewer textViewer;
    
    // コンストラクタ
    //   textViewer - テキストビューア 
    public FileOpenAction(TextViewer textViewer) {
        super();
        this.textViewer = textViewer;
    }

    // メニューの File > Open が選択された際のアクション
    public void actionPerformed(ActionEvent e) {
        // 1. テキストビューアにファイルを開いてもらう
        this.textViewer.fileOpen();
    }
}

また、変更後の TextViewer も掲載する。

package j2.lesson11;

import java.io.*;
import javax.swing.*;

public class TextViewer {

    protected JFrame window;
    protected JTextArea textarea;

    // ウィンドウを初期化する
    protected void init() {
        // window = タイトルが "text viewer" の新しいウィンドウ
        this.window = new JFrame("text viewer");
        this.window.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
        
        // menubar = メニューバーを作成する
        JMenuBar menubar = createMenuBar();
        
        // window に menubar を登録
        this.window.setJMenuBar(menubar);
        
        // textarea = テキストエリアを作成する
        this.textarea = new JTextArea();
        
        // textarea をスクロール可能にする
        // window に textarea を登録
        this.window.add(new JScrollPane(this.textarea));
        
        // window の初期サイズを 400x400 に設定
        this.window.setSize(400, 400);
        
        // window を表示
        this.window.setVisible(true);
    }
    
    // メニューバーを作成する
    protected JMenuBar createMenuBar() {
        // bar = 新しいメニューバー
        JMenuBar bar = new JMenuBar();
        
        // file = "ファイル" メニュー
        JMenu file = new JMenu("ファイル");
        
        // open = "ファイル" メニュー内の "開く" メニュー
        JMenuItem open = new JMenuItem("開く");
        open.addActionListener(new FileOpenAction(this));
        file.add(open);
        
        // barにメニューを追加
        bar.add(file);
        
        // return bar
        return bar;
    }
    
    // ファイルを開く
    public void fileOpen() {
        // ファイル選択ダイアログを開く
        JFileChooser chooser = new JFileChooser();
        chooser.showOpenDialog(this.window);
        // selected = 選択されたファイル
        File selected = chooser.getSelectedFile();
        // if ファイルが選択された
        if (selected != null) {
            // # getPath() をしないでそのまま渡してもよい
            BufferedReader reader;
            try {
                reader = new BufferedReader(new FileReader(selected.getPath()));
            }
            // if ファイルが読み込めなかった
            catch (FileNotFoundException e) {
                // テキストエリア (b) に "ファイルが読み込めませんでした" と表示
                this.textarea.setText("ファイルが読み込めませんでした");
                return;
            }
            try {
                // text = selected の内容
                // else (if-else の順番が逆転している)
                // テキストエリア (b) に text の内容を表示
                // 空に設定
                this.textarea.setText("");
                while (true) {
                    String line = reader.readLine();
                    if (line == null) {
                        break;
                    }
                    this.textarea.append(line);
                    // # 改行も追加
                    this.textarea.append("\n");
                }
            }
            // if ファイルが読み込めなかった
            catch (IOException e) {
                // テキストエリア (b) に "ファイルが読み込めませんでした" と表示
                this.textarea.setText("ファイルが読み込めませんでした");
            }
            finally {
                try {
                    reader.close();
                }
                catch (IOException e) {
                    // # 閉じる際の例外は無視
                }
            }
        }
        // else (ファイルが選択されなかった)
        else {
            // テキストエリア (b) に "ファイルが選択されませんでした" と表示
            this.textarea.setText("ファイルが選択されませんでした");
        }
    }

    // プログラム全体
    public static void main(String[] args) {
        // viewer = テキストビューア
        TextViewer viewer = new TextViewer();
        
        // viewer のウィンドウを初期化する
        viewer.init();
    }
}