4人チームでクイズアプリを作る(Gitチーム開発発展)
Gitチーム開発シリーズ:なぜGitを使うのか|WinFormsで2人開発を体験する|コンフリクトを体験して解消する|ブランチを体験する|Pull Requestを体験する|よくある失敗とFAQ|4人チームでクイズアプリを作る(今ここ)|目次へ
前提:以下の記事を読んでいるとスムーズです。
- WinFormsで2人開発を体験する(Gitチーム開発入門)
- ブランチを体験する(Gitチーム開発)
- Pull Requestを体験する(Gitチーム開発)
- C# CSVファイルを読み込む方法(担当A が QuestionLoader を担当する場合)
はじめに
チーム開発の練習題材として最適なのが「クイズアプリ」です。
本記事では、GitHub Desktopを使った最小運用ルールと、4人での役割分担を取り入れながら、WinFormsでクイズアプリを完成させるチュートリアルを紹介します。
このチュートリアルを通じて、以下のスキルを習得できます:
- WinForms の基本操作(ボタンやラベルなどのUI部品の扱い)
- 複数クラスによる責務分担(問題管理・判定処理・スコア管理・UI制御の分離)
- チーム開発の基本プロセス(役割ごとの実装と統合、GitHub Desktopを使ったバージョン管理)
- 設計原則の基礎理解(単一責任の原則やUIとロジックの分離)
Step 1. プロジェクト準備
リーダーが行う作業
- 新規WinFormsアプリを作成
- プロジェクト名:TeamQuizApp
- ※2人開発(電話帳)と同じフレームワークを使用。.NET Framework を想定。.NET 6/7/8 の場合はテンプレート「Windows Forms App (.NET)」で作成。
- フォームに配置するUI
| コントロール | 名前 |
|---|---|
| Label | questionLabel |
| Button ×4 | answerButton1 ~ answerButton4 |
| ListBox | logListBox |
※名前は「意味+型」の順(例:questionLabel)で、型を接頭辞にする記法(lblQuestion など)は避けています。
- リポジトリ作成と共有
- GitHub Desktopでリポジトリを作成し、初期Push。(01-WinFormsで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を読んでしまう混乱が起きます。教材・演習では"確実に一致"が重要なので「常にコピー」を勧めています。
questions.csv のサンプル
question,choice1,choice2,choice3,choice4,correctIndex
日本の首都はどこ?,大阪,東京,名古屋,札幌,1
C#で文字列を扱う型は?,int,string,bool,char,1
Unityでシーン切り替えに使うメソッドは?,LoadScene,ChangeScene,OpenScene,StartScene,0
注意:選択肢にカンマを含めると Split(',') で正しく分割できません。現状のサンプルでは問題ありませんが、データ作成時はカンマを含めないようにしてください。
Step 3. チームでの役割分担
ブランチ命名
| ブランチ名 | 担当・内容 |
|---|---|
| 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 を作成、問題文・選択肢の表示や、ログ出力を担当 | 中 |
難易度の目安:担当B(低)→ 担当C・D(中)→ 担当A・リーダー(高)。理解度に応じて役割を振り分けるとスムーズです。
4人未満の場合:1人が2役を兼任できます。例:リーダー兼担当B、担当A、担当C、担当D の4人。または 担当B+担当C を1人、担当A、担当D、リーダー の3人チームなど。
Step 4. コード実装
クラス構成
- モデル:Question
- データ取得:QuestionLoader
- 判定:AnswerChecker
- スコア管理:ScoreManager
- 画面更新:UiUpdater
- 統括制御:Form1
QuestionLoader.cs(担当A)
Question クラス
- 役割:1問分のデータを保持するためのモデル。
- 内容:問題文 (Text)、選択肢 (Choices)、正解インデックス (CorrectIndex) をプロパティとして持つ。
- ポイント:クイズ問題をオブジェクトとして扱えるようになる。
QuestionLoader クラス
- 役割:CSVファイルから問題を読み込み、ランダムに1問を取り出す処理を提供。
- 内容:List に問題を保持し、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")
{
string csvPath = Path.Combine(AppContext.BaseDirectory, path);
foreach (var line in File.ReadAllLines(csvPath))
{
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];
}
}
}
パス指定について
上記コードでは Path.Combine(AppContext.BaseDirectory, path) により、実行ファイルの出力フォルダを起点にCSVを探します。これにより、Visual Studio から実行する場合や配布後の実行でも確実にファイルを見つけられます。
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;
}
}
}
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()
{
if (TotalCount == 0) return "正解数: 0 / 0 (正答率 0.0%)";
return $"正解数: {CorrectCount} / {TotalCount} (正答率 {((double)CorrectCount / TotalCount * 100):F1}%)";
}
}
}
UiUpdater.cs(担当D)
- 役割:画面表示の更新を担当。
- 内容:問題文をラベルに表示、ボタンに選択肢をセット、リストボックスにログを追加する処理を持つ。
- ポイント:UI更新をまとめて管理することで、Form本体のコードがシンプルになる。
using System.Windows.Forms;
namespace TeamQuizApp
{
public class UiUpdater
{
private readonly Label _questionLabel;
private readonly Button[] _buttons;
private readonly ListBox _log;
public UiUpdater(Label questionLabel, Button[] buttons, ListBox log)
{
_questionLabel = questionLabel;
_buttons = buttons;
_log = log;
}
public void ShowQuestion(Question q)
{
_questionLabel.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);
}
}
}
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(questionLabel,
new[] { answerButton1, answerButton2, answerButton3, answerButton4 },
logListBox);
LoadNextQuestion();
}
private void answerButton_Click(object sender, EventArgs e)
{
var btn = sender as Button;
int index = Array.IndexOf(new[] { answerButton1, answerButton2, answerButton3, answerButton4 }, 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);
}
}
}
重要:すべての回答ボタン(answerButton1~4)の Click イベントを answerButton_Click に設定しておきましょう。
Step 5. GitHub Desktopでの運用ルール
全員が徹底すべき流れは 「Pull → 編集 → Commit → Push」
- クラスファイルを追加したら、すべて保存(Ctrl+Shift+S)してから Commit する。プロジェクトファイル(.csproj)の更新も含めて保存される
- 自分の担当クラス以外は触らない
- コミットメッセージ例:
Add QuestionLoader.cs (担当A)Implement AnswerChecker.cs (担当B)
これでコンフリクトは最小限に抑えられます。
まとめ
- データは CSVファイル、コードは クラスごとに分担 → 競合を回避できる
- GitHub Desktopの最小運用ルールを守れば、初心者でも安全にチーム開発可能
- 各クラスが責務を持つため、オブジェクト指向の理解にもつながる
参考(設計パターン)
このクイズアプリの構成では、以下の設計原則やパターンが関連しています。
単一責任の原則 (SRP)
各クラスがそれぞれ「問題読み込み」「解答判定」「スコア管理」「UI更新」という明確な責務に集中しており、SRP に基づいて設計されていると言えます。
ファサード (Facade)
Form1.cs が各担当クラスの機能をまとめて統合している様子から、複雑な内部構造を簡潔に扱うという点で ファサードパターン の考え方に似ています。
ロジックとUIの分離
処理ロジック(QuestionLoader, AnswerChecker, ScoreManager)と UI更新(UiUpdater, Form1)が明確に分離されており、MVP 的な分離の考え方に近しい設計とも見なせます。


ディスカッション
コメント一覧
まだ、コメントがありません