6日でできる 新Java入門|改良4:クイズゲームをGUIにする

SwingでクイズゲームをGUI化して、クリックで遊べるアニメクイズアプリを完成させよう

ここまでの記事では、CUIで動いていたクイズゲームを少しずつ発展させてきました。

まず、問題データを差し替えて、ドラゴンボール・クイズ、鬼滅の刃・クイズ、ジョジョの奇妙な冒険・クイズを扱えるようにしました。
次に、Swing入門①とSwing入門②で、GUIアプリを作るための基本部品とイベント処理を確認しました。

そして今回の「改良4:クイズゲームをGUIにする」では、いよいよ完成版のプログラムを作成します。

これまでのCUI版では、コンソールに文字を表示し、キーボードから番号を入力して答えていました。
今回のGUI版では、ウィンドウを表示し、ジャンルをコンボボックスから選び、スタートボタンを押して、選択肢ボタンをクリックして回答します。

つまり、これまでのクイズゲームが「入力して遊ぶプログラム」だったのに対して、今回は「画面を操作して遊ぶアプリ」になります。

今回掲載するファイルは、次の3つです。

ファイル名役割
QuizGame.javaSwingの画面作成、画面遷移、ボタン操作を担当
QuizGenre.javaクイズジャンル、ファイル名、タイトル、問題数を管理
QuizManager.java問題読み込み、ランダム出題、選択肢生成、正誤判定、スコア管理を担当

また、クイズ問題には次のテキストファイルを使います。

テキストファイル名内容
dragonball_quiz.txtドラゴンボール・クイズの問題
kimetsu_quiz.txt鬼滅の刃・クイズの問題
jojo_quiz.txtジョジョの奇妙な冒険・クイズの問題

これらの問題ファイルの中身は、前の記事「改良1:別のクイズ問題にする」で掲載したものを使用します。
この記事では、問題ファイルの内容は再掲せず、GUI化したJavaプログラムの全体像と動作を中心に解説します。

プログラムの全体像

今回のGUI版クイズゲームは、3つのJavaファイルで構成します。

クラス主な役割
QuizGameGUI画面を作り、ユーザー操作に応じて画面を切り替える
QuizGenre選択できるクイズジャンルを enum で管理する
QuizManagerクイズの問題データやスコアを管理する

CUI版では、main()メソッド の中で、ジャンル選択、問題読み込み、出題、入力受付、判定までをまとめて処理していました。

GUI版では、役割を分けます。

処理担当クラス
ウィンドウ表示QuizGame
タイトル画面の作成QuizGame
クイズ画面の作成QuizGame
結果画面の作成QuizGame
ジャンル情報の管理QuizGenre
問題ファイルの読み込みQuizManager
ランダム出題QuizManager
選択肢生成QuizManager
正誤判定QuizManager
スコア管理QuizManager

このように分けることで、画面に関する処理とクイズの中身に関する処理が混ざりにくくなります。

GUI版の実行遷移

GUI版クイズゲームは、次の流れで動きます。

順番画面・処理内容
1起動QuizGame の main() から開始
2タイトル画面ジャンル選択とスタートボタンを表示
3ジャンル選択JComboBox でジャンルを選ぶ
4スタートstartGame() が実行される
5問題読み込みQuizManager が問題ファイルを読み込む
6クイズ画面問題文と4つの選択肢ボタンを表示
7回答ボタンを押すと checkAnswer() が実行される
8判定正解・不正解を表示
9次の問題Timer で少し待ってから次の問題へ進む
10結果画面全問終了後に得点を表示
11タイトルへボタンでタイトル画面へ戻る

画面は、CardLayout を使って切り替えます。

カード名表示する画面
TITLEタイトル画面
QUIZクイズ画面
RESULT結果画面

GUI版では、コンソールに順番に文字を出すのではなく、画面の状態を切り替えながらゲームを進めるのが大きな特徴です。

QuizGame.java

QuizGame.java は、Swingの画面全体を担当するクラスです。
JFrame を継承し、タイトル画面、クイズ画面、結果画面を作成します。

