4人で作る簡易レジアプリ|GitHub Desktopで初めてのチーム開発チュートリアル

広告

「自分のコードを、ほかの人のコードとどう合わせるの?」。この制作課題では、4人で1つのWinFormsアプリを作りながら、GitHub Desktopでブランチ → コミット → Push → プルリクエスト(PR)→ レビュー → マージ → Pullを体験します。リーダーも4人のうちの1人です。

題材はDataGridViewで作る簡易レジアプリです。元記事のステップ3「Productクラス+List」を出発点に、商品追加・合計・選択行削除・お釣り計算を分担します。今回はDataSourceを設定せず、Rows.Addで表示します。DataTableへの切り替えは完成後の発展課題にします。

この課題のゴールと進め方

  • 4人全員が、自分のブランチで変更し、PRを作成して、他の人のPRを1件以上レビューする。
  • 全員のPCで同じmainブランチを起動し、商品追加から会計まで確認する。
  • 「保存・Commit・Push・Pullの違い」と「担当ファイルを分ける理由」を自分の言葉で説明する。

目安は50分×6〜8コマです。初回はコードを写す時間を長めに取り、Git操作の区切りで全員の画面を確認してください。授業用の試作として、消費税・割引・実際の決済・売上保存は扱いません。アプリを終了するとカートは消えます。

作業全員で止まって確認すること
1環境準備・共通ひな形の共有全員がCloneしたアプリを起動できる
2担当紹介ファイルで最初のPR全員がPRとレビューを1回経験する
3〜44つの機能を別々のブランチで実装各担当のブランチでビルドが成功する
5〜6順番にマージ・動作確認全員のmainで同じ操作結果になる
余裕があれば小さな改善・コンフリクト練習履歴を残して修正できる

Step 0:4人の環境と役割をそろえる

  • 各自のWindows PCにVisual Studioの「.NET デスクトップ開発」を用意する。GitHubアカウントとGitHub Desktopも各自のものを使う。
  • 4人で同じ.NET SDKとターゲットフレームワークを選ぶ。授業環境で利用できるサポート中の.NET版を講師が指定する。.NET Framework版と混在させない。
  • C#のクラス・プロパティ・List・foreachと、WinFormsのButton/Label/DataGridViewの基本を先に確認する。
  • ソリューション名・プロジェクト名・名前空間は TeamRegisterApp にそろえる。既存のSimpleRegisterAppを4人それぞれで作り直さず、リーダーが用意したものをCloneする。
担当役割実装するファイルブランチレビュー担当
A(リーダー兼任)画面とイベントの接続Form1.cs・Form1.Designer.cs・Form1.resx(変更分)feature/register-uiB
B商品一覧ProductCatalog.csfeature/product-catalogC
Cカート・合計・削除CartService.csfeature/cart-serviceD
D預かり金の検証・お釣りPaymentService.csfeature/payment-serviceA

Product.csは全員共通の約束です。名前・単価・数量・小計の型や名前を勝手に変えません。変更が必要なら、先に4人で相談し、専用のPRでそろえます。フォーム関連のForm1.cs・Form1.Designer.cs・Form1.resxはAが担当します。画面を見ながらチームで相談し、デザイナーの操作はAだけが行います。Designer.csは手作業で編集せず、デザイナーが生成した変更を共有します。

図1|4人の部品を、1つのレジ画面につなぐ
B|商品一覧
☕ コーヒー・パン・おにぎり
ProductCatalog.cs
C|カート
🛒 追加・合計・削除
CartService.cs
D|会計
🧮 入力チェック・お釣り
PaymentService.cs
🖥 A|画面・イベント(リーダー兼任)
Form1.csが3つの部品を呼び、結果を表示する
共通の約束:Product.cs
商品名・単価・数量・小計
矢印は、各部品の結果を画面で使う関係です。Aも含めて合計4人で分担します。

Step 1:Gitの言葉を、今回の作業に置き換える

言葉今回の意味
リポジトリアプリのファイルと変更履歴を入れる場所
main4人で確認しながら完成に近づける共通のブランチ
ブランチmainから分かれて自分の担当作業を進める場所
保存Visual Studio上の編集内容をファイルに書き込む
Commit手元の変更を、説明付きで履歴に記録する
Push手元のコミットをGitHubへ送る
Fetch/PullGitHub側の更新を確認する/今いるブランチに取り込む
PR自分の変更をmainに入れてよいか、見てもらう依頼
マージブランチの変更を別のブランチに取り込む

Commitだけでは他の人には届きません。PushでGitHubへ送っても、mainはまだ変わりません。PRを確認してマージした後、各自がmainでPullすると、同じ成果を受け取れます。

Step 2:Aが共通のひな形を作る

2-1:Visual Studioで作成してからGitHub Desktopへ追加する

この準備はAが行います。B・C・DはStep 3でCloneします。

① Visual Studioでアプリを作る

  1. C#・.NET版の「Windows Forms アプリ」を選ぶ。
  2. プロジェクト名・ソリューション名を両方 TeamRegisterApp にする。「場所」には作業する親フォルダーを選び、チーム共通のターゲットフレームワークで作成する。
  3. F5で空のフォームが起動することを確認し、終了して「すべて保存」を行う。

補足:Git管理は次のGitHub Desktopで始めます。Visual StudioではGitリポジトリの作成やGitHubへの公開を行いません。

