4人チームで作るWinFormsクイズアプリ(完全チュートリアル)
- 1. はじめに
- 2. Step0. 関連情報
- 3. Step 1. プロジェクト準備
- 4. Step 2. CSVファイルの用意
- 5. Step 3. チームでの役割分担
- 6. Step 4. コード実装
- 6.1. QuestionLoader.cs(担当A)
- 6.2. 意味と役割
- 6.3. まとめ:QuestionLoader(string path = “questions.csv") の役割
- 6.4. 何が起きているか
- 6.5. 典型的な使い方
- 6.6. 実務での注意点・改善
- 6.7. 参考:continue の挙動の違い
- 6.8. AnswerChecker.cs(担当B)
- 6.9. 初学者向けに省略しない形
- 6.10. 解説
- 6.11. まとめ
- 6.12. ScoreManager.cs(担当C)
- 6.13. 1. GetResult メソッドのままにする場合
- 6.14. 2. ToString() をオーバーライドする場合
- 6.15. 3. 実務的にはどうするか?
- 6.16. 結論
- 6.17. 両方を備えた ScoreManager の例
- 6.18. 使い方の例
- 6.19. まとめ
- 6.20. UiUpdater.cs(担当D)
- 6.21. Form1.cs(リーダー統合)
- 6.22. 1. new[] { btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4 }
- 6.23. 2. Array.IndexOf(配列, 探す対象)
- 6.24. 3. btn とは?
- 7. 4. 流れをまとめると
- 8. 5. 実行例
- 9. Step 5. GitHub Desktopでの運用ルール
- 10. まとめ
- 11. 参考)このクイズの仕組み
はじめに
チーム開発の練習題材として最適なのが「クイズアプリ」です。
本記事では、GitHub Desktopを使った最小運用ルールと、4人での役割分担を取り入れながら、WinFormsでクイズアプリを完成させるチュートリアルを紹介します。
このチュートリアルを通じて、以下のスキルを習得できます:
- WinForms の基本操作(ボタンやラベルなどのUI部品の扱い)
- 複数クラスによる責務分担(問題管理・判定処理・スコア管理・UI制御の分離)
- チーム開発の基本プロセス(役割ごとの実装と統合、GitHub Desktopを使ったバージョン管理)
- 設計原則の基礎理解(単一責任の原則やUIとロジックの分離)
Step0. 関連情報
これらの基礎知識を身につけておくと運用を潤滑に進めることが少しずつできるようになります
図で理解する Git/GitHub 基本フロー ー バージョン管理入門
初心者チームの Git 運用ルール(GitHub Desktop 版)
Visual Studio + GitHub Desktop で快適にソース管理を始めよう
GitHub Desktop と WinForms で始めるチーム開発 ~はじめてのステップ~
GitHub Desktop を使った ブランチ戦略とプルリクエスト運用
簡単な Git のコンフリクトのパターンと修正(GitHub Desktop で実践)
やり直し
失敗前提で“怖くないGit”——GitHub Desktopでやり直す実践ガイド(Discard復元まで)
プルリクエス&マージについて知りたい
GitHub Desktopにおけるmainブランチからの変更取り込み方法 ~マージとリベースの比較~
GitHub Desktopで学ぶブランチ & PR(プルリクエスト)
GitHub Desktop実践入門:CSV編集で体験するGitワークフロー
Step 1. プロジェクト準備
- リーダーが新規WinFormsアプリを作成
- プロジェクト名:TeamQuizApp
- フォームに配置するUI:
- Label(問題文表示用、名前:lblQuestion)
- Button ×4(選択肢、名前:btnAnswer1 ~ btnAnswer4)
- ListBox(解答ログ、名前:listBoxLog)
- リポジトリ作成と共有
- GitHub Desktopでリポジトリを作成し、初期Push。(2.のGitの初期化を参考に)
- メンバー全員がCloneして開発スタート。
Step 2. CSVファイルの用意
プロジェクト直下に questions.csv を作成し、プロパティで「出力ディレクトリにコピー → 常にコピー」に設定します。
理由は「実行時にプログラムが参照するのは“出力フォルダ(bin/…)”だから」です。
プロジェクト直下に置いた questions.csv は、そのままでは実行ファイルと同じ場所にありません。相対パス(例:"questions.csv")で読み込むコードは通常、実行ファイルのある出力フォルダ(bin/Debug/netX.X[-windows]/ 等)を起点に探すため、出力先にコピーしておかないと見つからず FileNotFoundException になります。
なぜ「常にコピー(Copy always)」なのか
- 毎ビルドで必ず最新化:CSVだけを直した場合でも、確実に新しい内容が出力フォルダへ反映されます(初心者がつまずきやすい「古いCSVが残っていた」問題を防ぎます)。
- 構成やフレームワーク切替でも安心:Debug/Release や net8.0/net8.0-windows のように出力先が複数できても、各出力先へ確実にコピーされます。
- 相対パスでシンプルに書ける:コード側で複雑なパス指定や配置調整を気にせずに済みます。
補足:「新しい場合にコピー(Copy if newer)」でも多くは動きますが、まれにタイムスタンプや増分ビルドの都合で更新が反映されず古いCSVを読んでしまう混乱が起きます。教材・演習では“確実に一致”が重要なので「常にコピー」を勧めています。
典型的な読み込みコード(安全策)
相対パスに依存せず、実行ファイルのある場所を起点にすると堅牢です。
using System;
using System.IO;
string csvPath = Path.Combine(AppContext.BaseDirectory, "questions.csv");
// 例: File.ReadAllLines(csvPath);
注意点
- 実行時にCSVへ書き込まない:出力フォルダのCSVに実行時変更を書き戻す設計にすると、次のビルドで「常にコピー」が上書きして消えます。ユーザーの成績や設定などは、AppData 等の保存用パスに別ファイルとして書き出すのが定石です。
- 配布(Publish)時も含めたい場合は、必要に応じて Copy to Publish Directory(SDK スタイルなら CopyToPublishDirectory)の設定も確認してください。
要するに、「実行時に確実に見つかる場所へ、毎回最新のCSVを置くため」に「出力ディレクトリにコピー → 常にコピー」を指定します。
実行中に内容が変わる可能性があるデータ(成績・設定・ユーザーが編集するCSV等)は AppData 配下に保存がベスト。
一方で、配布物に含める“読み取り専用の初期CSV”は AppContext.BaseDirectory(出力先に常にコピー)から読むのが簡単です。
使い分けの理由
- AppContext.BaseDirectory(=実行ファイルのある場所)は、公開後に 書き込みできない 場合があります(Program Files 配下や単一ファイル配布など)。ビルドでも上書きされがち。
- AppData はユーザーごとの書き込み領域。永続化・更新に向いています。
- Roaming が欲しければ ApplicationData
- PCローカルで十分・容量が大きいなら LocalApplicationData
- 全ユーザー共通に置きたいなら CommonApplicationData(要管理者権限のことあり)
実装パターン(初回は種ファイルをAppDataへ展開)
using System;
using System.IO;
string seedCsv = Path.Combine(AppContext.BaseDirectory, "questions.csv"); // 配布物(常にコピー)
string dataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), // Roaming にしたい場合
"TeamQuizApp");
Directory.CreateDirectory(dataRoot);
string userCsv = Path.Combine(dataRoot, "questions.csv");
// 初回起動など:AppData にユーザー用CSVが無ければ、種CSVをコピー
if (!File.Exists(userCsv))
{
File.Copy(seedCsv, userCsv, overwrite: false);
}
// 以降、読み書きは AppData 上の userCsv を使う
string csvPath = userCsv;
// 例) 読み込み
var lines = File.ReadAllLines(csvPath);
// 例) 書き込み
// File.WriteAllLines(csvPath, lines);
ポイント
- 配布用の種CSV(教材のデフォルト問題集)は「出力ディレクトリにコピー → 常にコピー」でビルド成果物に含める。
- 実行時の更新先は AppData(上の userCsv)。ここだけを編集・保存対象にする。
- 更新ロジック(問題集の差分配信など)があるなら、バージョン番号や最終更新日時を持たせて、起動時にマイグレーションする設計が安全。
代替案:種データを 埋め込みリソース にして、初回起動時に AppData へ書き出す方法もあります(配布ファイルを減らせる)。いずれにせよ “読み取り専用は配布元、書き込みは AppData” の分離が基本です。
questions.csv のサンプル
question,choice1,choice2,choice3,choice4,correctIndex
日本の首都はどこ?,大阪,東京,名古屋,札幌,1
C#で文字列を扱う型は?,int,string,bool,char,1
Unityでシーン切り替えに使うメソッドは?,LoadScene,ChangeScene,OpenScene,StartScene,0
Step 3. チームでの役割分担
ブランチ命名について
- 役割別 feature ブランチ(メンバーごと)
- リーダー用統合ブランチ(ホーム位置更新を担当)
どの形式がよい?
- 短くて明快な命名:feature/question-loader など。シンプルでわかりやすく、CIやコードレビュー時にも扱いやすい。
- GitHub Desktopのワークフローに合わせる:Pull → 編集 → Commit → Push が基本。CommitやPRのタイトルにも一致させやすい名前にすると統一感が出ます
ブランチ名案(役割 + リーダー統合)
ブランチ名 | 担当・内容 |
---|---|
feature/question-loader | CSVから問題読み込み(担当A) |
feature/answer-checker | 解答判定(担当B) |
feature/score-manager | スコア集計(担当C) |
feature/ui-updater | UI更新(担当D) |
feature/home-updater | リーダー:ホーム位置更新・統合 |
リーダー(L)
- プロジェクト作成・UI配置
- Form1 のイベントハンドラは空実装
- 各メンバーのコードをレビュー&統合
担当A:問題読み込み
- QuestionLoader.cs を作成
- CSVから問題を読み込む処理を実装
担当B:解答判定
- AnswerChecker.cs を作成
- 正解/不正解を判定する処理を実装
担当C:スコア管理
- ScoreManager.cs を作成
- 正解数や正答率を管理
担当D:UI更新
- UiUpdater.cs を作成
- 問題文・選択肢の表示や、ログ出力を担当
Step 4. コード実装
- モデル:Question
- データ取得:QuestionLoader
- 判定:AnswerChecker
- スコア管理:ScoreManager
- 画面更新:UiUpdater
- 統括制御:Form1
QuestionLoader.cs(担当A)
Question クラス
- 役割:1問分のデータを保持するためのモデル。
- 内容:問題文 (Text)、選択肢 (Choices)、正解インデックス (CorrectIndex) をプロパティとして持つ。
- ポイント:クイズ問題をオブジェクトとして扱えるようになる。
QuestionLoader クラス
- 役割:CSVファイルから問題を読み込み、ランダムに1問を取り出す処理を提供。
- 内容:List<Question> に問題を保持し、GetRandomQuestion() でランダムに返す。
- ポイント:データの取得とアプリ本体の処理を分離している。
using System;
using System.Collections.Generic;
using System.IO;
namespace TeamQuizApp
{
public class Question
{
public string Text { get; set; } = "";
public string[] Choices { get; set; } = new string[4];
public int CorrectIndex { get; set; }
}
public class QuestionLoader
{
private readonly List<Question> _questions = new List<Question>();
private readonly Random _rand = new Random();
public QuestionLoader(string path = "questions.csv")
{
foreach (var line in File.ReadAllLines(path))
{
if (line.StartsWith("question")) continue;
var cols = line.Split(',');
_questions.Add(new Question
{
Text = cols[0],
Choices = new[] { cols[1], cols[2], cols[3], cols[4] },
CorrectIndex = int.Parse(cols[5])
});
}
}
public Question GetRandomQuestion()
{
int idx = _rand.Next(_questions.Count);
return _questions[idx];
}
}
}
- Questionクラス
- 目的:1問のデータをまとめて扱えるようにするクラス。
- メリット:文字列をバラバラで扱うのではなく「ひとつのまとまり」として管理できる。
- Point:「Text」「Choices」「CorrectIndex」を持たせることで、問題文、4つの選択肢、正解の位置を
すべてひとまとめにできる。
- QuestionLoaderクラス
- 役割:CSV ファイルから問題を読み込み、リストとして保持する。
- File.ReadAllLines(filePath) → CSV の各行を読み込む。
- line.Split(',’) → カンマ区切りでデータを分ける。
- questions.Add(new Question(…)) → 先ほど作った Question クラスに変換してリストに入れる。
- GetRandomQuestion() → ランダムで1問返す。
意味と役割
1. コンストラクタの宣言
QuestionLoader クラスの コンストラクタ(インスタンス化時に呼ばれる初期化メソッド)です。
アクセス修飾子 public によって、他のクラスから自由に呼び出すことができます。
2. デフォルトパラメータ付きの引数
string path = “questions.csv" は デフォルト引数 を設定した宣言になっています。
- 引数 path が省略された場合、自動的に “questions.csv" を使う。
- 明示的にファイル名を指定することも可能。
たとえば:
new QuestionLoader(); // → "questions.csv" を使って読み込み
new QuestionLoader("myquiz.csv"); // → "myquiz.csv" というファイルを使って読み込み
3. CSV ファイルからの問題読み込み処理
- File.ReadAllLines(path) で指定されたファイルを行単位で読み込み。
- ヘッダー行("question"で始まる行)を if (line.StartsWith(“question")) continue; でスキップし、ヘッダーを読み飛ばす。
- CSV を line.Split(',’) によって列に分割し、Question インスタンスを生成して _questions リストに蓄積。
- 各 Question は、Text(問題文)、Choices(選択肢4つ)、CorrectIndex(正解番号)を持ちます。
4. ランダム問題取得メソッド
GetRandomQuestion() メソッドは、Random オブジェクトと _questions.Count を使ってランダムに問題を返しています。取ってきた Question は一問分の問題データです。
まとめ:QuestionLoader(string path = “questions.csv") の役割
機能 | 説明 |
---|---|
コンストラクタ | クラス初期化時に呼ばれる |
デフォルト引数 | path の省略可・省略時は “questions.csv" |
問題の読み込み | 指定した CSV から問題データを読み取り _questions に蓄積 |
ランダム取得準備 | GetRandomQuestion() で使えるように初期化する |
これは、チーム開発の「担当A:問題読み込み」のクラスであり、CSV データを扱う役割だけを持たせたシンプルで責任の明確な設計になっています。
if (line.StartsWith(“question")) continue; の意味は「その行が “question" で始まっていたら、残りの処理をスキップして次のループ反復へ進む」です。よく CSV/テキストの読み込みで、見出し行やメタ行を飛ばすときに使います。
何が起きているか
- line.StartsWith(“question")文字列 line が先頭から “question" という 接頭辞 を持つかを判定します。true なら該当行です。
- continueその時点で 現在の反復を中断 し、次の要素(foreach)や次のインデックス(for)に進みます。※ break はループ自体を終了するのに対し、continue は「この回を飛ばす」だけ。
典型的な使い方
foreach (var line in File.ReadLines(path))
{
if (line.StartsWith("question")) continue; // "question"で始まる行は無視
// ここに通常の処理(パースなど)
}
実務での注意点・改善
- 大文字小文字を無視したい:比較方法を明示しましょう。
if (line.StartsWith("question", StringComparison.OrdinalIgnoreCase)) continue;
- カルチャ影響を避けたい:既定の StartsWith(string) は現在カルチャ依存です。機械的なプレフィックス判定は StringComparison.Ordinal(または OrdinalIgnoreCase)が安全・高速。
- 先頭の空白を許容したい(例: " question,…" も飛ばす):
var trimmed = line.AsSpan().TrimStart(); // 余分なコピーを避けたい場合はSpanで
if (trimmed.StartsWith("question".AsSpan(), StringComparison.OrdinalIgnoreCase)) continue;
- ※ 単純に line = line.TrimStart(); としてから判定でも可。
- line が null の可能性:null なら StartsWith は呼べないので先にガード。
if (string.IsNullOrEmpty(line)) continue;
if (line.StartsWith("question", StringComparison.OrdinalIgnoreCase)) continue;
参考:continue の挙動の違い
- foreach:次の要素へ。
- for:continue 後に 増分式(i++ など)が実行され、その後条件判定へ。
- while:直ちに条件判定に戻る。
要するに、「特定のプレフィックスで始まる行は早めに弾くフィルター」です。処理本体をネストさせず、読みやすく・効率よくスキップできます。
AnswerChecker.cs(担当B)
- 役割:ユーザーが選んだ答えが正解かどうかを判定する。
- 内容:CheckAnswer(Question q, int index) メソッドで比較して true/false を返す。
- ポイント:判定処理を独立させることで、UIに依存しないロジックになる。
namespace TeamQuizApp
{
public class AnswerChecker
{
public bool CheckAnswer(Question q, int selectedIndex)
{
return q.CorrectIndex == selectedIndex;
}
}
}
このコードはかなり省略されていて、C#に慣れていない初学者には「何が起きているのか」が分かりにくいですね。
そこで、省略記法を使わずに、1つ1つ丁寧に書き直した形を示します。
初学者向けに省略しない形
using System;
namespace TeamQuizApp
{
// AnswerChecker クラスは、「選んだ答えが正しいかどうか」を判定する役割を持つ
public class AnswerChecker
{
// CheckAnswer メソッド
// 引数:
// q → Question 型のオブジェクト(問題文や正解の番号を持っている)
// selectedIndex → ユーザーが選んだ選択肢の番号
// 戻り値:
// bool 型(true なら正解、false なら不正解)
public bool CheckAnswer(Question q, int selectedIndex)
{
// if文で分岐処理を書く(true / false を直接 return しない)
if (q.CorrectIndex == selectedIndex)
{
// 正解だった場合
return true;
}
else
{
// 不正解だった場合
return false;
}
}
}
}
解説
1. using System;
- 標準ライブラリを使う準備。(今回のコードでは直接必要ないですが、初学者向けには「C#の基本的な書き方」として加えておくと安心です。)
2. namespace TeamQuizApp
- 名前空間。プログラム全体のグループ分け。
- クラス名が他のライブラリと衝突しないように整理する役割。
3. public class AnswerChecker
- AnswerChecker というクラスを定義。
- 「役割は答えが正しいかどうかをチェックすること」。
4. public bool CheckAnswer(Question q, int selectedIndex)
- メソッドの宣言。
- public → ほかのクラスから呼び出せる。
- bool → 戻り値は真偽値(true か false)。
- Question q → 問題データ(正解のインデックスを持つ)。
- int selectedIndex → ユーザーが選んだ番号。
5. if (q.CorrectIndex == selectedIndex)
- Question クラスが持つ CorrectIndex(正解番号)と、ユーザーが選んだ selectedIndex を比較。
- 一致していれば 正解 (true)、一致していなければ 不正解 (false) を返す。
まとめ
- 省略形の return q.CorrectIndex == selectedIndex; は短くて便利だけど、初学者には「式の結果がそのまま返る」という考え方が難しいこともある。
- 最初は if と return true/false を分けて書いた方が理解しやすい。
- 役割:選んだ答えが正解かどうかを判定する。
- == で「正解番号」と「選んだ番号」を比較。
- bool 型(true/false)を返すので、「正解か不正解か」をそのまま使える。
ScoreManager.cs(担当C)
- 役割:正解数や解答数を記録・集計する。
- 内容:CorrectCount・TotalCount をカウントし、GetResult() で結果を文字列に整形。
- ポイント:成績を管理する責務を一元化している。
namespace TeamQuizApp
{
public class ScoreManager
{
public int CorrectCount { get; private set; }
public int TotalCount { get; private set; }
public void Record(bool isCorrect)
{
TotalCount++;
if (isCorrect) CorrectCount++;
}
public string GetResult()
{
return $"正解数: {CorrectCount} / {TotalCount} (正答率 {((double)CorrectCount / TotalCount * 100):F1}%)";
}
}
}
ScoreManager のように「自分の状態を文字列にまとめて返す」機能を持たせる場合、GetResult メソッドにするか、ToString() をオーバーライドするか は設計方針の違いになります。
1. GetResult メソッドのままにする場合
var score = new ScoreManager();
score.Record(true);
score.Record(false);
Console.WriteLine(score.GetResult()); // 明示的に「結果文字列」を取得
- メリット
- 「結果を文字列で欲しい」という意図が明確。
- 他にも「JSON形式で出力する」「詳細なログ形式で出力する」などのメソッドを併設できる。
- 初学者には分かりやすい(GetResult という名前が役割をそのまま表す)。
- デメリット
- 毎回 GetResult() を呼ぶ必要がある。
- Console.WriteLine(score) と書いても結果は出力されない。
2. ToString() をオーバーライドする場合
public override string ToString()
{
return $"正解数: {CorrectCount} / {TotalCount} (正答率 {((double)CorrectCount / TotalCount * 100):F1}%)";
}
利用例:
var score = new ScoreManager();
score.Record(true);
score.Record(false);
Console.WriteLine(score); // 自動的に ToString() が呼ばれる
- メリット
- Console.WriteLine(score) のように自然に書ける。
- デバッグ時に便利(score.ToString() が呼ばれるので、ウォッチウィンドウなどで状態がすぐ確認できる)。
- 「オブジェクトを文字列にする」という C# の慣習に沿っている。
- デメリット
- 初学者には「ToString が勝手に呼ばれる」仕組みが少し分かりにくいかも。
- ToString() の用途は「デバッグ用文字列」という文化があり、業務コードではユーザー向けの結果表示と分けることも多い。
3. 実務的にはどうするか?
- デバッグ・学習用途なら ToString() オーバーライドがおすすめ。
- 「Console.WriteLine(score) と書くだけで出る」ことを体験できるのは理解が深まるからです。
- 実務・拡張性重視なら GetResult() など明示的な名前のメソッドを用意しておく方が無難。
- 例えば UI で表示形式が変わる場合、ToString() だと修正が難しくなる。
結論
- 学習段階 → ToString() オーバーライド(自然に結果が出力でき、クラス設計の勉強にもなる)
- 本格的な開発 → GetResult() のまま or 両方用意する(ToString() は開発者向けのデバッグ用、GetResult() はユーザー向け表示用)
続いて ToString() オーバーライドとGetResult() メソッドを併用した形を示します。
両方を備えた ScoreManager の例
using System;
namespace TeamQuizApp
{
public class ScoreManager
{
// 正解数
public int CorrectCount { get; private set; }
// 解答数
public int TotalCount { get; private set; }
// 正解/不正解を記録する
public void Record(bool isCorrect)
{
TotalCount++;
if (isCorrect)
{
CorrectCount++;
}
}
// 結果を「ユーザー向け表示」として返すメソッド
public string GetResult()
{
return $"正解数: {CorrectCount} / {TotalCount} (正答率 {GetAccuracy():F1}%)";
}
// 正答率だけを数値で取りたい場合に便利なプロパティ
public double GetAccuracy()
{
if (TotalCount == 0) return 0;
return (double)CorrectCount / TotalCount * 100;
}
// ToString をオーバーライドして「デバッグ時に便利な文字列」を返す
public override string ToString()
{
return $"ScoreManager: 正解 {CorrectCount}, 解答数 {TotalCount}, 正答率 {GetAccuracy():F1}%";
}
}
}
使い方の例
class Program
{
static void Main()
{
var score = new TeamQuizApp.ScoreManager();
score.Record(true); // 1問目 正解
score.Record(false); // 2問目 不正解
score.Record(true); // 3問目 正解
// GetResult メソッドを使う(ユーザー表示向け)
Console.WriteLine(score.GetResult());
// 出力: 正解数: 2 / 3 (正答率 66.7%)
// ToString が自動で呼ばれる(デバッグ向け)
Console.WriteLine(score);
// 出力: ScoreManager: 正解 2, 解答数 3, 正答率 66.7%
}
}
まとめ
- GetResult() → ユーザー向けの「きれいな結果表示」
- ToString() → デバッグや Console.WriteLine(score) で使いやすい表現
両方備えておけば、学習にも実務にも対応できます。
- 役割:スコア(正解数と挑戦数)を記録する。
- Record(bool isCorrect)
- 問題に答えるたびに呼び出す。
- totalCount を必ず +1。
- 正解なら correctCount を +1。
- GetResult()
- 今の成績を文字列として返す(例:「2 / 3 正解」)。
UiUpdater.cs(担当D)
- 役割:画面表示の更新を担当。
- 内容:問題文をラベルに表示、ボタンに選択肢をセット、リストボックスにログを追加する処理を持つ。
- ポイント:UI更新をまとめて管理することで、Form本体のコードがシンプルになる。
using System.Windows.Forms;
namespace TeamQuizApp
{
public class UiUpdater
{
private readonly Label _lblQuestion;
private readonly Button[] _buttons;
private readonly ListBox _log;
public UiUpdater(Label lblQuestion, Button[] buttons, ListBox log)
{
_lblQuestion = lblQuestion;
_buttons = buttons;
_log = log;
}
public void ShowQuestion(Question q)
{
_lblQuestion.Text = q.Text;
for (int i = 0; i < 4; i++)
{
_buttons[i].Text = q.Choices[i];
}
}
public void LogResult(string msg)
{
_log.Items.Add(msg);
}
}
}
- 役割:UI(画面表示)を更新する。
- コンストラクタで Label、Button、ListBox を受け取り、内部で保持。
- ShowQuestion() → 質問文と選択肢を UI に表示。
- LogResult() → 判定結果を ListBox に追加。
Form1.cs(リーダー統合)
- 役割:全体の流れを制御する中核。
- 内容:ロード時にQuestionLoaderで問題を読み込み、ボタンイベントで回答チェックを実行、次の問題へ進む。
- ポイント:各部品(Loader/Checker/Score/UI)を組み合わせ、イベント駆動でアプリを動かす。
using System;
using System.Windows.Forms;
namespace TeamQuizApp
{
public partial class Form1 : Form
{
private readonly QuestionLoader loader;
private readonly AnswerChecker checker;
private readonly ScoreManager score;
private readonly UiUpdater ui;
private Question current;
public Form1()
{
InitializeComponent();
loader = new QuestionLoader();
checker = new AnswerChecker();
score = new ScoreManager();
ui = new UiUpdater(lblQuestion,
new[] { btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4 },
listBoxLog);
LoadNextQuestion();
}
private void btnAnswer_Click(object sender, EventArgs e)
{
var btn = sender as Button;
int index = Array.IndexOf(new[] { btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4 }, btn);
bool result = checker.CheckAnswer(current, index);
score.Record(result);
ui.LogResult(result ? "正解!" : "不正解...");
ui.LogResult(score.GetResult());
LoadNextQuestion();
}
private void LoadNextQuestion()
{
current = loader.GetRandomQuestion();
ui.ShowQuestion(current);
}
}
}
- Form1 が 全部のまとめ役。
- 起動時に
- QuestionLoader で問題を読み込む。
- UiUpdater で UI を操作できるように準備。
- NextQuestion() で最初の問題を表示。
- btnAnswer_Click()
- 押されたボタンがどの選択肢かを調べる。
- AnswerChecker で判定。
- ScoreManager でスコア更新。
- UiUpdater で結果を表示。
- そして次の問題へ。
この1行は、三項演算子(条件演算子)?:でメッセージ文字列を分岐し、それを ui.LogResult に渡しているだけです。
ポイント
- result ? A : B は、result が true なら A、false なら B を返します。
- この例では返り値が “正解!" または “不正解…" の string になり、LogResult(string message) の引数に渡されます。
- 三項演算子は選ばれた方しか評価されません(未選択側は実行されない)。
if-else に書き換えるとこうなります(意味は同じ):
if (result)
{
ui.LogResult("正解!");
}
else
{
ui.LogResult("不正解...");
}
よくある補足
- result が bool?(null 許容)なら、条件にはそのまま使えないことがあります。安全に書くなら:
ui.LogResult(result == true ? "正解!" : "不正解...");
- 追加情報を出したいときは、未選択側だけに詳細を載せるなども可(選ばれた側しか評価されない性質を活用):
ui.LogResult(result ? "正解!" : $"不正解... 正解は {correctAnswer}");
- 型は左右で互換が必要です(この例は両方 string なのでOK)。互換でないとコンパイルエラーになります。
まとめると、「result が真なら『正解!』、偽なら『不正解…』という文字列を作ってログに出す」1行です。
1. new[] { btnAnswer1, btnAnswer2, btnAnswer3, btnAnswer4 }
- これは「4つのボタンを配列にまとめた」ものです。例:
[0] = btnAnswer1
[1] = btnAnswer2
[2] = btnAnswer3
[3] = btnAnswer4
- 配列を作ることで、「ボタンをまとめて番号で扱える」ようになります。
2. Array.IndexOf(配列, 探す対象)
- C# 標準のメソッドで、「配列の中で探す対象が何番目にあるか」を返します。
- もし見つからなければ -1 を返します。
3. btn とは?
- クリックイベントの引数 object sender を Button にキャストしたものです。
Button btn = (Button)sender;
- つまり「今回押されたボタン」が btn に入っています。
4. 流れをまとめると
- 「押されたボタン」= btn を受け取る。
- 4つのボタンを配列にして並べる。
- その配列の中で btn が何番目かを探す。
- その番号(0〜3)を index に入れる。
5. 実行例
- もし btnAnswer1 が押されたら→ 配列の [0] にあるので index = 0
- もし btnAnswer3 が押されたら→ 配列の [2] にあるので index = 2
これで「何番目の選択肢が押されたか」がわかります。
その番号を AnswerChecker に渡して、「正解かどうか」を判定できる仕組みです。
✅ 初心者向けポイント
- 配列を使うと「複数のボタン」を一度にまとめられる
- IndexOf で「どのボタンが押されたか」を番号化できる → これが「選択肢番号」として使える
もし「配列にまとめるのがまだ難しい」という学習者向けには、
int index = -1; // 初期値(エラー検出用)
if (btn == btnAnswer1)
index = 0;
else if (btn == btnAnswer2)
index = 1;
else if (btn == btnAnswer3)
index = 2;
else if (btn == btnAnswer4)
index = 3;
のように書いても動きますが、配列 + IndexOf を使うとコードがシンプルで保守しやすくなります。
すべての回答ボタンの Click イベントを btnAnswer_Click に設定しておきましょう。
Step 5. GitHub Desktopでの運用ルール
- 全員が徹底すべき流れは 「Pull → 編集 → Commit → Push」
- 自分の担当クラス以外は触らない
- コミットメッセージ例:
- Add QuestionLoader.cs (担当A)
- Implement AnswerChecker.cs (担当B)
これでコンフリクトは最小限に抑えられます。
まとめ
- データは CSVファイル、コードは クラスごとに分担 → 競合を回避できる
- GitHub Desktopの最小運用ルールを守れば、初心者でも安全にチーム開発可能
- 各クラスが責務を持つため、オブジェクト指向の理解にもつながる
参考)このクイズの仕組み
「4人チームで作るWinFormsクイズアプリ(完全チュートリアル)」では、以下のようなクラス構成と役割分担になっています:
- QuestionLoader.cs(問題読み込み)
- AnswerChecker.cs(解答判定)
- ScoreManager.cs(スコア管理)
- UiUpdater.cs(UI更新)
- Form1.cs(統合とUIイベント処理)
関連する設計パターンや設計原則
1. 単一責任の原則 (Single Responsibility Principle, SRP)
各クラスがそれぞれ「問題読み込み」「解答判定」「スコア管理」「UI更新」という明確な責務に集中しており、SRP に基づいて設計されていると言えます。
2. ファサード (Facade)
Form1.cs が各担当クラスの機能をまとめて統合している様子から、複雑な内部構造を簡潔に扱うという点で ファサードパターン の考え方に似ています。
3. プレゼンター/モデルビュー・プレゼンター (MVP) パターン
Stack Overflow の情報によれば、WinForms アプリケーションでは、MVP(が例えば「Passive View」や「Supervising Controller」)が適用されることがあります。
今回の構成では厳密な MVP 構造ではないものの、「処理ロジック(QuestionLoader, AnswerChecker, ScoreManager)」と「UI更新(UiUpdater.Form1)」が明確に分離されており、MVP 的な分離の考え方に近しい設計とも見なせます。
まとめ
クラス構成 | 関連するパターン/原則 |
---|---|
QuestionLoader, AnswerChecker, ScoreManager, UiUpdater にそれぞれ責務が明確 | 単一責任原則 (SRP) に基づいた設計 |
Form1 で各担当者クラスをまとめ → 操作を簡潔に提供 | ファサードパターン (Facade) 的な役割 |
処理ロジックと UI 表示が別クラスで処理されている構造 | MVP もしくはロジックと UI 分離の設計 |
図1:クラス図(主要クラスと依存関係)
クイズアプリの主要クラスと依存関係を示す
(Question/QuestionLoader/AnswerChecker/ScoreManager/UiUpdater/Form1。UI部品名やCSVは記事定義に準拠)

- 目的:アプリケーションを構成する主要な「クラス」と、その間の関係を表す。
- 図の内容:
- Form1(UIの中心)から各クラスを呼び出して処理を組み立てている。
- QuestionLoader は問題を読み込む。
- AnswerChecker は答えを判定する。
- ScoreManager は正解数などのスコアを記録する。
- UiUpdater はラベルやボタンに表示を反映する。
- Question は1問分のデータ構造。
- ポイント:クラスごとの「責務の分担」が一目で分かる。
図2:シーケンス図(解答ボタン押下〜次問表示)
(Array.IndexOfで押下ボタンのインデックスを取得 → 判定 → スコア記録 → ログ表示 → 次の問題を取得して表示)

- 目的:時間の流れに沿って「誰が誰にメッセージを送るか」を表す。
- 図の内容:
- ユーザーがボタンを押す
- Form1が AnswerChecker に正誤判定を依頼
- ScoreManager に結果を記録
- UiUpdater で画面にログを表示
- QuestionLoader から次の問題を取得
- UiUpdater が新しい問題を表示
- ポイント:処理の流れ(イベントハンドラから次の問題表示まで)が分かりやすい。
図3:状態遷移図(クイズの進行サイクル)
(起動時に読込→表示→回答待ち→判定・記録→ログ→次問へループ)

- 目的:「アプリ全体の動作サイクル」を状態ごとに表す。
- 図の内容:
- 起動時は「初期化」状態
- 「問題表示」→「回答待ち」
- ボタン押下で「判定処理」
- 「スコア更新」→「ログ表示」
- 再び「問題表示」へ戻りループ
- ポイント:クイズアプリの進行フローを直感的に把握できる。
図4:パッケージ図(責務分割)
(UIとロジックの分離/データ読み込みの独立)
- UI構成(lblQuestion/btnAnswer1〜4/listBoxLog)やCSVの列構成は記事のStep 1・2に準拠しています。
- 各クラスのAPIはStep 4のコード断片を反映しています(GetRandomQuestion、CheckAnswer、Record、GetResult、ShowQuestion、LogResult、btnAnswer_Clickなど)。

- 目的:ソフト全体を「層ごと」に整理して、依存関係をシンプルに示す。
- 図の内容:
- UI層:ユーザーと直接やり取り(画面表示や操作の受け付け)
- ロジック層:答えの判定やスコア計算など「処理の中身」
- データ層:問題データの読み込みや保持
- ポイント:MVC的な責務分離が分かりやすくなる。
まとめ
- クラス図:どんな部品があるか
- シーケンス図:処理の流れ
- 状態遷移図:アプリ全体の進行サイクル
- パッケージ図:役割のまとまりと依存関係
ディスカッション
コメント一覧
まだ、コメントがありません