WinFormsでつくる「4択クイズ」完全ガイド(個人制作編)
対象:C#基礎(変数/配列/条件分岐/ループ/配列の基本操作)を終えた人
到達目標:
- CSV(外部ファイル)から問題を読み込み、4択クイズを自力で完成できる
- イベント駆動(ボタンクリック)と配列の実用を体験できる
- 完成後、小さな設計分割(最低1クラス抽出)に挑戦できる
目次
作るもの
- 画面:問題文(Label)/解答ボタン×4(Button)/結果ログ(ListBox)
- 流れ:起動 → 1問表示 → どれかのボタンを押す → 正誤判定 → ログに記録 → 次の問題へ
- 問題データ:questions.csv(後述のフォーマット)
開発環境
- Windows / Visual Studio 2022 以降
- .NET 6 以上(WinForms テンプレート)
Step 1. プロジェクトとUIを用意する
- Visual Studio で「Windows フォーム アプリ」を作成(.NET 6 以上)
ソリューション名・プロジェクト名は「QuizApp」 - Form1 を開き、以下を配置・命名する
- Label … lblQuestion(AutoSize を True(既定)に。長文なら MaximumSize を調整)
- Button × 4 … btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4
- ListBox … lstLog(幅広めに)
ここでは命名が重要です。後で配列にまとめて扱います。
Step 2. CSV を用意する
- プロジェクト直下に questions.csv を追加(**UTF-8(BOM)**推奨)
- プロパティ → 「出力ディレクトリにコピー」を 常にコピー に設定
- フォーマット(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 を差し替えると内容が変わるか(ビルド後も有効)
よくあるつまずき
- CSVが読めない
- questions.csv のプロパティ → 「出力ディレクトリにコピー」を 常にコピー に
- 実行ファイルのあるフォルダに CSV があることを確認
- クリックしても反応しない
- 4つのボタンすべてに 同じハンドラ(OnAnswerClicked)を紐づけたか
- 文字化け
- CSV は UTF-8(BOM) 推奨
- 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回
ディスカッション
コメント一覧
まだ、コメントがありません