【C#】WindowsFormsアプリ作成で考えるオブジェクト指向、ポリモーフィズムの学習を含みます(Controlを継承)

2023年8月8日

WindowsFormsアプリで、ウィンドウ上にキャラクタのイメージを作成したい場合を想定して学習を進めましょう

サンプルアプリ

自作クラス(Playerクラス)には、属性として複数のアイテム(武器や薬)を持っていることとします
アイテムの一覧から選択し、攻撃ボタンをクリックすると武器の場合、備えている攻撃方法を使った攻撃する様子を文字列として表現しています

このサンプルでは、単純化のため、PlayerクラスにAttackメソッドを実装するようにはしていません

コード整理の項でリファクタリングしていますので、完成したらそちらに更新してみましょう

実行結果

最初にこの学習で構成したフォームでの実行結果をみてみましょう

サンプル画面の構成

実行中の様子

クラス設計

クラスの属性と振る舞いについて考えます

イメージ

継承関係とインターフェースの実装

サンプルのプログラムでは、攻撃できるインターフェース(IAttackble)型で宣言された変数に、こん棒か刀のインスタンスを代入することで違った振る舞いをするポリモーフィズムを実現しています

何が便利になったのかも含め、学習の材料としてください

デザイン画面の設計

このサンプルは、ソリューション名及びプロジェクト名をSimpleRPGSample1として新規に作成しています

プレイヤーは、実行中にnewキーワードで作成されますので最初配置はしません

フォーム画面

リソースの追加

次のリンクを参考に画像ファイルをリソースに追加します

コード

Form1クラス

起動した時のウィンドウのインスタンスになります
プレイヤーの作成とアイテムの選択、攻撃ボタンの実行の役割があります

イベントハンドラをイベントに登録する処理をVisualStudioに任せる場合

ListBoxでいずれかのデータが選択されたときに実行されるメソッド(イベントハンドラ)を登録するには、フォーム上のListBoxのオブジェクトをダブルクリックします
また、Buttonも同様にイベントハンドラを作成できます

自分でコードを書く場合

VisualStudioでコード作成を自分で代わりに記述する場合は次のように記述します
(上記の動画の説明のところの登録は削除します)

button1.Click += button1_Click; // button1_Clickは、メソッド(イベントハンドラ)名

全体のコード

using SimpleRPGSample1.Properties;
using System;
using System.Drawing;
using System.Windows.Forms;

namespace SimpleRPGSample1
{
    public partial class Form1 : Form
    {
        IAttackable attackable;
        Player player;

        public Form1()
        {
            InitializeComponent();

            InitializePlayerCharacter();
        }

        private void InitializePlayerCharacter()
        {
            // Playerクラスからインスタンスの作成
            player = new Player("山田");
            // サイズ
            player.Size = new Size(100, 100);
            // 場所
            player.Location = new Point(220, 270);
            // イメージ画像(Playerクラスではプロパティに代入)
            player.Image = Resources.onepiece01_luffy;

            // フォームのインスタンスのコントロールのリストに追加(大切です)
            Controls.Add(player);

            // プレイヤーの持ち物に追加1
            player.AddItem(new Stick("朽ち果てた斧"));
            // プレイヤーの持ち物に追加2
            player.AddItem(new Dagger("光り輝く刀"));

            // リストボックスに持ち物を表示
            foreach (var item in player.Items)
            {
                listBox1.Items.Add(item.Name).ToString();
            }

            // ボタンイベントの登録をコードで記述する場合(Button1_Clickは、メソッド(イベントハンドラ名)
            // button1.Click += button1_Click; 
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // 選択されてなければ何もしない
            if (attackable == null)
            {
                return;
            }
            // 攻撃すると、登録されているメソッドが呼び出され、その結果(サンプルでは文字列)が取得できます
            label1.Text = attackable.Attack();
        }

        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            // リストボックス(持ち物一覧)に表示された中で選択中のもの(選択番号はlist.Box1.SelectedIndexで取得)を
            // IAttacableにキャストします。実装されていないときはnullが代入されます
            attackable = player.Items[listBox1.SelectedIndex] as IAttackable;
        }
    }
}