また、スタートボタンや選択肢ボタンが押されたときのイベント処理も、このクラスで扱います。

ファイル名:QuizGame.java

import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

public class QuizGame extends JFrame {
    private boolean cheatMode;

    private CardLayout cardLayout;
    private JPanel mainPanel;

    private JComboBox<QuizGenre> genreComboBox;
    private JLabel quizTitleLabel;
    private JLabel questionCountLabel;
    private JLabel questionLabel;
    private JLabel resultMessageLabel;
    private JLabel scoreLabel;
    private JButton[] choiceButtons;

    private QuizManager manager;
    private QuizManager.QuizQuestion currentQuestion;

    public QuizGame(boolean cheatMode) {
        this.cheatMode = cheatMode;

        setTitle("アニメクイズゲーム");
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        setSize(760, 520);
        setLocationRelativeTo(null);

        cardLayout = new CardLayout();
        mainPanel = new JPanel(cardLayout);

        mainPanel.add(createTitlePanel(), "TITLE");
        mainPanel.add(createQuizPanel(), "QUIZ");
        mainPanel.add(createResultPanel(), "RESULT");

        add(mainPanel);
        showTitle();
    }

    private JPanel createTitlePanel() {
        JPanel panel = new JPanel(new BorderLayout());
        panel.setBackground(new Color(235, 245, 255));

        JLabel titleLabel = new JLabel("アニメクイズゲーム", SwingConstants.CENTER);
        titleLabel.setFont(new Font("Meiryo", Font.BOLD, 38));
        titleLabel.setBorder(BorderFactory.createEmptyBorder(50, 10, 20, 10));

        JLabel descriptionLabel = new JLabel("ジャンルを選んで「スタート」を押してください", SwingConstants.CENTER);
        descriptionLabel.setFont(new Font("Meiryo", Font.PLAIN, 18));

        genreComboBox = new JComboBox<QuizGenre>(QuizGenre.values());
        genreComboBox.setFont(new Font("Meiryo", Font.PLAIN, 18));
        genreComboBox.setPreferredSize(new Dimension(420, 40));

        JButton startButton = new JButton("スタート");
        startButton.setFont(new Font("Meiryo", Font.BOLD, 22));
        startButton.setPreferredSize(new Dimension(180, 50));

        startButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                startGame();
            }
        });

        JPanel centerPanel = new JPanel();
        centerPanel.setOpaque(false);
        centerPanel.setLayout(new BoxLayout(centerPanel, BoxLayout.Y_AXIS));

        JPanel comboPanel = new JPanel();
        comboPanel.setOpaque(false);
        comboPanel.add(genreComboBox);

        JPanel buttonPanel = new JPanel();
        buttonPanel.setOpaque(false);
        buttonPanel.add(startButton);

        centerPanel.add(descriptionLabel);
        centerPanel.add(Box.createVerticalStrut(25));
        centerPanel.add(comboPanel);
        centerPanel.add(Box.createVerticalStrut(25));
        centerPanel.add(buttonPanel);

        panel.add(titleLabel, BorderLayout.NORTH);
        panel.add(centerPanel, BorderLayout.CENTER);

        return panel;
    }

    private JPanel createQuizPanel() {
        JPanel panel = new JPanel(new BorderLayout());
        panel.setBackground(Color.WHITE);

        quizTitleLabel = new JLabel("", SwingConstants.CENTER);
        quizTitleLabel.setFont(new Font("Meiryo", Font.BOLD, 24));
        quizTitleLabel.setBorder(BorderFactory.createEmptyBorder(20, 10, 5, 10));

        questionCountLabel = new JLabel("", SwingConstants.CENTER);
        questionCountLabel.setFont(new Font("Meiryo", Font.PLAIN, 16));

        questionLabel = new JLabel("", SwingConstants.CENTER);
        questionLabel.setFont(new Font("Meiryo", Font.BOLD, 24));
        questionLabel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));

        resultMessageLabel = new JLabel(" ", SwingConstants.CENTER);
        resultMessageLabel.setFont(new Font("Meiryo", Font.BOLD, 18));
        resultMessageLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));

        JPanel topPanel = new JPanel(new GridLayout(2, 1));
        topPanel.setOpaque(false);
        topPanel.add(quizTitleLabel);
        topPanel.add(questionCountLabel);

        JPanel choicePanel = new JPanel(new GridLayout(4, 1, 10, 10));
        choicePanel.setBackground(Color.WHITE);
        choicePanel.setBorder(BorderFactory.createEmptyBorder(10, 90, 10, 90));

        choiceButtons = new JButton[4];
        for (int i = 0; i < choiceButtons.length; i++) {
            final int index = i;
            choiceButtons[i] = new JButton();
            choiceButtons[i].setFont(new Font("Meiryo", Font.PLAIN, 18));
            choiceButtons[i].setFocusPainted(false);
            choiceButtons[i].addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    checkAnswer(index);
                }
            });
            choicePanel.add(choiceButtons[i]);
        }

        JPanel centerPanel = new JPanel(new BorderLayout());
        centerPanel.setOpaque(false);
        centerPanel.add(questionLabel, BorderLayout.NORTH);
        centerPanel.add(choicePanel, BorderLayout.CENTER);
        centerPanel.add(resultMessageLabel, BorderLayout.SOUTH);

        panel.add(topPanel, BorderLayout.NORTH);
        panel.add(centerPanel, BorderLayout.CENTER);

        return panel;
    }

    private JPanel createResultPanel() {
        JPanel panel = new JPanel(new BorderLayout());
        panel.setBackground(new Color(245, 250, 255));

        JLabel titleLabel = new JLabel("ゲーム終了", SwingConstants.CENTER);
        titleLabel.setFont(new Font("Meiryo", Font.BOLD, 36));
        titleLabel.setBorder(BorderFactory.createEmptyBorder(60, 10, 20, 10));

        scoreLabel = new JLabel("", SwingConstants.CENTER);
        scoreLabel.setFont(new Font("Meiryo", Font.BOLD, 26));

        JButton titleButton = new JButton("タイトルへ");
        titleButton.setFont(new Font("Meiryo", Font.BOLD, 22));
        titleButton.setPreferredSize(new Dimension(200, 55));
        titleButton.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                showTitle();
            }
        });

        JPanel buttonPanel = new JPanel();
        buttonPanel.setOpaque(false);
        buttonPanel.add(titleButton);

        panel.add(titleLabel, BorderLayout.NORTH);
        panel.add(scoreLabel, BorderLayout.CENTER);
        panel.add(buttonPanel, BorderLayout.SOUTH);

        return panel;
    }

    private void startGame() {
        try {
            QuizGenre quizGenre = (QuizGenre) genreComboBox.getSelectedItem();

            manager = new QuizManager(
                quizGenre.getFileName(),
                quizGenre.getTitle(),
                quizGenre.getQuestionCount(),
                cheatMode
            );

            manager.startQuiz();
            quizTitleLabel.setText(quizGenre.getTitle());
            showNextQuestion();
            cardLayout.show(mainPanel, "QUIZ");

        } catch (Exception e) {
            JOptionPane.showMessageDialog(
                this,
                "問題データの読み込みに失敗しました。\n" + e.getMessage(),
                "エラー",
                JOptionPane.ERROR_MESSAGE
            );
        }
    }

    private void showNextQuestion() {
        currentQuestion = manager.nextQuestion();

        if (currentQuestion == null) {
            showResult();
            return;
        }

        questionCountLabel.setText(currentQuestion.getNumber() + "問目 / " + manager.getQuestionCount() + "問");
        questionLabel.setText("<html><div style='text-align:center;'>「" + currentQuestion.getQuestionText() + "」</div></html>");
        resultMessageLabel.setText(" ");

        for (int i = 0; i < choiceButtons.length; i++) {
            choiceButtons[i].setEnabled(true);
            choiceButtons[i].setText((i + 1) + ":" + currentQuestion.getChoice(i));
        }

        if (manager.isCheatMode()) {
            resultMessageLabel.setText("【チートモード】正解は「" + currentQuestion.getCorrectAnswer() + "」です。");
        }
    }

    private void checkAnswer(int selectedIndex) {
        boolean correct = manager.checkAnswer(currentQuestion, selectedIndex);

        for (int i = 0; i < choiceButtons.length; i++) {
            choiceButtons[i].setEnabled(false);
        }

        if (correct) {
            resultMessageLabel.setText("正解です!");
        } else {
            resultMessageLabel.setText("不正解。正解は「" + currentQuestion.getCorrectAnswer() + "」です。");
        }

        Timer timer = new Timer(1200, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                showNextQuestion();
            }
        });
        timer.setRepeats(false);
        timer.start();
    }

    private void showResult() {
        scoreLabel.setText(
            "<html><div style='text-align:center;'>全" + manager.getQuestionCount()
            + "問終了!<br>あなたの得点は " + manager.getScore() + " 点です。</div></html>"
        );
        cardLayout.show(mainPanel, "RESULT");
    }

    private void showTitle() {
        cardLayout.show(mainPanel, "TITLE");
    }

    public static void main(String[] args) {
        final boolean cheatMode = (args.length > 0 && args[0].equals("cheat"));

        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                new QuizGame(cheatMode).setVisible(true);
            }
        });
    }
}

