WinFormsアプリで学ぶオブジェクト指向アプリの作り方(具体版)

2025年2月26日

この資料では、新しくアプリケーションを作成する手順と、オブジェクト指向の考え方を用いてプログラムを組み立てる方法について解説します。従来の教科書では、C#の文法やクラスの作り方、犬や車といったオブジェクトの例が紹介されることが多いですが、実際に自分が作りたいものをゼロから組み立てる方法や考え方はあまり説明されていません。ここでは、その手順やコツを分かりやすく紹介します。

おさらい

手続型の場合

手続型では、コードは上から順に実行されるように記述します。たとえば、次のようなコードでは、単に変数を宣言して処理を行います。

int player1Hp = 10;
int player2Hp = 20;

player1Hp--;

Console.WriteLine(player1Hp); // 出力: 9

オブジェクト指向の場合

オブジェクト指向では、実際に扱う「モノ(オブジェクト)」に注目します。以下は、プレイヤークラスを用いた例です。

Player player1 = new Player();
player1.Hp = 10;
Player player2 = new Player();

player1.Hp--;

Console.WriteLine(player1.Hp); // 出力: 9

class Player
{
    public int Hp { get; set; }
}

オブジェクト指向では、各クラスはその役割に沿った処理だけを持つように設計し、全体の動きを整理しやすくなります。

オブジェクト指向アプローチ

取り組み方

知識の暗記ではなく、実践的な作業を重ねる
C#の文法を覚えるだけではなく、実際に作りたいものに合わせてどのようにコードを組み立てるかが大切です。

作りたいものをまず決める
大手開発会社が手がけるような大規模なものではなく、シンプルな機能から始め、徐々に拡張していくのがポイントです。

具体例:AppleCatchFormの作成

ゲームの概要

  • ゲーム名: アップルキャッチ
  • ゲーム内容:
    落ちてくるリンゴをバスケットで受け止め、受け止めたリンゴの数が多いほど高得点に。
    ただし、爆弾リンゴを受け取ると得点が減少します。
  • 操作方法:
    左右の矢印キーでバスケットを操作し、制限時間内にできるだけ多くリンゴを受け止めます。ウィンドウを閉じるとゲーム終了。

ゲーム開発のための基本要素

  1. 名詞抽出
    ゲームに必要な要素をリストアップします。
    例:赤いリンゴ、爆弾リンゴ、バスケット、背景、得点、残り時間など
  2. 動詞句の抽出
    名詞がどう動作するかを考え、処理(メソッド)を定義します。
    例:落ちる、横に動く、得点を更新する、時間を減らす、ゲームオーバー判定
  3. 変数とメソッドの定義
    必要なクラスや変数、そしてメソッドを順次作成していきます。

やってはいけないのは、いきなりエディタを起動してコードを書き始めることです

名詞抽出法

ストーリー(仕様)をまとめる

上記のゲーム内容を掘り下げます
画面の構想をイラストに描いて、そこからイメージされることをまとめてもいいでしょう

細かく書き直す

落ちてくる赤いリンゴを、横に動くバスケットで受け止める
受け止めた赤いりんごの数が多いほど高得点になる
ただし、爆弾リンゴをとってしまうと得点が減る
制限時間内がある(時間経過するとゲームオーバーになる)
場面の背景は山
得点と残り時間が上に表示される

名詞と名詞句を考える

ひたすら、名詞(バスケット)や名詞句(赤い+リンゴ)を書き出します

赤いりんご、爆弾りんご、バスケット、背景、得点、残り時間、経過時間(見えないものですが、ゲームに登場する大事なもの)

動詞句を考える

名詞と名詞句以外は、処理(メソッド)になります

落ちてくる、横に動く、得点が増減する、残り時間が減る、ゲームオーバーになる、得点が表示される、残り時間が表示される

名詞を変数として定義する

とりあえず、どんどん作業していきます。型を記述して、変数名を書いていきます

Apple apple = new Apple();
PoisonApple poisonApple = new PoisonApple();
Basket basket = new Basket();
BackGround backGround = new BackGround();
int score;
int remainTime;
.....など

動詞句は処理やメソッドになる

名詞の時と同じようにどんどん作っていきます
プログラムにした時にどうなるのだろう?とか考え込まないことです