Itemクラス

このケースでは、2つのアイテム(棍棒と刀)の基底クラス(継承元)になります
名前のプロパティのみ属性としてあります
コンストラクタでインスタンス作成時に名前を登録することができます
派生クラスでは、クラス名に:をつけて記述します

namespace SimpleRPGSample1
{
    internal class Item
    {
        public string Name { get; set; }

        public Item(string name)
        {
            Name = name;
        }
    }
}

IAttackableインターフェース(攻撃できるものに実装)

攻撃ができるアイテム
今回のサンプルではStick(こん棒)とDagger(刀)に実装します
実装クラスでは、クラス名に:をつけて記述します

namespace SimpleRPGSample1
{
    internal interface IAttackable
    {
        string Attack();
    }
}

Stickクラス(こん棒)

こん棒は、アイテムなのでItemクラスを継承しています
また、攻撃可能なアイテムとしてIAttackableインターフェースを実装しています
継承と実装の2つを備えるので(,)カンマで区切って記述します

namespace SimpleRPGSample1
{
    /// <summary>
    /// こん棒
    /// </summary>
    internal class Stick : Item, IAttackable
    {
        public Stick(string name) : base(name)
        {
        }

        public string Attack()
        {
            return "たたきつけ";
        }
    }
}

Daggerクラス(刀)

刀は、アイテムなのでItemクラスを継承しています
また、攻撃可能なアイテムとしてIAttackableインターフェースを実装しています
継承と実装の2つを備えるので(,)カンマで区切って記述します

namespace SimpleRPGSample1
{
    /// <summary>
    /// 刀
    /// </summary>
    internal class Dagger : Item, IAttackable
    {
        public Dagger(string name) : base(name)
        {
        }

        public string Attack()
        {
            return "切り付け攻撃";
        }
    }
}

Playerクラス(プレイヤー)

プレイヤーは、フォーム上の登場させるのでControlクラスを継承しています
Form1クラスで Controls.Addメソッドでフォールのコントロールリストに追加されます

また、プレイヤーは、自分のイラストを持っています(PictureBoxクラスの変数PictureBox)

Controlクラス(継承元)には、コントロールの一覧を持つ機能があります
Playerクラス(コントロール)にPictureBoxコントロールを持つ必要がありますので、Controls.Addメソッドで追加します

using System.Collections.Generic;
using System.Drawing;
using System.Windows.Forms;

namespace SimpleRPGSample1
{
    internal class Player : Control
    {
        // アイテムの一覧を保持するプロパティ
        public List<Item> Items { get; set; }
        // イラストを持っています
        public PictureBox PictureBox { get; private set; }
        // プレイヤーのイラストを代入してもらうプロパティ
        public Image Image { set => LoadImage(value); }

        // プレイヤーの名前も代入できますが、これは基底クラスのControlクラスに属性があるためです


        public Player(string name)
        {
            Name = name;
            Items = new List<Item>();

            InitializePictureBox();
        }

        public void AddItem(Item item)
        {
            Items.Add(item);
        }

        private void InitializePictureBox()
        {
            PictureBox = new PictureBox();
            PictureBox.Size= new Size(100, 100);
            PictureBox.BackColor = Color.Transparent;
            PictureBox.SizeMode = PictureBoxSizeMode.Zoom;

            Controls.Add(PictureBox);
        }

        private void LoadImage(Image image)
        {
            PictureBox.Image = image;
        }
    }
}

参考

コントロールの一覧はコントロールクラスが持っている!!

イメージ

コントロールないにコントロール・・・・

例えば、パネルコントロール内にパネルコントロールなど

クラス図