QuizGame.java のポイント

QuizGame.java では、Swingの画面操作を中心に処理しています。

メソッド役割
QuizGame(boolean cheatMode)ウィンドウと各画面を初期化する
createTitlePanel()タイトル画面を作る
createQuizPanel()クイズ画面を作る
createResultPanel()結果画面を作る
startGame()ジャンルを取得してゲームを開始する
showNextQuestion()次の問題を画面に表示する
checkAnswer(int selectedIndex)回答ボタンの正誤判定を行う
showResult()結果画面を表示する
showTitle()タイトル画面を表示する
main(String[] args)Swingアプリを起動する

このクラスでは、GUIに関係する処理が中心です。
問題の読み込みや選択肢生成は QuizManager に任せているため、画面制御の役割がはっきりしています。

QuizGenre.java

QuizGenre.java は、ジャンル情報を管理する enum です。
GUI版では、JComboBox に QuizGenre.values() を渡して、ジャンル一覧を表示します。

ファイル名:QuizGenre.java

public enum QuizGenre {
    DRAGONBALL(1, "dragonball_quiz.txt", "ドラゴンボール・クイズ", 10),
    KIMETSU(2, "kimetsu_quiz.txt", "鬼滅の刃・クイズ", 10),
    JOJO(3, "jojo_quiz.txt", "ジョジョの奇妙な冒険・クイズ", 10);