// 概要のコメント
// 落ちてくる → Drop()
// 横に動く → Move()
// 得点が増減する → UpdateScore()
// 時間が減る → DecreaseTime()
// ゲームオーバー判定 → bool GameOver()

コードを徐々に作っていく

次のように作っていきます

変数の宣言

概要のコメント

    プログラムの処理

      -> メソッド作成

当てはめると、

// 変数の宣言
Apple apple = new Apple();
PoisonApple poisonApple = new PoisonApple();
Basket basket = new Basket();
BackGround backGround = new BackGround();
int score;
int remainTime;
Time time = new Time();

// 概要のコメント
// 落ちてくる
// 横に動く
// 得点が増減する
// 時間が減る
// ゲームオーバーになる

概要のコメントは次のようになります
上から次から次へとりんご、または毒りんごが落ちてくるので繰り返し処理が必要だとかは、今は考えません。後で考えることが大切です

これで全てのメソッドが作れて、プログラムが完成すると考えるのではなく、必要なものが見つかれば都度更新していきます。最初に完璧を目指しません

// 概要のコメント
// 落ちてくる
void Drop()
// 横に動く
void Move() 
// 得点が増減する
void UpdateScore()
// 時間が減る
void DecreaseTime()
// ゲームオーバーになる
bool GameOver()

オブジェクトの動きに絞って少しずつ作っていく

りんごが落ちてくるところに絞って考えてみます
りんごだけじゃなくて時間も必要ですね

次の部分を使います
Appleクラス宣言で既にエラーになりますのでインテリセンスで自動作成してもらいましょう

// 変数の宣言
Apple apple = new Apple();
Time time = new Time();

// 概要のコメント
// 落ちてくる
void Drop()

インテリセンスも活用して組み立てていく

続いて、Timeクラスですが、既にTimerクラスが定義されていますので、これを使います
つまり、TimeをTimerに変更します

ここまでのコード

Appleクラスが作られています

// 変数の宣言
Apple apple = new Apple();
Timer timer = new Timer();

// 概要のコメント
// 落ちてくる
void Drop()
internal class Apple
{
}

落ちてくるは、アップルなので、これはAppleクラスに記述します

ここまでのコード

// 変数の宣言
Apple apple = new Apple();
Timer timer = new Timer();
internal class Apple
{
    // 落ちてくる
    public void Drop()
    {

    }
}

フォームアプリとして機能を追加

りんごのイラストを出せるようにする

りんごの実態は、PictureBoxです
PictureBoxの機能は全て使えるようにします
これは、継承でしたよね

internal class Apple : PictureBox

また、継承追加でエラーになりますので、インテリセンスの力を借ります

using System.Windows.Forms;

が追加されましたよね。

タイマーで動くようにする

次のように動かしたいですよね

Timer timer = new Timer();

timer.Interval = 1000;
timer.Tick += apple.Drop;
timer.Start(); // timer.Enabled = true;でも可

ただ、DropメソッドはAppleの話なのでイベントハンドラで作成は、次のように変更しインテリセンスに作成してもらいます

timer.Tick += apple.DropEvent;

Dropメソッドを考えると、Appleクラスは次のようになります
Topプロパティは、PictureBoxのY座標です

internal class Apple : PictureBox
{
    // 落ちてくる
    public void Drop()
    {
        // 背景色と重なって見えないため、とりあえず色をつけています
        // 本来、コンストラクタに記述します
        BackColor = System.Drawing.Color.Red;
        Top += 10;
    }

    internal void DropEvent(object sender, EventArgs e)
    {
        Drop();
    }
}

呼び出し側(Form1のコンストラクタブロック)

public Form1()
{
    InitializeComponent();

    // 変数の宣言
    Apple apple = new Apple();
    // アップルの親コンテナをthis(つまりフォーム)にする必要がありますので追記
    apple.Parent = this;

    Timer timer = new Timer();
    timer.Interval = 1000;
    timer.Tick += apple.DropEvent;
    timer.Start();
}

実行結果

まとめ

今回はオブジェクト指向で作っていく方法をみてきました
手続型との違いは感じられましたか?
最初から実際に動かすためのコードを作り始めないのがコツです
全体像ができてから、メソッドブロックに手続を記述することで解りやすく、まとまりのある設計になります。
これで、1つのメソッドにいろいろなことをさせることがなくなります
クラスではその名前に沿った必要としていることだけを記述します
そうすることで、頭の整理もできて理解しやすく、拡張もしやすいものになります