@startuml
interface IAttacable {
    Attack() : string
}
class Dagger {
    + Dagger(name:string)
    + Attack() : string
}
Item <|-- Dagger
IAttacable <|..  Dagger
class Stick {
    + Stick(name:string)
    + Attack() : string
}
Item <|-- Stick
IAttacable <|..  Stick
class Form1 <<partial>> {
    + Form1()
    - RegisterItems() : void
    - button1_Click(sender:object, e:EventArgs) : void
    - listBox1_SelectedIndexChanged(sender:object, e:EventArgs) : void
}
Form <|-- Form1
Form1 --> "attacable" IAttacable
Form1 --> "player" Player
class Form1 <<partial>> {
    # <<override>> Dispose(disposing:bool) : void
    - InitializeComponent() : void
}
class Item {
    + Name : string <<get>> <<set>>
    + Item(name:string)
}
class Player {
    + Name : string <<get>>
    + Player(name:string)
    + AddItem(item:Item) : void
}
class "List`1"<T> {
}
Player --> "Items<Item>" "List`1"
class Program <<static>> {
    {static} - Main() : void
}
class Settings <<sealed>> <<partial>> {
}
Settings o-> "defaultInstance" Settings
Settings --> "Default" Settings
class Resources {
    <<internal>> Resources()
}
@enduml

ユーザーにコントロールを簡単に作れるクラスも用意されています

Windowsフォームアプリを作成する時に、ビジュアル表示用にユーザーが自作しやすいようなクラスがあります
これは、Controlクラスを継承していますので、慣れてくればそちらを使う方が楽に作れます

コード整理

プレイヤーの武器選択と攻撃はプレイヤーのクラスに実装するように変更してみましょう

Form1クラス

武器の選択は、次のようにプレイヤークラスのメソッドを呼ぶようにします

// プレイヤークラスの武器選択を実行
player.ItemSelect(listBox1.SelectedIndex);

攻撃は、次のようにプレイヤークラスのメソッドを呼ぶようにします

// プレイヤークラスの攻撃メソッドを実行。戻り値が攻撃結果
string attackRusult = player.Attack();

if (attackRusult == null)
{
    return;
}

// 攻撃結果をラベルに表示
label1.Text = attackRusult;

全体のコード

さらに次のフィールドはプレイヤークラスに移動しています

 IAttackable attackable;
using SimpleRPGSample1.Properties;
using System;
using System.Drawing;
using System.Windows.Forms;

namespace SimpleRPGSample1
{
    public partial class Form1 : Form
    {
        Player player;

        public Form1()
        {
            InitializeComponent();

            InitializePlayerCharacter();
        }

        private void InitializePlayerCharacter()
        {
            // Playerクラスからインスタンスの作成
            player = new Player("山田");
            // サイズ
            player.Size = new Size(100, 100);
            // 場所
            player.Location = new Point(220, 270);
            // イメージ画像(Playerクラスではプロパティに代入)
            player.Image = Resources.onepiece01_luffy;

            // フォームのインスタンスのコントロールのリストに追加(大切です)
            Controls.Add(player);

            // プレイヤーの持ち物に追加1
            player.AddItem(new Stick("朽ち果てた斧"));
            // プレイヤーの持ち物に追加2
            player.AddItem(new Dagger("光り輝く刀"));

            // リストボックスに持ち物を表示
            foreach (var item in player.Items)
            {
                listBox1.Items.Add(item.Name).ToString();
            }

            // ボタンイベントの登録をコードで記述する場合(Button1_Clickは、メソッド名)
            //button1.Click += button1_Click;
        }

        // 攻撃ボタンをクリック
        private void button1_Click(object sender, EventArgs e)
        {
            // プレイヤークラスの攻撃メソッドを実行。戻り値が攻撃結果
            string attackRusult = player.Attack();

            if (attackRusult == null)
            {
                return;
            }

            // 攻撃結果をラベルに表示
            label1.Text = attackRusult;
        }

        // 武器の選択
        private void listBox1_SelectedIndexChanged(object sender, EventArgs e)
        {
            // プレイヤークラスの武器選択を実行
            player.ItemSelect(listBox1.SelectedIndex);
        }
    }
}

Playerクラス

武器の選択

Form1のコードを移動させた形になります

 // 武器の選択
