WinFormsでつくる「4択クイズ」完全ガイド(個人制作編)

対象:C#基礎(変数/配列/条件分岐/ループ/配列の基本操作)を終えた人

到達目標

  • CSV(外部ファイル)から問題を読み込み、4択クイズを自力で完成できる
  • イベント駆動(ボタンクリック)と配列の実用を体験できる
  • 完成後、小さな設計分割(最低1クラス抽出)に挑戦できる

作るもの

  • 画面:問題文(Label)/解答ボタン×4(Button)/結果ログ(ListBox)
  • 流れ:起動 → 1問表示 → どれかのボタンを押す → 正誤判定 → ログに記録 → 次の問題へ
  • 問題データ:questions.csv(後述のフォーマット)

開発環境

  • Windows / Visual Studio 2022 以降
  • .NET 6 以上(WinForms テンプレート)

Step 1. プロジェクトとUIを用意する

  1. Visual Studio で「Windows フォーム アプリ」を作成(.NET 6 以上)
    ソリューション名・プロジェクト名は「QuizApp
  2. Form1 を開き、以下を配置・命名する
    • Label … lblQuestion(AutoSize を True(既定)に。長文なら MaximumSize を調整)
    • Button × 4 … btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4
    • ListBox … lstLog(幅広めに)

ここでは命名が重要です。後で配列にまとめて扱います。


Step 2. CSV を用意する

  1. プロジェクト直下に questions.csv を追加(**UTF-8(BOM)**推奨)
  2. プロパティ → 「出力ディレクトリにコピー」を 常にコピー に設定
  3. フォーマット(1行目はヘッダ、以降は1問=1行):
question,choice1,choice2,choice3,choice4,correctIndex
「C#で配列の先頭インデックスは?」,1,0,2,3,1
「WinFormsでクリック時に発火するイベントは?」,Hover,Click,Shown,Load,1
  • correctIndex は 0~3(1番目の選択肢なら0)
  • 文字化けする場合はエディタの保存形式を確認(UTF-8 BOM)

Step 3. まずは「動く版」を完成させる

以下を そのままコピペ して動かして構いません。

Question と QuestionLoader は最小限です。

Question.cs

using System;

public sealed class Question
{
    public string Text { get; }
    public string[] Choices { get; }
    public int CorrectIndex { get; }

    public Question(string text, string[] choices, int correctIndex)
    {
        Text = text;
        Choices = choices;
        CorrectIndex = correctIndex;
    }
}

QuestionLoader.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

public sealed class QuestionLoader
{
    private readonly List<Question> _questions = new();
    private readonly Random _rand = new();

    public QuestionLoader(string csvPath)
    {
        var lines = File.ReadAllLines(csvPath);
        if (lines.Length <= 1) throw new InvalidOperationException("問題がありません。");

        foreach (var line in lines.Skip(1)) // ヘッダをスキップ
        {
            if (string.IsNullOrWhiteSpace(line)) continue;
            var cols = line.Split(',');
            if (cols.Length < 6) continue;

            var text = cols[0];
            var choices = new[] { cols[1], cols[2], cols[3], cols[4] };
            if (!int.TryParse(cols[5], out var correct)) continue;

            _questions.Add(new Question(text, choices, correct));
        }
        if (_questions.Count == 0) throw new InvalidOperationException("有効な問題がありません。");
    }

    public Question GetRandom() => _questions[_rand.Next(_questions.Count)];
}

Form1.cs(核心)

using System;
using System.Windows.Forms;

namespace QuizApp
{
    public partial class Form1 : Form
    {
        private Button[] _answerButtons = Array.Empty<Button>();
        private QuestionLoader _loader = null!;
        private Question _current = null!;
        private int _total, _correct;

        public Form1()
        {
            InitializeComponent();

            // 4つのボタンを配列にまとめる(同じ処理で扱える)
            _answerButtons = new[] { btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4 };
            foreach (var b in _answerButtons)
            {
                b.Click += OnAnswerClicked; // 共通ハンドラにまとめる
            }

            lstLog.Items.Add("クイズを開始します。");
            _loader = new QuestionLoader("questions.csv");
            LoadNext();
        }