② GitHub DesktopでGit管理を始める

  1. エクスプローラーでソリューションファイル(TeamRegisterApp.sln または TeamRegisterApp.slnx)が入ったフォルダーを開き、そのフォルダーごとGitHub Desktopへドラッグ&ドロップする。
  2. 「Gitリポジトリではない」という案内から、create a repository here(この場所にリポジトリを作成)を選ぶ。
  3. Nameを TeamRegisterApp、Git ignoreを VisualStudio にし、READMEの作成を選択する。
  4. 作成先がソリューションファイルのある既存フォルダーそのものであることを確認し、Create repositoryを押す。

フォルダーの確認:ソリューションファイル自体や、内側のプロジェクトフォルダーだけを渡しません。Local pathとNameの組み合わせで、内側に別のTeamRegisterAppフォルダーを作らないようにします。ソリューションとプロジェクトを同じディレクトリに配置した場合も、ソリューションファイルがあるフォルダーを選びます。

既にGit管理されている場合:Add repositoryで追加します。リポジトリを重ねて作成する必要はありません。

③ 共有前に確認する

  • Repository → Show in Explorerで、ソリューションファイルがあるフォルダーが開く。
  • Current Branchmain になっている。違う名前なら、共有前にAがmainへ変更する。
  • ソリューション、.csproj、.cs、.resx、READMEなどがGit管理の対象になり、作業用の.vs/bin/obj/*.userが除外されている。

作業用ファイルがChangesに大量に出たら、Commitする前に.gitignoreとリポジトリの場所を確認してください。リポジトリ作成時に初期コミットができていても、次の共通ファイル追加後に改めてCommitします。

2-2:共通ファイルと、ビルドできる仮実装を追加する

フォームにはまだコントロールを配置しません。Form1.cs・Form1.Designer.cs・Form1.resx・Program.csはテンプレートのままにします。ソリューションエクスプローラーでプロジェクトを右クリック → 追加 → クラスを選び、以下の4ファイルを作ります。各ファイルはコード全体を置き換えてください。

Product.cs:全員共通のデータの形
namespace TeamRegisterApp
{
    public class Product
    {
        public string Name { get; set; } = "";
        public decimal Price { get; set; }
        public int Quantity { get; set; }
        public decimal Subtotal => Price * Quantity;
    }
}

金額にはdecimalを使い、今回は整数の円だけを扱います。Nameを空文字列で初期化しています。SubtotalはPriceとQuantityから計算するプロパティです。

ProductCatalog.cs:担当B用の仮実装
using System.Collections.Generic;

namespace TeamRegisterApp
{
    public class ProductCatalog
    {
        public List<Product> GetProducts()
        {
            // 担当Bが実装する
            return new List<Product>();
        }
    }
}
CartService.cs:担当C用の仮実装
using System.Collections.Generic;

namespace TeamRegisterApp
{
    public class CartService
    {
        public bool Add(string name, decimal price, int quantity)
        {
            return false; // 担当Cが実装する
        }
        public List<Product> GetItems()
        {
            return new List<Product>();
        }
        public decimal GetTotal()
        {
            return 0m;
        }
        public bool RemoveAt(int index)
        {
            return false;
        }
        public void Clear()
        {
        }
    }
}
PaymentService.cs:担当D用の仮実装
namespace TeamRegisterApp
{
    public class PaymentService
    {
        public bool TryGetChange(decimal total, string cashText,
            out decimal change, out string message)
        {
            change = 0m;
            message = "会計機能は準備中です。";
            return false; // 担当Dが実装する
        }
    }
}

outは、戻り値とは別に結果を呼び出し側へ渡す仕組みです。このメソッドは成功・失敗をboolで返し、お釣りとメッセージをoutで返します。

仮実装は、名前・引数・戻り値だけを先に決めた「差し込み口」です。まだ商品の追加や会計はできませんが、担当Aはこれらを呼ぶコードを先に書けます。この段階では、空のフォームが起動すれば成功です。

  1. Visual Studioで「すべて保存」(Ctrl+Shift+S)を行う。
  2. 「ビルド → ソリューションのビルド」を実行し、エラーがないことを確認する。F5で空のフォームを起動し、終了する。
  3. GitHub DesktopのChangesで追加ファイルを確認する。4つのクラスとプロジェクトがリポジトリ内に入っているかを見る。
  4. Summaryに 共通モデルと4人開発のひな形を追加 と入力し、Commit to mainを押す。
  5. Publish repositoryでGitHubへ作成する。授業ではKeep this code privateをオンにし、リポジトリ名を確認する。
  6. AはGitHubのリポジトリ設定からB・C・DをCollaboratorsとして招待し、3人は自分のアカウントで招待を承認する。詳しい操作は、GitHub Desktop と WinForms で始めるチーム開発 ~はじめてのステップ~「1-4:チームメンバーの招待(コラボレーターの追加)」を参照する。B・C・D全員が、自分のアカウントでリポジトリを開けることを確認してから、Step 3のCloneへ進む。組織リポジトリなら講師が書き込み権限を用意する。

参考:GitHub Desktopで最初のリポジトリを作成する(公式)。ボタン名はバージョンや表示言語で多少異なります。

図2|最初の共有は、Aから始める
① AのVisual Studio
ソリューション名:TeamRegisterApp
プロジェクト名:TeamRegisterApp
空のフォームを起動して確認
② AのGitHub Desktop
📁 ソリューションが入ったフォルダーをドラッグ&ドロップ
Git管理を開始 → 共通ひな形を追加 → Commit
③ GitHubへ共有
Publish repository → B・C・Dを招待
BのPC
Clone → 起動確認
CのPC
Clone → 起動確認
DのPC
Clone → 起動確認
B・C・DはAが共有したプロジェクトをCloneします。ソリューションファイルは.slnまたは.slnxです。

Step 3:B・C・DがCloneして起動する

  1. GitHub Desktopで File → Clone repository を開き、TeamRegisterAppを選ぶ。表示されなければ招待承認・ログインアカウントを確認する。
  2. 各自のPC内に保存先を指定してCloneする。ZIPダウンロードではなくCloneを使う。
  3. Repository → Show in Explorerからソリューションファイルを開く。Visual Studioでビルドして空のフォームを起動する。
  4. 4人でCurrent Branchがmainであること、同じ初期コミットをHistoryで確認できることを確かめる。

ここで全員が起動できるまで次へ進みません。ターゲットフレームワークがないと言われたら、勝手に.csprojを書き換えず、講師とSDK・Visual Studioの環境をそろえます。

図3|自分の変更が、全員に届くまで
① 自分のPC
最新main → 作業ブランチを作成
編集 → 保存 → ビルド → Commit
Push(初回はPublish branch)
② GitHub上の作業ブランチ
PRを作成 → 他の人がレビュー
必要なら同じブランチで修正・Commit・Push
レビュー後にマージ
③ GitHub上のmain
確認した変更が共通ブランチに入る
④ 全員のPC
mainへ切り替え → Fetch → Pull
同じ変更を受け取り、ビルド・動作確認
Commitは手元の履歴、PushはGitHubへの送信です。全員がmainでPullするところまでが1回の共有です。

Step 4:コードを書く前に、全員で最初のPRを体験する

最初の1回は、担当紹介の短いファイルを追加します。A → B → C → Dの順で実施し、残りの3人は操作を見ます。以下はBの例です。A・C・Dは文字を自分の担当に置き換えてください。

  1. Visual Studioのデバッグを停止し、すべて保存する。GitHub DesktopのChangesが空であることを確認する。
  2. Current Branchでmainを選び、Fetch originを押す。Pull originが表示されたら押す。
  3. Current Branch → New Branchpractice/member-b を作る。分岐元がmainであることを確認する。
  4. Repository → Show in Explorerを開き、リポジトリ直下に members フォルダーと b.md を作る。Windowsで拡張子を表示し、b.md.txtにならないよう注意する。
  5. 内容を「担当:B」「実装:商品一覧」「確認したいこと:3商品が選択できる」の3行にして保存する。実名やメールアドレスは不要。
  6. Changesでmembers/b.mdだけが追加されたことを確認し、Summaryに Bの担当紹介を追加 と入力してCommitする。
  7. 初回はPublish branch、以後はPush originで送る。
  8. Preview Pull Requestを開き、base: main、compare: practice/member-b を確認する。Create Pull Requestからブラウザーへ進み、タイトルと確認内容を入力して作成する。
  9. CがFiles changedを読み、誤りがなければReview changesからApproveする。B本人のApproveで代用しない。
  10. Aがレビュー結果を見てMerge pull request → Confirm mergeでmainへ取り込む。A自身のPRもBに見てもらう。
  11. 全員がmainへ戻り、Fetch origin → Pull origin。members/b.mdが自分のPCにも現れたら成功。

練習用ブランチはPRがMergedになった後に削除できます。Delete branchは作業をmainへ取り込んだことを確認してから使います。次の作業は、更新したmainから新しいブランチを作ります。

参考:GitHub DesktopからPRを作成する(公式)

Step 5:実装を始める前に、部品の約束を読む

部品公開するメソッド呼び出す側が期待すること
商品一覧GetProducts()商品名・単価を持つList<Product>を返す
カートAdd(name, price, quantity)追加できたらtrue。1回の追加で1行増える
カートGetItems()/GetTotal()表示順の一覧/小計の合計を返す
カートRemoveAt(index)/Clear()指定位置の削除/全行の消去
会計TryGetChange(total, cashText, out change, out message)成功ならtrueとお釣り、失敗ならfalseと理由を返す
画面Form1のイベント部品を呼び、結果をDataGridViewやLabelへ表示する

4人とも最新のmainから、役割表にあるfeatureブランチを作ります。B・C・Dは担当クラス1つを置き換え、AはデザイナーとForm1.csで画面を作るのが今回の実装単位です。以下は完成例です。先に役割とメソッドの入出力を読み、担当部分を自分の言葉で説明してから入力してください。

Step 6:担当B ― 商品一覧を作る

feature/product-catalogでProductCatalog.csを置き換えます。CSVはまだ使わず、3商品をコードで用意します。

ProductCatalog.cs:コードと解説を見る
using System.Collections.Generic;

namespace TeamRegisterApp
{
    public class ProductCatalog
    {
        public List<Product> GetProducts()
        {
            return new List<Product>
            {
                new Product { Name = "コーヒー", Price = 150m, Quantity = 1 },
                new Product { Name = "パン", Price = 180m, Quantity = 1 },
                new Product { Name = "おにぎり", Price = 130m, Quantity = 1 }
            };
        }
    }
}

GetProductsは商品候補の一覧を作って返します。カートに入っている商品とは別です。150mのmはdecimal型の数値であることを表します。

Bの確認:商品はコーヒー150円、パン180円、おにぎり130円の3件です。ビルドを確認して 商品一覧に3商品を実装 でCommit・Pushし、PRを作ります。画面担当がまだmainに入っていないときは、PRに「単体ブランチのビルド確認済み。画面での商品選択は統合後に確認」と記載します。

Step 7:担当C ― カート・合計・削除を作る

feature/cart-serviceでCartService.csを置き換えます。同じ商品を2回追加したら、別々の2行として記録します。今回は同じ商品の行をまとめません。

CartService.cs:コードと解説を見る
using System.Collections.Generic;

namespace TeamRegisterApp
{
    public class CartService
    {
        private readonly List<Product> items = new List<Product>();

        public bool Add(string name, decimal price, int quantity)
        {
            if (string.IsNullOrWhiteSpace(name) || price <= 0m ||
                price > 100000m || price != decimal.Truncate(price) ||
                quantity < 1 || quantity > 99 || items.Count >= 100)
            {
                return false;
            }

            items.Add(new Product
            {
                Name = name,
                Price = price,
                Quantity = quantity
            });
            return true;
        }

        public List<Product> GetItems()
        {
            return new List<Product>(items);
        }

        public decimal GetTotal()
        {
            decimal total = 0m;
            foreach (Product item in items)
            {
                total += item.Subtotal;
            }
            return total;
        }

        public bool RemoveAt(int index)
        {
            if (index < 0 || index >= items.Count)
            {
                return false;
            }
            items.RemoveAt(index);
            return true;
        }

        public void Clear()
        {
            items.Clear();
        }
    }
}

画面ではなくitemsがカートの本体です。Addでは商品一覧のオブジェクトを使い回さず、購入数量を持つ別のProductを作ります。単価は1〜100,000円の整数、数量は1〜99、最大100行とします。GetItemsはリストの入れ物をコピーして返しますが、Product自体の複製ではありません。この課題の画面側は読み取りだけに使います。

Cの確認:150円×2と180円×1で合計480円。0番目の行を削除すると180円。Clear後は0円。負の位置や範囲外の削除はfalseになります。ビルド後、カートの追加・合計・削除を実装 でCommit・Pushします。

Step 8:担当D ― 預かり金とお釣りを計算する

feature/payment-serviceでPaymentService.csを置き換えます。預かり金は「1000」のように半角数字だけを入力する約束です。空欄、文字、負数、小数点、カンマ、空白は受け付けません。

PaymentService.cs:コードと解説を見る
using System.Globalization;

namespace TeamRegisterApp
{
    public class PaymentService
    {
        public bool TryGetChange(decimal total, string cashText,
            out decimal change, out string message)
        {
            change = 0m;
            message = "";

            if (total <= 0m)
            {
                message = "商品を追加してください。";
                return false;
            }

            decimal cash;
            if (!decimal.TryParse(cashText, NumberStyles.None,
                CultureInfo.InvariantCulture, out cash))
            {
                message = "預かり金は半角数字だけで入力してください。";
                return false;
            }

            if (cash < total)
            {
                message = "預かり金が不足しています。";
                return false;
            }

            change = cash - total;
            message = "会計できました。";
            return true;
        }
    }
}

TryParseは入力を数値に変換できたかどうかを返します。NumberStyles.Noneで余分な記号を許可しません。商品がない場合、数字にできない場合、金額が不足する場合を先に確認してから、お釣りを計算します。

Dの確認:合計870円なら1000円でお釣り130円、870円なら0円、800円なら不足です。合計0円では会計できません。ビルド後、預かり金の検証とお釣り計算を実装 でCommit・Pushします。

Step 9:担当A ― デザイナーで画面を作り、3つの部品につなぐ

この教材では、実行前に画面の配置を視覚的に確認しながら作れるデザイナーを使います。位置・大きさ・文字の収まり・余白を見ながら調整でき、チームでも「合計を目立たせたい」「ボタンをここに置きたい」と完成イメージを共有できます。実行時の表示も最後に確認しましょう。

Aはfeature/register-uiで作業します。B・C・Dは担当クラスを編集し、フォーム関連のファイルはAだけが編集します。Aの変更対象はForm1.csと、デザイナー操作で変更されるForm1.Designer.cs・Form1.resxです。Designer.csは自動生成されるため、手作業で書き換えません。

9-1:ツールボックスから部品を配置する

ソリューションエクスプローラーでForm1.csを右クリックし「デザイナーの表示」を選びます。「表示 → ツールボックス」から次の部品をドラッグ&ドロップしてください。部品を選んでF4を押し、プロパティ画面の(Name)を表の名前に変更します。Textは画面に見せる文字、(Name)はコードから使う名前です。

種類(Name)Text/用途
ComboBoxproductComboBox商品選択
LabelquantityLabel数量
NumericUpDownquantityNumericUpDown数量入力
ButtonaddButton商品を追加
DataGridViewcartDataGridView明細一覧
ButtonremoveButton選択行を削除
ButtonclearButton次のお客様/全消去
LabeltotalLabel合計:0円
LabelcashLabel預かり金(円)
TextBoxcashTextBoxTextを空にする
ButtoncheckoutButton会計
LabelchangeLabelお釣り:未計算
画面配置のイメージ(実際のスクリーンショットではありません)
商品を選択 ▼  数量[1] [商品を追加]
商品名 | 単価(円)| 数量 | 小計(円)
ここに購入する商品を表示
[選択行を削除] [次のお客様/全消去] 合計:0円
預かり金(円)[   ] [会計] お釣り:未計算
上から商品選択、明細、削除・合計、会計の順に配置します。文字の収まりを見ながら幅や余白を調整してください。

9-2:プロパティを設定する

対象設定
Form1Text:4人チームの簡易レジ、ClientSize:800, 540を目安に調整、FormBorderStyle:FixedSingle、MaximizeBox:False
productComboBoxDropDownStyle:DropDownList。Itemsは空のまま(実行時に商品一覧から追加)
quantityNumericUpDownMinimum:1、Maximum:99、Value:1
cartDataGridViewAllowUserToAddRows:False、AllowUserToDeleteRows:False、ReadOnly:True、MultiSelect:False
cartDataGridView(続き)SelectionMode:FullRowSelect、AutoSizeColumnsMode:Fill、RowHeadersVisible:False、DataSource:未設定
各LabelAutoSize:Trueを基本に、文字と隣の部品が重ならないよう調整
clearButton「次のお客様/全消去」が収まる幅に広げる

DataGridViewを選択し、プロパティのColumnsの「…」から列の編集画面を開きます。バインドされていないDataGridViewTextBoxColumnを、次の順で4列追加します。(Name)は列の識別名、HeaderTextは見出しです。全列のSortModeをNotSortableにしてください。

(Name)HeaderTextDefaultCellStyleのFormat
ProductName商品名空欄
Price単価(円)N0
Quantity数量空欄
Subtotal小計(円)N0

DefaultCellStyleの「…」からFormatを設定できます。並べ替えを無効にするのは、画面の行番号とカートの位置を一致させて削除するためです。列はデザイナーだけで作成し、Columns.Addをコードへ追加しません。

9-3:Form1.csに処理を書き、イベントを接続する

まずデザイナーでの変更をすべて保存し、コード表示へ切り替えてForm1.csだけを以下の内容に置き換えます。Form1.Designer.cs・Form1.resx・Program.csは置き換えません。InitializeComponentがデザイナーで配置した部品を作るので、同じコントロールをnewしたりBuildScreenを追加したりする必要はありません。

Form1.cs:デザイナー方式の処理コードを見る
using System;
using System.Collections.Generic;
using System.Windows.Forms;

namespace TeamRegisterApp
{
    public partial class Form1 : Form
    {
        private readonly ProductCatalog catalog = new ProductCatalog();
        private readonly CartService cart = new CartService();
        private readonly PaymentService payment = new PaymentService();
        private List<Product> products = new List<Product>();

        public Form1()
        {
            InitializeComponent();
            products = catalog.GetProducts();
            foreach (Product product in products)
            {
                productComboBox.Items.Add(product.Name);
            }
            if (products.Count > 0)
            {
                productComboBox.SelectedIndex = 0;
            }
            RefreshCart();
        }

        private void AddButton_Click(object? sender, EventArgs e)
        {
            int index = productComboBox.SelectedIndex;
            if (index < 0 || index >= products.Count)
            {
                MessageBox.Show("商品を選択してください。");
                return;
            }
            Product product = products[index];
            int quantity = (int)quantityNumericUpDown.Value;
            if (!cart.Add(product.Name, product.Price, quantity))
            {
                MessageBox.Show("追加できません。数量や100行の上限を確認してください。");
                return;
            }
            RefreshCart();
        }

        private void RemoveButton_Click(object? sender, EventArgs e)
        {
            if (cartDataGridView.SelectedRows.Count == 0)
            {
                MessageBox.Show("削除する行を選択してください。");
                return;
            }
            int index = cartDataGridView.SelectedRows[0].Index;
            if (cart.RemoveAt(index))
            {
                RefreshCart();
            }
        }

        private void ClearButton_Click(object? sender, EventArgs e)
        {
            cart.Clear();
            RefreshCart();
        }

        private void CheckoutButton_Click(object? sender, EventArgs e)
        {
            decimal change;
            string message;
            if (payment.TryGetChange(cart.GetTotal(), cashTextBox.Text,
                out change, out message))
            {
                changeLabel.Text = $"お釣り:{change:N0}円";
            }
            else
            {
                changeLabel.Text = "お釣り:未計算";
                MessageBox.Show(message);
            }
        }

        private void CashTextBox_TextChanged(object? sender, EventArgs e)
        {
            changeLabel.Text = "お釣り:未計算";
        }

        private void RefreshCart()
        {
            cartDataGridView.Rows.Clear();
            foreach (Product item in cart.GetItems())
            {
                cartDataGridView.Rows.Add(item.Name, item.Price,
                    item.Quantity, item.Subtotal);
            }
            cartDataGridView.ClearSelection();
            totalLabel.Text = $"合計:{cart.GetTotal():N0}円";
            cashTextBox.Clear();
            changeLabel.Text = "お釣り:未計算";
        }
    }
}

RefreshCartはCartServiceから一覧を受け取り、Rows.Clear → Rows.Addで表示を更新します。画面の配置・列の作成はデザイナー、商品の操作と部品の呼び出しはこのコードが担当します。

保存したらデザイナーへ戻り、各部品を選択します。プロパティ画面の雷マーク(イベント)を開き、表のイベント欄で右側のドロップダウンから既存のメソッドを選びます。空のイベントメソッドを新しく作るためのダブルクリックは不要です。

部品イベント選択するメソッド
addButtonClickAddButton_Click
removeButtonClickRemoveButton_Click
clearButtonClickClearButton_Click
checkoutButtonClickCheckoutButton_Click
cashTextBoxTextChangedCashTextBox_TextChanged

イベント登録はこのプロパティ操作に統一します。Form1.csにClick +=などを重ねて書くと処理が2回実行されます。候補が出ない場合は、Form1.csを保存したか、部品名とメソッド名が一致しているか確認してください。

カートの内容や預かり金を変更すると、前のお釣りを「未計算」に戻します。会計後も明細を残し、「次のお客様/全消去」で次へ進みます。

Aの確認:ビルドして、デザイナーと実行画面の文字・配置を見比べます。仮実装だけの場合は商品候補が空、追加や会計が未完成でも構いません。B・C・Dを統合した後に全操作を確認します。ChangesでForm1.csと変更されたForm1.Designer.cs・Form1.resxを確認し、デザイナーでレジ画面とイベントを実装でCommit・Pushします。変更のないファイルを無理に含める必要はありません。

Step 10:PRを出して、レビューして、順番に取り込む

各担当は「すべて保存 → ビルド → Changesで差分確認 → Commit → Push → PR」を行います。PRのbaseはmain、compareは自分のfeatureブランチです。AのPRにはデザイナー操作によるForm1.Designer.cs・Form1.resxの変更も含めます。B・C・DのPRにフォーム関連ファイルなど担当外の変更が混ざっていたら、内容を確認してから進めます。

PRの説明には次の4点を書きます。まだ確認できない項目は、未確認と明記してください。

  • 変更内容:何ができるようになったか
  • 変更ファイル:自分が変更した担当ファイル名(Aはデザイナー生成分も記載)
  • 確認結果:ビルドと、実施した確認の結果
  • 統合後に確認すること:他担当が必要な操作

レビューの組み合わせはAのPRをB、BをC、CをD、DをAです。Files changedで担当範囲とメソッドの約束を確認し、疑問はコメントします。修正が必要なら作成者が同じブランチで直してCommit・Pushします。既存のPRに追加されるので、修正のたびにPRを作り直しません。

先に別のPRがマージされたら

  1. 自分の変更をすべて保存・Commitし、Pushする。Changesが空か確認する。
  2. mainへ切り替え、Fetch origin → Pull originで最新にする。
  3. 自分のfeatureブランチへ戻る。
  4. Current Branchの Choose a branch to merge into… からmainを選ぶ。mainを自分のfeatureへ取り込む方向を確認してマージする。
  5. Visual Studioに再読み込みを求められたら読み込み直し、ビルド・必要な操作確認を行う。Push originでPRも更新する。

featureブランチでPullするだけでは、mainの更新は入りません。同じ名前のリモートブランチとの同期と、mainからの取り込みは別の操作です。今回はマージでそろえ、RebaseやForce pushは使いません。参考:ブランチの同期と他ブランチの取り込み(公式)

おすすめのマージ順と、その時点の成功条件

順番mainに入れるもの確認すること
1B:商品一覧ビルド成功。まだ画面は空でよい
2C:カートBを取り込んでビルド成功。画面操作はまだ待つ
3D:会計B・Cを取り込んでビルド成功
4A:画面最新mainを取り込み、全操作を確認してからマージ

各PRは指定した人のレビュー後にAが1件ずつマージします。AのPRもBがレビューします。途中でmainが更新されたら、取り込み後の変更についてもう一度確認します。mainへ直接コードを追加するのは初期ひな形までとし、それ以降の修正も新しいブランチとPRで進めます。

Step 11:全員のmainで完成確認する

  1. デバッグを停止し、手元のChangesを空にする。
  2. mainに切り替えてFetch origin → Pull originを実行する。
  3. 同じ最新コミットであることをHistoryで確認し、Visual Studioでビルド・起動する。
  4. 次の表を各自で確認する。成功した人の画面を見ただけで終わりにしない。
操作期待する結果
起動直後3商品を選べる。明細0行、合計0円
コーヒーを数量2で追加1行、小計300円、合計300円
パン1個、おにぎり3個を追加3行。小計は300円・180円・390円、合計870円
預かり金1000で会計お釣り130円
預かり金870で会計お釣り0円
預かり金800で会計不足を表示し、お釣りは未計算
空欄、abc、-1、1.5、1,000で会計半角数字で入力する案内が出る
会計後に預かり金を書き換える前のお釣りが消え、未計算になる
パンの行を選んで削除2行、合計690円。預かり金が空になり、お釣りは未計算
未選択のまま削除行を選ぶ案内が出て、明細は変わらない
同じ商品を2回追加2回分が別々の行として追加される
次のお客様/全消去明細0行、合計0円、預かり金は空
空のカートで会計商品を追加する案内が出る

単価・数量・小計を画面から直接書き換えられないこと、見出しをクリックしても並べ替わらないことも確認します。小計はProductで計算し、合計はCartServiceで計算します。

困ったときの確認表

症状確認と対処
Pushできない招待を承認したか、自分のアカウントか、書き込み権限があるか確認する
他の人の変更が見えないPRはMergedか。自分はmainか。Fetch後にPullしたか確認する
ブランチ変更時に未コミット変更の案内が出るキャンセルして今のブランチを確認し、作業内容を保存・Commitしてから切り替える
型やメソッドが見つからない名前空間TeamRegisterApp、ファイル追加先、メソッド名と引数、共通ひな形の取り込みを確認する
画面が二重になる/処理が2回走る部品・列はデザイナー、イベント接続はプロパティ画面で行う。BuildScreen、手書きのコントロール生成、Columns.Add、Click +=を本編に混ぜていないか確認する
列が重複する/Rows.Addでエラーこの課題はDataSource未設定。元記事のDataTable用コードを混ぜていないか確認する
コンフリクトが出た担当外のファイルを編集していないか、同じファイルを作り直していないか確認する。相手と内容を読み合わせて解消する
binやobjが大量に出るリポジトリの保存場所と.gitignoreを確認する。追跡済みのファイルは.gitignore追加だけでは追跡解除されないので、講師に相談する

コンフリクトは「どちらの変更を残すかGitだけでは決められない」状態です。GitHub Desktopから該当ファイルをエディターで開き、相手と必要な内容を相談します。<<<<<<<=======>>>>>>>を含む競合部分を正しい内容に直し、保存・ビルドしてからマージを完了し、Pushします。Designerや.csprojで判断できない場合は、マージを中止して講師と確認してください。

提出物と振り返り

  • チーム:GitHubリポジトリURL、完成したmainのコミット、動作確認表、実行画面の画像。非公開リポジトリの場合は講師が閲覧できるようにする。
  • 個人:練習PRと機能PRのURL、自分がレビューしたPRのURL、担当クラスの役割を3文で説明したもの。
  • 振り返り:「保存・Commit・Pushの違い」「featureでPullしてもmainの更新が入らない理由」「Form1を1人の担当にした理由」を話し合う。

評価は画面が動くことだけでなく、全員の履歴・レビュー・動作確認が残っていることを重視します。作業を1人に集めず、分からない操作は画面を見せ合って説明しながら進めましょう。

完成後の小さな発展課題

  • 商品を1つ追加する:B以外の人が商品一覧の担当を体験し、専用PRで変更する。
  • 表示文言を改善する:合計ラベルの文字を大きくするなど、デザイナーで小さな画面変更を行いPRをもう一度回す。
  • DataTableへ切り替える:元記事のステップ4を参考に、列の初期化とDataSourceの扱いを1つのPRで変更する。Rows.Add方式と混ぜない。
  • コンフリクトを体験する:講師と練習用のテキストファイルを用意し、2本のブランチで同じ行を変更する。最初はアプリ本体のDesignerで練習しない。

参考資料:デザイナーを使わず、コードで画面を作る方法

ここからは本編完成後に読む補足です。本編の制作手順はデザイナー方式で統一してください。デザイナーも内部では、コントロールを作成・配置するコードを生成しています。自分でnew Button()やControls.AddRange()を書いて画面を作る方法もあります。

コード方式には、部品名・設定・イベント登録をまとめて再現できる、Gitの差分で変更を追いやすい、画面が組み立てられる仕組みを学べる、といった利点があります。商品数に応じてボタンを増やすような動的な画面にもつながります。一方、本編では位置・大きさ・文字の収まりを視覚的に確認し、チームで相談できるデザイナーの利点を優先します。

次の例は本編へ追加するコードではなく、画面作成方法を置き換える別方式です。試す場合はチームのリポジトリとは別の保存場所に、新しいWinFormsプロジェクトをソリューション名・プロジェクト名ともTeamRegisterAppで作ります。フォームは空のままにし、完成したProduct.cs・ProductCatalog.cs・CartService.cs・PaymentService.csを追加したうえで、Form1.csだけを以下に置き換えてください。新しいプロジェクトのテンプレートが作ったForm1.Designer.cs・Form1.resx・Program.csはそのまま使い、デザイナーで部品やイベントを追加しません。

参考:コードだけで画面を作るForm1.cs(本編と混在させない)
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace TeamRegisterApp
{
    public partial class Form1 : Form
    {
        private readonly ProductCatalog catalog = new ProductCatalog();
        private readonly CartService cart = new CartService();
        private readonly PaymentService payment = new PaymentService();
        private List<Product> products = new List<Product>();

        private readonly ComboBox productComboBox = new ComboBox();
        private readonly NumericUpDown quantityNumericUpDown = new NumericUpDown();
        private readonly Button addButton = new Button();
        private readonly DataGridView cartDataGridView = new DataGridView();
        private readonly Button removeButton = new Button();
        private readonly Button clearButton = new Button();
        private readonly Label totalLabel = new Label();
        private readonly TextBox cashTextBox = new TextBox();
        private readonly Button checkoutButton = new Button();
        private readonly Label changeLabel = new Label();

        public Form1()
        {
            InitializeComponent();
            BuildScreen();
            products = catalog.GetProducts();
            foreach (Product product in products)
            {
                productComboBox.Items.Add(product.Name);
            }
            if (products.Count > 0)
            {
                productComboBox.SelectedIndex = 0;
            }
            RefreshCart();
        }

        private void BuildScreen()
        {
            Text = "4人チームの簡易レジ";
            ClientSize = new Size(800, 540);
            FormBorderStyle = FormBorderStyle.FixedSingle;
            MaximizeBox = false;

            productComboBox.SetBounds(20, 20, 230, 30);
            productComboBox.DropDownStyle = ComboBoxStyle.DropDownList;
            quantityNumericUpDown.SetBounds(270, 20, 80, 30);
            quantityNumericUpDown.Minimum = 1;
            quantityNumericUpDown.Maximum = 99;
            quantityNumericUpDown.Value = 1;
            addButton.SetBounds(370, 20, 130, 30);
            addButton.Text = "商品を追加";

            cartDataGridView.SetBounds(20, 70, 760, 300);
            cartDataGridView.AllowUserToAddRows = false;
            cartDataGridView.AllowUserToDeleteRows = false;
            cartDataGridView.ReadOnly = true;
            cartDataGridView.MultiSelect = false;
            cartDataGridView.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
            cartDataGridView.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
            cartDataGridView.RowHeadersVisible = false;
            cartDataGridView.Columns.Add("ProductName", "商品名");
            cartDataGridView.Columns.Add("Price", "単価(円)");
            cartDataGridView.Columns.Add("Quantity", "数量");
            cartDataGridView.Columns.Add("Subtotal", "小計(円)");
            foreach (DataGridViewColumn column in cartDataGridView.Columns)
            {
                column.SortMode = DataGridViewColumnSortMode.NotSortable;
            }
            cartDataGridView.Columns["Price"]!.DefaultCellStyle.Format = "N0";
            cartDataGridView.Columns["Subtotal"]!.DefaultCellStyle.Format = "N0";

            removeButton.SetBounds(20, 390, 130, 30);
            removeButton.Text = "選択行を削除";
            clearButton.SetBounds(170, 390, 140, 30);
            clearButton.Text = "次のお客様/全消去";
            totalLabel.SetBounds(340, 390, 400, 30);
            Label cashLabel = new Label();
            cashLabel.SetBounds(20, 450, 120, 25);
            cashLabel.Text = "預かり金(円)";
            cashTextBox.SetBounds(150, 450, 160, 30);
            checkoutButton.SetBounds(330, 450, 100, 30);
            checkoutButton.Text = "会計";
            changeLabel.SetBounds(460, 450, 320, 50);

            Controls.AddRange(new Control[]
            {
                productComboBox, quantityNumericUpDown, addButton,
                cartDataGridView, removeButton, clearButton, totalLabel,
                cashLabel, cashTextBox, checkoutButton, changeLabel
            });
            addButton.Click += AddButton_Click;
            removeButton.Click += RemoveButton_Click;
            clearButton.Click += ClearButton_Click;
            checkoutButton.Click += CheckoutButton_Click;
            cashTextBox.TextChanged += CashTextBox_TextChanged;
        }

        private void AddButton_Click(object? sender, EventArgs e)
        {
            int index = productComboBox.SelectedIndex;
            if (index < 0 || index >= products.Count)
            {
                MessageBox.Show("商品を選択してください。");
                return;
            }
            Product product = products[index];
            int quantity = (int)quantityNumericUpDown.Value;
            if (!cart.Add(product.Name, product.Price, quantity))
            {
                MessageBox.Show("追加できません。数量や100行の上限を確認してください。");
                return;
            }
            RefreshCart();
        }

        private void RemoveButton_Click(object? sender, EventArgs e)
        {
            if (cartDataGridView.SelectedRows.Count == 0)
            {
                MessageBox.Show("削除する行を選択してください。");
                return;
            }
            int index = cartDataGridView.SelectedRows[0].Index;
            if (cart.RemoveAt(index))
            {
                RefreshCart();
            }
        }

        private void ClearButton_Click(object? sender, EventArgs e)
        {
            cart.Clear();
            RefreshCart();
        }

        private void CheckoutButton_Click(object? sender, EventArgs e)
        {
            decimal change;
            string message;
            if (payment.TryGetChange(cart.GetTotal(), cashTextBox.Text,
                out change, out message))
            {
                changeLabel.Text = $"お釣り:{change:N0}円";
            }
            else
            {
                changeLabel.Text = "お釣り:未計算";
                MessageBox.Show(message);
            }
        }

        private void CashTextBox_TextChanged(object? sender, EventArgs e)
        {
            changeLabel.Text = "お釣り:未計算";
        }

        private void RefreshCart()
        {
            cartDataGridView.Rows.Clear();
            foreach (Product item in cart.GetItems())
            {
                cartDataGridView.Rows.Add(item.Name, item.Price,
                    item.Quantity, item.Subtotal);
            }
            cartDataGridView.ClearSelection();
            totalLabel.Text = $"合計:{cart.GetTotal():N0}円";
            cashTextBox.Clear();
            changeLabel.Text = "お釣り:未計算";
        }
    }
}

BuildScreenで部品の配置・列の作成・イベント登録を行います。本編ではこれらをデザイナーで設定しました。参考例は本編の画面と完全に同じ配置を保証するものではありません。

参考にした記事

訪問数 21 回, 今日の訪問数 21回

広告