    private int code;
    private String fileName;
    private String title;
    private int questionCount;

    QuizGenre(int code, String fileName, String title, int questionCount) {
        this.code = code;
        this.fileName = fileName;
        this.title = title;
        this.questionCount = questionCount;
    }

    public int getCode() {
        return code;
    }

    public String getFileName() {
        return fileName;
    }

    public String getTitle() {
        return title;
    }

    public int getQuestionCount() {
        return questionCount;
    }

    public static QuizGenre fromInt(int code) {
        for (QuizGenre g : QuizGenre.values()) {
            if (g.code == code) {
                return g;
            }
        }
        return DRAGONBALL;
    }

    public String toString() {
        return title;
    }
}

QuizGenre.java のポイント

QuizGenre では、各ジャンルに次の情報を持たせています。

項目内容
codeジャンル番号
fileName読み込む問題ファイル名
title画面に表示するタイトル
questionCount出題数

今回の対応関係は次のとおりです。

enumfileNametitlequestionCount
DRAGONBALLdragonball_quiz.txtドラゴンボール・クイズ10
KIMETSUkimetsu_quiz.txt鬼滅の刃・クイズ10
JOJOjojo_quiz.txtジョジョの奇妙な冒険・クイズ10

特に重要なのは toString() です。

public String toString() {
    return title;
}