public void ItemSelect(int selectItem)
{
// リストボックス(持ち物一覧)に表示された中で選択中のもの(選択番号はlist.Box1.SelectedIndexで取得)を
// IAttacableにキャストします。実装されていないときはnullが代入されます
attackable = Items[selectItem] as IAttackable;
        }

攻撃

こちらもForm1のコードを移動させた形になります

// 攻撃
public string Attack()
{
// 選択されてなければ何もしない
if (attackable == null)
{
    return null;
}
// 攻撃すると、登録されているメソッドが呼び出され、その結果(サンプルでは文字列)が取得できます
    return attackable.Attack();
}

全体のコード

using System.Collections.Generic;
using System.Drawing;
using System.Reflection.Emit;
using System.Windows.Forms;

namespace SimpleRPGSample1
{
    internal class Player : Control
    {
        IAttackable attackable;

        // アイテムの一覧を保持するプロパティ
        public List<Item> Items { get; set; }
        // イラストを持っています
        public PictureBox PictureBox { get; private set; }
        // プレイヤーのイラストを代入してもらうプロパティ
        public Image Image { set => LoadImage(value); }

        // プレイヤーの名前も代入できますが、これは基底クラスのControlクラスに属性があるためです


        public Player(string name)
        {
            Name = name;
            Items = new List<Item>();

            InitializePictureBox();
        }

        public void AddItem(Item item)
        {
            Items.Add(item);
        }

        private void InitializePictureBox()
        {
            PictureBox = new PictureBox();
            PictureBox.Size = new Size(100, 100);
            PictureBox.BackColor = Color.Transparent;
            PictureBox.SizeMode = PictureBoxSizeMode.Zoom;

            Controls.Add(PictureBox);
        }

        private void LoadImage(Image image)
        {
            PictureBox.Image = image;
        }

        // 武器の選択
        public void ItemSelect(int selectItem)
        {
            // リストボックス(持ち物一覧)に表示された中で選択中のもの(選択番号はlist.Box1.SelectedIndexで取得)を
            // IAttacableにキャストします。実装されていないときはnullが代入されます
            attackable = Items[selectItem] as IAttackable;
        }

        // 攻撃
        public string Attack()
        {
            // 選択されてなければ何もしない
            if (attackable == null)
            {
                return null;
            }
            // 攻撃すると、登録されているメソッドが呼び出され、その結果(サンプルでは文字列)が取得できます
            return attackable.Attack();
        }
    }
}

クラス図

@startuml
class Dagger {
    + Dagger(name:string)
    + Attack() : string
}
Item <|-- Dagger
IAttackable <|-- Dagger
interface IAttackable {
    Attack() : string
}
class Stick {
    + Stick(name:string)
    + Attack() : string
}
Item <|-- Stick
IAttackable <|-- Stick
class Form1 <<partial>> {
    + Form1()
    - InitializePlayerCharacter() : void
    - button1_Click(sender:object, e:EventArgs) : void
    - listBox1_SelectedIndexChanged(sender:object, e:EventArgs) : void
}
Form <|-- Form1
Form1 --> "player" Player
class Form1 <<partial>> {
    # <<override>> Dispose(disposing:bool) : void
    - InitializeComponent() : void
}
class Item {
    + Name : string <<get>> <<set>>
    + Item(name:string)
}
class Player {
    + Player(name:string)
    + AddItem(item:Item) : void
    - InitializePictureBox() : void
    - LoadImage(image:Image) : void
    + ItemSelect(selectItem:int) : void
    + Attack() : string
}
class "List`1"<T> {
}
Control <|-- Player
Player --> "attackable" IAttackable
Player --> "Items<Item>" "List`1"
Player --> "PictureBox" PictureBox
Player --> "Image" Image
class Program <<static>> {
    {static} - Main() : void
}
class Settings <<sealed>> <<partial>> {
}
Settings o-> "defaultInstance" Settings
Settings --> "Default" Settings
class Resources {
    <<internal>> Resources()
}
@enduml