オブジェクト指向で作った15パズルの基本的な機能を完成させる

2020年12月10日(木)
松本 吉生(まつもと よしお)
連載4回目となる今回は、前回に続いてオブジェクト指向で作った15パズルにコードを追加し、基本的な機能を完成させます。

カスタムボタンの配列を作る

前回の記事ではタイルの働きをする「カスタムボタン」を定義しましたが、このタイルは画面に16個置くことになります。このように、同じ機能を持ったオブジェクトを複数利用したいときに、オブジェクトを配列として扱う方法があります。配列にすると一括処理ができるので、プログラムを簡潔に書くことができます。

カスタムボタンCustomButtonをmyCustomButtonの名前で配列として定義するコードは次のようになります。

リスト1:カスタムボタンを配列として定義

CustomButton[] myCustomButton;

この定義はForm1クラスに書くので、実際のコードは次のようになります。

リスト2:カスタムボタンの配列はここで定義する

namespace SampleProject2
{
    public partial class Form1 : Form
    {
        CustomButton[] myCustomButton;

        public Form1()
        {
(以下略)

カスタムボタン配列のインスタンスを作り実体化する

オブジェクトの定義をクラスと言い、クラスに基づいてオブジェクトを実体化したものをインスタンスと言います。オブジェクトを実体化することを「インスタンス化」と言います。次のコードでは、CustomButtonクラスを配列にしたmyCustomButtonのインスタンスを、for繰り返し文で16個作ります。またそれぞれのmyCustomButton配列をクリックしたときのイベントハンドラを作り、フォームに加える処理をし、Initialize()メソッドを呼び出して各ボタンの設定を初期化します。

リスト3:各カスタムボタンをインスタンス化し初期化

for (int i = 0; i < 16; i++)
{
    myCustomButton[i] = new CustomButton();
    myCustomButton[i].Click += myCustomButton_Click;
    this.Controls.Add(myCustomButton[i]);
    myCustomButton[i].Initialize(i);
}

では、このインスタンスを生成するコードはどこに書けばよいでしょう。カスタムボタン配列のインスタンスは、ゲームの最初に生成していなければいけません。ですから、このコードは、フォームのLoadイベントで実行するようにすればよいでしょう。Loadイベントハンドラとイベントハンドラによって実行されるメソッドを作るには、デザイナでフォームの何もないところをダブルクリックします。すると、次のようなコードが表示されます。

リスト4:インスタンスを生成するコードを書く場所

private void Form1_Load(object sender, EventArgs e)
{

}

ここで見えているのは「Loadイベントハンドラによって実行されるメソッド」です。ここに書いたコードは、フォームがロードされたときに実行されます。myCustomButton配列をインスタンス化する以外に、もうひとつ、ゲームの最初に実行したいコードがあります。それはフォーム全体の大きさClientSizeを決めるコードです。またボタン配列を16個作った後、最後の16番目のカスタムボタンのVisibleプロパティをfalseにして非表示にします。配列の添え字は0から始まるので、配列の要素としてはmyCustomButton[15]となります。

これらを追加した結果、Form1のロードイベントによって実行されるForm1_Load()メソッドのコードは次のようになります。

リスト5:完成したForm1_Load()メソッド

private void Form1_Load(object sender, EventArgs e)
{
    this.ClientSize = new Size(400, 400);
    myCustomButton = new CustomButton[16];

    for (int i = 0; i < 16; i++)
    {
        myCustomButton[i] = new CustomButton();
        myCustomButton[i].Click += myCustomButton_Click;
        this.Controls.Add(myCustomButton[i]);
        myCustomButton[i].Initialize(i);
    }

    myCustomButton[15].Visible = false;
}

次にmyCustomButtonのクリックイベントによって実行されるmyCustomButton_Click()メソッドを定義しておかなければいけません。このメソッドはここでは何も記述せず、いまのところは何もしないメソッドのままにしておきます。中身のないメソッドを定義しておくのは、イベントハンドラに対応するメソッドがないと、デバッグ実行したときにエラーになるからです。以下のコードをForm1_Load()メソッドの次に書きましょう。

リスト6:ボタンをクリックした際に実行されるメソッドの準備

private void myCustomButton_Click(object sender, EventArgs e)
{

}

ここまでできれば、Visual Studioで緑の三角「開始」ボタンをクリックするか、「デバッグ」メニューの「デバッグの開始」でビルドしてプログラムを実行します。CustomButtonクラスを配列にしたmyCustomButtonのインスタンスが実体化し、フォーム上に並びます。

プログラムを実行したところ

プログラムを実行したところ

各ボタンの表示はデバッグのために要素がわかるようにしています。最初の数字はボタン配列の番号を意味します。その次に「True」か「False」のいずれかが表示されますが、これはクリックしたときに移動可能かどうかを判定するisMovableフィールドの値です。最後の数字は各ボタンの位置を示す値です。位置は左上から右下に向けて1から16まで付けられます。これらの値は、カスタムボタンをクラスとして定義したときにフィールドとして持たせた値です。

マウスでボタンをクリックすると、ボタンの色が変わりクリックできることがわかります。これはVisual Studioで用意されたボタンオブジェクトの性質を「継承」しているからです。しかしクリックしてもボタンに変化は起こりません。なぜなら、クリックイベントによって実行されるメソッドに何も記述をしていないからです。また右下の空白タイルの上と左に位置するタイルは移動可能ですが、isMovableフィールドの値を設定していないので、どちらもFalseのままになっています。

クリックイベントによって実行されるメソッド

myCustomButtonのクリックイベントによって実行されるmyCustomButton_Click()メソッドをプログラミングしましょう。このメソッドには引数としてobject senderと EventArgs eが定義されています。ここでobject senderとは、このイベントを発生させたオブジェクト自身のことを指しています。つまり、myCustomButtonは配列ですから、たとえばmyCustomButton[0]がクリックされたときはmyCustomButton[0]自身が、myCustomButton[1]がクリックされたときはmyCustomButton[1]自身がsenderとなります。このクリックイベントによって実行したいのは、自分自身と空白のタイルであるmyCustomButton[15]との位置を交換することですから、交換処理はCustomButtonクラスにMoveメソッドとして定義したものを呼び出せばよいのです。つまり、自分とmyCustomButton[15]との位置を交換するには、次のように書けばよいことになります。

リスト7:タイルの位置を交換するメソッド?

sender.Move(myCustomButton[15])

しかしこのコードはエラーになります。理由は、クリックイベントハンドラからメソッドに処理を移すときに、オブジェクトがobject型に変換されて渡されるからです。そこで実際のコードは、senderをCustomButtonに戻す型変換をして処理することになります。myCustomButton_Click()メソッドは次のように記述します。

リスト8:正しいタイルの位置を交換するメソッド

private void myCustomButton_Click(object sender, EventArgs e)
{
    ((CustomButton)sender).Move(myCustomButton[15]);
}

カスタムボタン配列のisMovableフィールドをセットするメソッド

タイルはクリックすると空白のところに移動します。空白には見えない16番目のタイルがありますから、空白への移動は16番目のタイルと位置を交換する処理になります。このとき、移動できるかどうかを判定するために、カスタムボタンにisMovableフィールドを作りました。isMovableフィールドの値は、移動可能なタイルはTrueに、移動不可能なタイルはFalseとなります。この値は、タイルの位置が変わるたびに書き換えなければいけません。そこでカスタムボタン配列のisMovableフィールドをセットするメソッドを作ります。メソッド名はsetMovable()としましょう。

このメソッドを考えるときに、カスタムボタンを配列にしたことを活かす処理ができます。それはforeachステートメントを使うことです。foreachステートメントの中では、配列の要素すべてに対する処理を簡潔に書くことができます。foreachステートメントの書き方は次のようになります。このステートメントは、myCustomButton配列のすべての要素に対して、CustomButtonクラスのallCustomButtonという名前で処理をする、という意味になります。たとえば次のコードは、すべてのカスタムボタン配列に対してisMovableフィールドの値をtrueにします。

リスト9:すべてのisMovableフィールドをtrueにする

foreach (CustomButton allCustomButton in myCustomButton)
{
    allCustomButton.isMovable = true;
}

すべてのカスタムボタン配列に対してisMovableフィールドの値を設定し、移動可能かどうかを判定するsetMovable()メソッドを作りましょう。コードは次のようになります。このコードではすべてのカスタムボタン配列に対してisMovableフィールドの値をtrueにし、表示する文字をテスト用のコードで書き替えています。

リスト10:setMovable()メソッド

private void setMovable()
{
    foreach (CustomButton allCustomButton in myCustomButton)
    {
        allCustomButton.isMovable = true;
        allCustomButton.Text = allCustomButton.Name + allCustomButton.isMovable + allCustomButton.Position;//テスト用のコード
    }
}

次にsetMovable()メソッドがフォームの起動時に実行されるように、Form1_Load()メソッドにsetMovable();を記述します。ここまでできれば、Visual Studioで緑の三角「開始」ボタンをクリックするか、「デバッグ」メニューの「デバッグの開始」でビルドしてプログラムを実行します。実行するとすべてのボタンのisMovableフィールドの値がtrueになっていることがわかります。そしてどのボタンもクリックすると空白の位置と交換できることがわかります。

ここまでのプログラムを実行したところ

ここまでのプログラムを実行したところ

isMovableフィールドの値は、そのボタンが空白に移動可能な位置にあるかどうかを判定するためのものです。そこでsetMovable()メソッドを次のように、空白のタイルである16番目のボタンmyCustomButton[15]との位置を判定してから、isMovableフィールドの値をtrueにするコードに変更します。

条件式は、そのボタンのTopの値とmyCustomButton[15]のTopの値が同じで、かつ、そのボタンのLeftの値とmyCustomButton[15]のLeftの値の差が100であること、または、そのボタンのLeftの値とmyCustomButton[15]のLeftの値が同じで、かつ、そのボタンのTopの値とmyCustomButton[15]のTopの値の差が100であること、です。

リスト11:完成したsetMovable()メソッド

private void setMovable()
{
    foreach (CustomButton allCustomButton in myCustomButton)
    {
        if ((allCustomButton.Top == myCustomButton[15].Top & Math.Abs(allCustomButton.Left - myCustomButton[15].Left) == 100 | (allCustomButton.Left == myCustomButton[15].Left & Math.Abs(allCustomButton.Top - myCustomButton[15].Top) == 100)))
        {
            allCustomButton.isMovable = true;
        }
        else
        {
            allCustomButton.isMovable = false;
        }

        allCustomButton.Text = allCustomButton.Name + allCustomButton.isMovable + allCustomButton.Position;//テスト用のコード
    }
}

ボタンをクリックすると位置が変わるので、そのたびに位置を判定してisMovableフィールドの値を書き換えなければいけません。そのためにmyCustomButton_Click()メソッドにもsetMovable();を記述します。ここまでできれば、Visual Studioで緑の三角「開始」ボタンをクリックするか、「デバッグ」メニューの「デバッグの開始」でビルドしてプログラムを実行します。実行すると、ボタンの位置を判定し、右下の隅にある空白の上と左にあるボタン、番号では12と15のボタンだけisMovableフィールドの値がtrueになっていることがわかります。isMovableフィールドの値がfalseになっている他のボタンは、クリックしても位置が動きません。

試しに12のボタンをクリックしてみると下に動き、空白と位置を交換します。空白が上がると上下と左に接するボタンのisMovableフィールドの値がtrueになり、クリックして移動できるようになります。また、各ボタンの表示で最初の数字はボタン配列の番号を意味しており、最後の数字はカスタムボタンをクラスとして定義したときにフィールドとして持たせた、各ボタンの位置を示す値です。位置は左上から右下に向けて1から16まで付けられているので、ボタンをクリックした後にこれらの値が変わることを確かめてください。12番のボタンは、はじめは「12True12」と表示していますが、クリックして下に移動すると表示は「12True16」に変わります。ボタンの番号は「12」で、位置交換は「True」で可能、タイルの位置は「16」になったことを示しています。

ここまでできればゲームの基本的機能が完成です。

起動時の画面

起動時の画面

12番のタイルをクリックした後の画面。ボタンの表示が変わっている

12番のタイルをクリックした後の画面。ボタンの表示が変わっている

ここまでのコードは次のようになります。

リスト12:ここまでのコード全体

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace _15puzzle_objective_Text
{
    public partial class Form1 : Form
    {
        CustomButton[] myCustomButton;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.ClientSize = new Size(400, 400);
            myCustomButton = new CustomButton[16];

            for (int i = 0; i < 16; i++)
            {
                myCustomButton[i] = new CustomButton();
                myCustomButton[i].Click += myCustomButton_Click;
                this.Controls.Add(myCustomButton[i]);
                myCustomButton[i].Initialize(i);
            }

            myCustomButton[15].Visible = false;

            setMovable();
        }
        private void myCustomButton_Click(object sender, EventArgs e)
        {
            ((CustomButton)sender).Move(myCustomButton[15]);

            setMovable();
        }

        private void setMovable()
        {
            foreach (CustomButton allCustomButton in myCustomButton)
            {
                if ((allCustomButton.Top == myCustomButton[15].Top & Math.Abs(allCustomButton.Left - myCustomButton[15].Left) == 100 | (allCustomButton.Left == myCustomButton[15].Left & Math.Abs(allCustomButton.Top - myCustomButton[15].Top) == 100)))
                {
                    allCustomButton.isMovable = true;
                }
                else
                {
                    allCustomButton.isMovable = false;
                }

                allCustomButton.Text = allCustomButton.Name + allCustomButton.isMovable + allCustomButton.Position;//テスト用のコード
            }
        }
    }
    class CustomButton : Button
    {
        public string Position;
        public bool isMovable;

        public CustomButton()
        {
            this.Width = 100;
            this.Height = 100;
            this.Font = new Font("Arial", 20);
            this.isMovable = false;
        }
        public void Initialize(int i)
        {
            this.Name = (i + 1).ToString();
            this.Position = (i + 1).ToString();
            this.Text = (i + 1).ToString();

            this.Top = (i / 4) * 100;
            this.Left = (i % 4) * 100;
            this.Text = this.Name + this.isMovable + this.Position;//テスト用のコード
        }

        public void Move(CustomButton cb)
        {
            if (this.isMovable == true)
            {
                int tempTop;
                int tempLeft;
                string tempPosition;
                tempTop = this.Top;
                tempLeft = this.Left;
                tempPosition = this.Position;
                this.Top = cb.Top;
                this.Left = cb.Left;
                this.Position = cb.Position;
                cb.Top = tempTop;
                cb.Left = tempLeft;
                cb.Position = tempPosition;
            }
        }
    }
}
著者
松本 吉生(まつもと よしお)

京都に生まれ、神戸で幼少期を過ごす。

大学で応用化学を学んだのち、理科教諭として高等学校に勤務する。教育の情報化が進む中で校内ネットワークの構築運用に従事し、兵庫県立明石高等学校で文部科学省の「光ファイバー網による学校ネットワーク活用方法研究開発事業」に携わる。兵庫県立西宮香風高等学校では学籍管理データベースシステムをSQL Serverで構築・運用、兵庫県立神戸工業高等学校ではC#プログラミング、IoTなどのコンピュータ教育を行い、現在は兵庫県立神戸甲北高等学校に勤務する。

2004年からマイクロソフトMVP(Microsoft Most Valuable Professional)を受賞し、現在17回目の連続受賞。2020年1月14日に「令和元年度文部科学大臣優秀教職員表彰」を受賞。

連載バックナンバー

開発言語技術解説
第5回

オブジェクト指向で作った15パズルの完成

2020/12/24
連載5回目となる今回は、これまで作成してきた15パズルを完成させます。
開発言語技術解説
第4回

オブジェクト指向で作った15パズルの基本的な機能を完成させる

2020/12/10
連載4回目となる今回は、前回に続いてオブジェクト指向で作った15パズルにコードを追加し、基本的な機能を完成させます。
開発言語技術解説
第3回

オブジェクト指向プログラミングで15パズルを作ってみる

2020/11/26
連載3回目となる今回は、前回まで作成したオブジェクト指向でないプログラムの問題点を明らかにしつつ、オブジェクト指向を意識したプログラムを作成していきます。

Think ITメルマガ会員登録受付中

Think ITでは、技術情報が詰まったメールマガジン「Think IT Weekly」の配信サービスを提供しています。メルマガ会員登録を済ませれば、メルマガだけでなく、さまざまな限定特典を入手できるようになります。

Think ITメルマガ会員のサービス内容を見る

他にもこの記事が読まれています