JComboBox に enum を入れたとき、画面には toString() の戻り値が表示されます。
これにより、DRAGONBALL ではなく ドラゴンボール・クイズ と表示できます。

QuizManager.java

QuizManager.java は、クイズの中身を管理するクラスです。
問題ファイルの読み込み、ランダム出題、選択肢生成、正誤判定、スコア管理を担当します。

GUI版では、CUI版のように startQuiz() の中で一気に全問を出すのではなく、nextQuestion() で1問ずつ問題を返す形になっています。

ファイル名:QuizManager.java

import java.io.BufferedReader;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.Random;

public class QuizManager {
    private String fileName;
    private String title;
    private int questionCount;
    private boolean cheatMode;

    private ArrayList<String> questions = new ArrayList<String>();
    private ArrayList<String> answers = new ArrayList<String>();

    private ArrayList<String> quizQuestions;
    private ArrayList<String> quizAnswers;

    private int score;
    private int currentNumber;
    private Random rand = new Random();

    public QuizManager(String fileName, String title, int questionCount, boolean cheatMode) throws Exception {
        this.fileName = fileName;
        this.title = title;
        this.questionCount = questionCount;
        this.cheatMode = cheatMode;
        loadQuestions();
    }

    private void loadQuestions() throws Exception {
        BufferedReader br = new BufferedReader(new FileReader(fileName));
        String line;

        while ((line = br.readLine()) != null) {
            int idx = line.indexOf(',');

            if (idx == -1) {
                continue;
            }

            questions.add(line.substring(0, idx));
            answers.add(line.substring(idx + 1));
        }

        br.close();
    }

    public void startQuiz() {
        score = 0;
        currentNumber = 0;

        quizQuestions = new ArrayList<String>(questions);
        quizAnswers = new ArrayList<String>(answers);
    }

    public QuizQuestion nextQuestion() {
        if (currentNumber >= questionCount || quizQuestions.size() == 0) {
            return null;
        }

        int qIdx = rand.nextInt(quizQuestions.size());
        String questionText = quizQuestions.remove(qIdx);
        String correctAnswer = quizAnswers.remove(qIdx);

        ArrayList<String> choices = new ArrayList<String>();
        ArrayList<String> backupAnswers = new ArrayList<String>(quizAnswers);

        for (int j = 0; j < 3 && backupAnswers.size() > 0; j++) {
            int cIdx = rand.nextInt(backupAnswers.size());
            choices.add(backupAnswers.remove(cIdx));
        }

        int insertIdx = rand.nextInt(choices.size() + 1);
        choices.add(insertIdx, correctAnswer);

        currentNumber++;

        return new QuizQuestion(
            currentNumber,
            title,
            questionText,
            correctAnswer,
            choices,
            insertIdx
        );
    }

    public boolean checkAnswer(QuizQuestion question, int selectedIndex) {
        boolean correct = (selectedIndex == question.getCorrectIndex());

        if (correct) {
            score++;
        }

        return correct;
    }

    public int getScore() {
        return score;
    }

    public int getQuestionCount() {
        return questionCount;
    }

    public boolean isCheatMode() {
        return cheatMode;
    }

    public static class QuizQuestion {
        private int number;
        private String title;
        private String questionText;
        private String correctAnswer;
        private ArrayList<String> choices;
        private int correctIndex;

        public QuizQuestion(
            int number,
            String title,
            String questionText,
            String correctAnswer,
            ArrayList<String> choices,
            int correctIndex
        ) {
            this.number = number;
            this.title = title;
            this.questionText = questionText;
            this.correctAnswer = correctAnswer;
            this.choices = choices;
            this.correctIndex = correctIndex;
        }

        public int getNumber() {
            return number;
        }

        public String getTitle() {
            return title;
        }

        public String getQuestionText() {
            return questionText;
        }