        private void LoadNext()
        {
            _current = _loader.GetRandom();
            lblQuestion.Text = _current.Text;
            for (int i = 0; i < 4; i++)
            {
                _answerButtons[i].Text = _current.Choices[i];
            }
        }

        private void OnAnswerClicked(object? sender, EventArgs e)
        {
            if (sender is not Button btn) return;

            // どのボタンが押されたか? → 配列内の位置(0..3)を調べる
            int index = Array.IndexOf(_answerButtons, btn);

            _total++;
            bool ok = (index == _current.CorrectIndex);
            if (ok) _correct++;

            lstLog.Items.Add(ok ? "正解!" : $"不正解…(正解は {_current.CorrectIndex + 1} 番)");
            Text = $"スコア:{_correct}/{_total}";

            LoadNext();
        }
    }
}

なぜ Array.IndexOf を使うのか

  • 4つのボタンすべてに if (btn == btnAnswer1) … else if … と書くと重複が増え、ミスしやすい
  • 配列にまとめれば、1本のロジックで判定でき、拡張(ボタン数の増減)にも強い

代替案:各ボタンの Tag に 0~3 を設定し、index = (int)btn.Tag; とする方法もあります。


Step 4. テストとチェック

  • 起動時に最初の問題が表示されるか
  • どのボタンを押しても、正誤判定 → ログに追記 → 次の問題になっているか
  • フォームのタイトルに スコア:正解数/挑戦数 が表示されているか
  • questions.csv を差し替えると内容が変わるか(ビルド後も有効

よくあるつまずき

  1. CSVが読めない
    • questions.csv のプロパティ → 「出力ディレクトリにコピー」を 常にコピー に
    • 実行ファイルのあるフォルダに CSV があることを確認
  2. クリックしても反応しない
    • 4つのボタンすべてに 同じハンドラ(OnAnswerClicked)を紐づけたか
  3. 文字化け
    • CSV は UTF-8(BOM) 推奨
  4. IndexOutOfRange(配列外参照)
    • CSV の列数が足りない、correctIndex が 0~3 になっていない等を確認

ここから先は「設計力」強化(任意の発展)

まずは動く版を完成させたら、少しずつ分割しましょう。

最低でも以下どれか 1つ を抽出できれば合格ラインです。

  • AnswerChecker(正誤判定を担当)
  • ScoreManager(正解数・挑戦数・正答率の管理)
  • UiUpdater(ラベル・ボタン・ログの更新責務を分離)

例:ScoreManager(簡易)

public sealed class ScoreManager
{
    public int Total { get; private set; }
    public int Correct { get; private set; }
    public double Rate => Total == 0 ? 0 : (double)Correct / Total;

    public void Record(bool isCorrect)
    {
        Total++;
        if (isCorrect) Correct++;
    }
}
  • Form1 側では _score.Record(ok); → Text = $"スコア:{_score.Correct}/{_score.Total}"; のように置き換え

さらに伸ばすための発展課題(任意)

  • 選択肢のシャッフル(毎問、ボタン配置を入れ替える)
  • 出題の重複を避ける(一度出た問題を除外)
  • 10問で終了して結果ダイアログ
  • タイマー(制限時間・時間切れで不正解扱い)
  • 結果保存(CSVに履歴を追記)
  • 例外・入力検証(列不足・変換失敗時のガード)

ミニ用語メモ

  • イベント駆動:ユーザー操作(クリックなど)をきっかけ(イベント)にして処理が動く仕組み
  • 単一責任:1つのクラス/メソッドには1つの役割だけを持たせる考え方
  • 配列にまとめる理由:複数のコントロールを同じロジックで処理でき、重複を減らせる

まとめ

  • まずは「動く版」を完成させ、イベント駆動と配列の扱いを体で覚える
  • その後、小さく設計分割して「読みやすい/直しやすい」コードへ
  • CSV を差し替えるだけで中身が変わる「データ駆動」の便利さも体験できる

この資料のコードを写経して動かし、少しずつ自分の発想を足していくのが上達の近道です。

訪問数 27 回, 今日の訪問数 1回

C#

Posted by hidepon