コード作成段階まで進んだときに、足りない名詞や動詞に気付いた場合

最初に気づかず途中から必要なことがわかった場合は、名詞や動詞句のところの手順に戻ってから続きの作業を進めるようにします

この後のプログラミングの進め方

最初に見てもらったビデオの全てが今までお話ししたことで実現されているわけではありません
最初から欲張って全てを盛り込む必要はありません。
慣れてくると、名詞抽出法で洗い出せるものが増えてくるでしょう。その中には、触れないものもたくさん出てくるでしょう。(例えば、「ゲームのコントロール」という名詞句がひ浮かぶようになります)
その時点で最初に戻って再設計するのです。

このページで全てのパターン、作成の手順について漏らさず記載することも可能ですが、どのようなことが起こるでしょうか?見た人は、最初からこれだけのことを見出せない、自分には到底無理って思うのではないでしょうか?

誰しもそのようには作っていません。コツコツ積み重ねるのです。
時間に使い方は、手続きのようではありません。プログラムが条件分岐文と繰り返し文で膨れ上がることもありません。
最初、手続きで作ってみて、メリットを体感するのも上達の近道です

おまけ

イラストの追加は、いろいろな方法があります

次は一例になります

画像を登録、設定

リソースの追加方法

ソリューションエクスプローラーから、Resources.resxをダブルクリック
リソースの追加メニューをプルダウン

既存のファイルの追加を選択し、登録したいファイルを選びます

リソースから画像を取得するプロパティ設定

apple.Image = Properties.Resources.fruit_apple;

他の方法について

リファクタリング案

今のコードは、りんごが落ちてくるタイミング(タイマーで制御)をフォーム側で行っています
ここは、りんご自身の作業でもよさそうです
りんごの画像も登録、使っています

Form1クラスコード(フル)

using System.Windows.Forms;

namespace BasePractice
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();

            // 変数の宣言
            Apple apple = new Apple();
            apple.Parent = this;
        }
    }
}

Appleクラスコード(フル)

using System;
using System.Drawing;
using System.Windows.Forms;

namespace BasePractice
{
    internal class Apple : PictureBox
    {
        public int Point { get; set; } = 10;

        public Apple()
        {
            Image = Properties.Resources.fruit_apple;
            SizeMode = PictureBoxSizeMode.StretchImage;
            Size = new Size(100, 100);

            Timer timer = new Timer();

            timer.Interval = 1000;
            timer.Tick += Update;
            timer.Start();
        }

        // 落ちてくる
        public void Drop()
        {
            Top += 10;

            if (Top > 100)
            {
                Dispose();
            }
        }

        //一定時間ごとに実行
        internal void Update(object sender, EventArgs e)
        {
            Drop();
        }
    }
}

InitializeComponentメソッド

変更の必要がないコードです。
Form1のコンストラクタから呼び出されるInitializeComponentメソッドのコードになります
Form1の情報のみであり、コントロールの追加はしてません

namespace BasePractice
{
    partial class Form1
    {
        /// <summary>
        /// 必要なデザイナー変数です。
        /// </summary>
        private System.ComponentModel.IContainer components = null;

        /// <summary>
        /// 使用中のリソースをすべてクリーンアップします。
        /// </summary>
        /// <param name="disposing">マネージド リソースを破棄する場合は true を指定し、その他の場合は false を指定します。</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && (components != null))
            {
                components.Dispose();
            }
            base.Dispose(disposing);
        }

        #region Windows フォーム デザイナーで生成されたコード

        /// <summary>
        /// デザイナー サポートに必要なメソッドです。このメソッドの内容を
        /// コード エディターで変更しないでください。
        /// </summary>
        private void InitializeComponent()
        {
            this.SuspendLayout();
            // 
            // Form1
            // 
            this.AutoScaleDimensions = new System.Drawing.SizeF(13F, 24F);
            this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
            this.ClientSize = new System.Drawing.Size(800, 450);
            this.Name = "Form1";
            this.Text = "Form1";
            this.ResumeLayout(false);

        }

        #endregion
    }
}

実行結果

参考