        public String getCorrectAnswer() {
            return correctAnswer;
        }

        public String getChoice(int index) {
            return choices.get(index);
        }

        public int getCorrectIndex() {
            return correctIndex;
        }
    }
}

QuizManager.java のポイント

QuizManager では、クイズのロジックをまとめています。

メソッド・クラス役割
loadQuestions()問題ファイルを読み込む
startQuiz()スコアや出題用リストを初期化する
nextQuestion()次の1問を作成して返す
checkAnswer()選択された番号が正解か判定する
getScore()現在の得点を返す
getQuestionCount()出題数を返す
isCheatMode()チートモードかどうかを返す
QuizQuestion1問分の情報をまとめる内部クラス

GUI版で重要なのは、nextQuestion() が1問分の情報を QuizQuestion として返すことです。

QuizQuestion には、次の情報が入っています。

情報内容
number何問目か
titleクイズタイトル
questionText問題文
correctAnswer正解
choices選択肢一覧
correctIndex正解の位置

QuizGame は、この QuizQuestion を受け取り、画面に表示します。

CUI版からGUI版への大きな変化

CUI版では、for文の中で問題を出し、入力を受け取り、判定して、次の問題へ進んでいました。

GUI版では、ユーザーがボタンを押すたびに処理が進みます。
そのため、クイズの進行方法が変わっています。

CUI版GUI版
for文で全問を順番に処理nextQuestion() で1問ずつ取得
キーボード入力で回答選択肢ボタンで回答
System.out.println で表示JLabel や JButton の setText で表示
入力後すぐ次の処理へ進むTimer で少し待って次の問題へ進む
コンソールに結果表示結果画面に得点表示

つまり、GUI版では、クイズのロジックを一気に進めるのではなく、画面操作に合わせて1問ずつ進める形に変えています。

プログラムの実行遷移を詳しく見る

ここで、GUI版の実行遷移をもう少し詳しく見てみます。

1. main() から起動する

QuizGame.java の main() で cheatMode を判定します。

final boolean cheatMode = (args.length > 0 && args[0].equals("cheat"));

その後、SwingUtilities.invokeLater を使って画面を起動します。

SwingUtilities.invokeLater(new Runnable() {
    public void run() {
        new QuizGame(cheatMode).setVisible(true);
    }
});

Swingでは、GUIの作成や更新は専用の流れで行うのが基本です。
そのため、invokeLater の中で画面を作っています。

2. タイトル画面を表示する

コンストラクタで JFrame の設定を行い、CardLayout に3つの画面を登録します。

mainPanel.add(createTitlePanel(), "TITLE");
mainPanel.add(createQuizPanel(), "QUIZ");
mainPanel.add(createResultPanel(), "RESULT");

最初は showTitle() によってタイトル画面を表示します。

3. スタートボタンでゲーム開始

タイトル画面でスタートボタンを押すと、startGame() が実行されます。

startButton.addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        startGame();
    }
});

startGame() では、選択されたジャンルを取得し、QuizManager を作成します。

QuizGenre quizGenre = (QuizGenre) genreComboBox.getSelectedItem();

manager = new QuizManager(
    quizGenre.getFileName(),
    quizGenre.getTitle(),
    quizGenre.getQuestionCount(),
    cheatMode
);

4. 最初の問題を表示する

QuizManager の startQuiz() でクイズを初期化し、showNextQuestion() で1問目を表示します。

manager.startQuiz();
quizTitleLabel.setText(quizGenre.getTitle());
showNextQuestion();
cardLayout.show(mainPanel, "QUIZ");

これで画面はクイズ画面へ切り替わります。

5. 回答ボタンで正誤判定する

選択肢ボタンを押すと、checkAnswer(index) が実行されます。

choiceButtons[i].addActionListener(new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        checkAnswer(index);
    }
});

checkAnswer() では、QuizManager に判定を依頼します。

boolean correct = manager.checkAnswer(currentQuestion, selectedIndex);

正解なら 正解です!、不正解なら正しい答えを表示します。

