Winフォームアプリで、オブジェクト指向アプリの作り方を学びましょう

2022年7月24日

ここでは、新しくアプリケーションを作る手順についてお話しします
教科書では、C#の文法やクラスの作り方、犬や車を使ったオブジェクトの概念について記述されていますが、自分で最初から作る時のノウハウについては示されていないことが多いです
理由としては、作るアプリケーションによってアプローチが多岐に渡ることが挙げられます
全てのアプリケーションをオブジェクト指向で作成することが最適解ではないこと、ただ単にオブジェクトにすればいいということでもないため、レクチャーが難しいのです

おさらい

手続型

次のように上から順番に実行されるようにコード行を追記していきます
全ての実行をこのように組み込んでいきます
構成的には、1つのクラス内にとどまるコードの書き方になります
オブジェクト指向でももちろんこれを使いますが、メソッドブロック内にコンパクトにまとめるところが違います

サンプルコード

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);

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

表示

9

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

取り組み方

教科書では文法を暗記してC#を理解しようとしていたと思います。もちろん大事なことですが、その延長上だけではプログラムの作成にたどり着けません。「あいうえお」だけの勉強では、小説が書けないですよね。

どんどんコードを書いていくことが大事なのですが、闇雲にやってもダメですね。コツが必要です。小説も書くことがありますよね。

そのコツの一つがオブジェクト指向になります。なので、暗記するとかのジャンルではなく、C#を知っている前提でどのように書き進めていくかのコツを手に入れる方法を身につけるという考え方が重要です

作りたいものを決める

一番大事なことです。文法を学ぶのではないので、まず作りたいものを決めます。大手の開発会社が手がけるようなものを考える必要は全くありません。無謀ですね。シンプルな機能のものから、徐々に大きくすればいいです。その場合、オブジェクト指向で作り始めることで、大きくしやすくなります

次のようなものをイメージしたとしましょう

実際は、頭の中にイメージを抱いてそれをイラストや箇条書きにメモることになります。

ゲーム名

アップルキャッチ

ゲーム内容

落ちてくるリンゴを少しでもたくさんカゴで受け止めましょう
受け止めたりんごの数が多いほど高得点になります
ただし、爆弾リンゴをとってしまうと得点が減りますので注意しましょう

操作方法

左右の矢印キーでカゴを操作します 制限時間内に少しでも多くリンゴを受け止めましょう ウィンドウを閉じるとゲーム終了です

オブジェクト指向的な最初の作業

やってはいけないのは、どのようにコードを書こうかを考え始めること、エディタを起動してコードを書き始めることです

このように考えるのは、手続型の思考になります。手続型でプログラミングするときは、分岐がどうなって繰り返しがどうなってとか、コード化した場合を想像していきますが、オブジェクト指向ではこの手順は後回しです

では、どうするのか。

名詞抽出法

名詞と名詞句を考える

ひたすら、名詞(猫)や名詞句(小さい猫)を書き出します

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

動詞句を考える

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

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

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

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

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() // ゲームオーバーになる(続くかどちらかなのでbool型)

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

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

変数の宣言

概要のコメント

    プログラムの処理

      -> メソッド作成

当てはめると、

// 変数の宣言
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 = 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(); // enabled = true;でも可

ただ、Dropメソッドはイベントハンドラではないため、一旦次のように変更しインテリセンスに助けてもらいます

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つのメソッドにいろいろなことをさせることがなくなります
また、クラスでも同じですね。
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
    }
}

実行結果

参考