
6日でできる 新Java入門|改良4:クイズゲームをGUIにする
SwingでクイズゲームをGUI化して、クリックで遊べるアニメクイズアプリを完成させよう
ここまでの記事では、CUIで動いていたクイズゲームを少しずつ発展させてきました。
まず、問題データを差し替えて、ドラゴンボール・クイズ、鬼滅の刃・クイズ、ジョジョの奇妙な冒険・クイズを扱えるようにしました。
次に、Swing入門①とSwing入門②で、GUIアプリを作るための基本部品とイベント処理を確認しました。
そして今回の「改良4:クイズゲームをGUIにする」では、いよいよ完成版のプログラムを作成します。
これまでのCUI版では、コンソールに文字を表示し、キーボードから番号を入力して答えていました。
今回のGUI版では、ウィンドウを表示し、ジャンルをコンボボックスから選び、スタートボタンを押して、選択肢ボタンをクリックして回答します。
つまり、これまでのクイズゲームが「入力して遊ぶプログラム」だったのに対して、今回は「画面を操作して遊ぶアプリ」になります。
今回掲載するファイルは、次の3つです。
| ファイル名 | 役割 |
|---|---|
| QuizGame.java | Swingの画面作成、画面遷移、ボタン操作を担当 |
| QuizGenre.java | クイズジャンル、ファイル名、タイトル、問題数を管理 |
| QuizManager.java | 問題読み込み、ランダム出題、選択肢生成、正誤判定、スコア管理を担当 |
また、クイズ問題には次のテキストファイルを使います。
| テキストファイル名 | 内容 |
|---|---|
| dragonball_quiz.txt | ドラゴンボール・クイズの問題 |
| kimetsu_quiz.txt | 鬼滅の刃・クイズの問題 |
| jojo_quiz.txt | ジョジョの奇妙な冒険・クイズの問題 |
これらの問題ファイルの中身は、前の記事「改良1:別のクイズ問題にする」で掲載したものを使用します。
この記事では、問題ファイルの内容は再掲せず、GUI化したJavaプログラムの全体像と動作を中心に解説します。
プログラムの全体像
今回のGUI版クイズゲームは、3つのJavaファイルで構成します。
| クラス | 主な役割 |
|---|---|
| QuizGame | GUI画面を作り、ユーザー操作に応じて画面を切り替える |
| 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 | 出題数 |
今回の対応関係は次のとおりです。
| enum | fileName | title | questionCount |
|---|---|---|---|
| DRAGONBALL | dragonball_quiz.txt | ドラゴンボール・クイズ | 10 |
| KIMETSU | kimetsu_quiz.txt | 鬼滅の刃・クイズ | 10 |
| JOJO | jojo_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() | チートモードかどうかを返す |
| QuizQuestion | 1問分の情報をまとめる内部クラス |
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 cheatEclipseで実行する場合
Eclipseで実行する場合は、次のように進めます。
| 手順 | 内容 |
|---|---|
| 1 | Javaプロジェクトを作成する |
| 2 | QuizGame.java、QuizGenre.java、QuizManager.java を作成する |
| 3 | dragonball_quiz.txt、kimetsu_quiz.txt、jojo_quiz.txt をプロジェクト直下に配置する |
| 4 | QuizGame.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アプリ開発の入口としても大きな一歩になります。