6. Timerで次の問題へ進む

判定結果を表示したあと、Timer で1.2秒待ってから次の問題へ進みます。

Timer timer = new Timer(1200, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        showNextQuestion();
    }
});
timer.setRepeats(false);
timer.start();

すぐに次の問題へ切り替えないことで、ユーザーが結果を読めるようにしています。

7. 全問終了で結果画面へ進む

showNextQuestion() で次の問題がなくなると、showResult() を呼びます。

if (currentQuestion == null) {
    showResult();
    return;
}

showResult() では、最終得点を表示して RESULT 画面に切り替えます。

cardLayout.show(mainPanel, "RESULT");

チートモードの動き

チートモードは、コマンドライン引数に cheat を指定すると有効になります。

java QuizGame cheat

チートモードが有効な場合、問題表示時に正解も表示されます。

if (manager.isCheatMode()) {
    resultMessageLabel.setText("【チートモード】正解は「" + currentQuestion.getCorrectAnswer() + "」です。");
}

通常モードで実行した場合は、この表示は出ません。

java QuizGame
実行方法動作
java QuizGame通常モード
java QuizGame cheatチートモード

チートモードは、GUI化してもCUI版と同じ考え方で使えます。
違うのは、正解表示をコンソールではなく resultMessageLabel に表示している点です。

問題ファイルについて

今回のGUI版クイズゲームでは、次の3つの問題ファイルを使います。

ファイル名内容
dragonball_quiz.txtドラゴンボール・クイズ
kimetsu_quiz.txt鬼滅の刃・クイズ
jojo_quiz.txtジョジョの奇妙な冒険・クイズ

これらのファイルは、Eclipseの場合はプロジェクト直下に配置します。

ファイルの中身は、次の形式にします。

問題文,正解

例です。

孫悟空のサイヤ人としての名前,カカロット

この記事では、問題ファイルの中身は掲載しません。
問題ファイルの内容は、前の記事「改良1:別のクイズ問題にする」で作成したものを使用します。

プログラムのポイント

画面切り替えにCardLayoutを使っている

今回のGUI版では、タイトル画面、クイズ画面、結果画面を CardLayout で切り替えています。

画面カード名
タイトル画面TITLE
クイズ画面QUIZ
結果画面RESULT

CardLayout を使うことで、1つのウィンドウ内で複数画面を切り替えられます。

1問分の情報をQuizQuestionにまとめている

QuizManager の内部クラス QuizQuestion は、1問分の情報をまとめるためのクラスです。

情報内容
問題番号何問目か
問題文表示する問題
正解正しい答え
選択肢4択の内容
正解位置どの選択肢が正解か

これにより、QuizGame 側では currentQuestion から必要な情報を取り出して画面に表示できます。

GUI処理とクイズ処理を分けている

QuizGame は画面担当、QuizManager はクイズ処理担当です。

この分担により、画面の変更とクイズロジックの変更を分けて考えられます。

変更したい内容主に見るファイル
画面デザインQuizGame.java
ジャンル追加QuizGenre.java
出題方法QuizManager.java
採点方法QuizManager.java
問題内容テキストファイル

Timerで自然なテンポを作っている

回答ボタンを押したあと、すぐに次の問題へ進まず、1.2秒待つようにしています。

Timer timer = new Timer(1200, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        showNextQuestion();
    }
});

これにより、ユーザーが正解・不正解のメッセージを確認できます。

JOptionPaneで読み込みエラーを表示している

問題ファイルが見つからない場合や、読み込みに失敗した場合は、JOptionPane でエラー表示します。

JOptionPane.showMessageDialog(
    this,
    "問題データの読み込みに失敗しました。\n" + e.getMessage(),
    "エラー",
    JOptionPane.ERROR_MESSAGE
);

GUIアプリでは、コンソールにエラーを出すだけでなく、画面上に分かりやすく表示することが大切です。

実行方法

コマンドラインで実行する場合

まず、QuizGame.java、QuizGenre.java、QuizManager.java を同じプロジェクト内に用意します。
さらに、dragonball_quiz.txt、kimetsu_quiz.txt、jojo_quiz.txt をプロジェクト直下に配置します。

通常モードで実行する場合です。

java QuizGame

チートモードで実行する場合です。

java QuizGame cheat

Eclipseで実行する場合

Eclipseで実行する場合は、次のように進めます。

手順内容
1Javaプロジェクトを作成する
2QuizGame.java、QuizGenre.java、QuizManager.java を作成する
3dragonball_quiz.txt、kimetsu_quiz.txt、jojo_quiz.txt をプロジェクト直下に配置する
4QuizGame.java を実行する
5チートモードにしたい場合は 実行構成 のプログラムの引数に cheat を入力する

通常モードでは、プログラムの引数は空のままで実行します。

実行例

メニューの「実行」→「実行構成」をクリックします。

「引数」のタブで、「プログラムの引数」に「cheat」と入力し、「実行」ボタンをクリックします。

実行結果のイメージ

タイトル画面

起動すると、ウィンドウにタイトル画面が表示されます。

ここでジャンルを選択し、スタートボタンを押します。

クイズ画面

ドラゴンボール・クイズを選んだ場合、次のような画面になります。

選択肢ボタンをクリックすると、正解・不正解が表示されます。

不正解の場合は、次のように表示されます。

チートモードの場合は、回答前に正解が表示されます。

結果画面

全10問が終了すると、結果画面に切り替わります。

タイトルへボタンを押すと、最初のタイトル画面に戻ります。

図:GUI版クイズゲームの全体構成

↓クリックすると拡大表示されます。

この図が示していること

GUI版クイズゲームで、QuizGame、QuizGenre、QuizManager、問題ファイルがどのように連携しているかを示しています。

この図から分かること

QuizGame は画面操作、QuizGenre はジャンル設定、QuizManager はクイズ処理を担当しており、役割を分けることでプログラムが整理されていることが分かります。

図:GUI版クイズゲームの画面遷移

↓クリックすると拡大表示されます。

この図が示していること

タイトル画面、クイズ画面、結果画面が CardLayout によって切り替わりながら、クイズゲームが進行する様子を示しています。

この図から分かること

GUI版では、コンソールに文字を流すのではなく、画面を切り替えながらユーザー操作に反応してゲームが進むことが分かります。

今回の完成形で身につくこと

今回のGUI化によって、クイズゲームはCUIアプリからSwingを使ったGUIアプリへ発展しました。

この完成版から学べることは、かなり多いです。

学べること内容
Swingの基本構成JFrame、JPanel、JLabel、JButton などの使い方
画面遷移CardLayout による画面切り替え
イベント処理ActionListener によるボタン操作
遅延処理Timer による自然な画面更新
エラー表示JOptionPane によるダイアログ表示
クラス分担GUI処理とクイズ処理の分離
データ読み込みテキストファイルから問題を読み込む
enum活用QuizGenre によるジャンル管理
内部クラスQuizQuestion による1問分の情報管理

特に大切なのは、画面処理とクイズ処理を分けている点です。

QuizGame.java にすべてを書き込むこともできますが、それではコードが長くなりすぎます。
QuizManager.java にクイズの中身を任せることで、画面側のコードが整理されます。

この構成を理解できれば、今後さらに次のような改造もしやすくなります。

改造案主に変更する場所
背景色やデザインを変えるQuizGame.java
問題数を変えるQuizGenre.java
新ジャンルを追加するQuizGenre.java と問題ファイル
ヒント機能を追加するQuizGame.java と QuizManager.java
結果画面を詳しくするQuizGame.java
ランキング機能を追加するQuizManager.java または新しいクラス

今回の改良によって、アニメクイズゲームは、テキストベースのCUIプログラムから、ボタンで操作できるGUIアプリへ完成しました。
ここまでの流れを理解できれば、Javaの基本文法だけでなく、GUIアプリ開発の入口としても大きな一歩になります。