オブジェクト指向で作った15パズルの基本的な機能を完成させる
カスタムボタンの配列を作る
前回の記事ではタイルの働きをする「カスタムボタン」を定義しましたが、このタイルは画面に16個置くことになります。このように、同じ機能を持ったオブジェクトを複数利用したいときに、オブジェクトを配列として扱う方法があります。配列にすると一括処理ができるので、プログラムを簡潔に書くことができます。
カスタムボタンCustomButtonをmyCustomButtonの名前で配列として定義するコードは次のようになります。
CustomButton[] myCustomButton;
この定義はForm1クラスに書くので、実際のコードは次のようになります。
namespace SampleProject2 { public partial class Form1 : Form { CustomButton[] myCustomButton; public Form1() { (以下略)
カスタムボタン配列のインスタンスを作り実体化する
オブジェクトの定義をクラスと言い、クラスに基づいてオブジェクトを実体化したものをインスタンスと言います。オブジェクトを実体化することを「インスタンス化」と言います。次のコードでは、CustomButtonクラスを配列にしたmyCustomButtonのインスタンスを、for繰り返し文で16個作ります。またそれぞれのmyCustomButton配列をクリックしたときのイベントハンドラを作り、フォームに加える処理をし、Initialize()メソッドを呼び出して各ボタンの設定を初期化します。
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イベントハンドラとイベントハンドラによって実行されるメソッドを作るには、デザイナでフォームの何もないところをダブルクリックします。すると、次のようなコードが表示されます。
private void Form1_Load(object sender, EventArgs e) { }
ここで見えているのは「Loadイベントハンドラによって実行されるメソッド」です。ここに書いたコードは、フォームがロードされたときに実行されます。myCustomButton配列をインスタンス化する以外に、もうひとつ、ゲームの最初に実行したいコードがあります。それはフォーム全体の大きさClientSizeを決めるコードです。またボタン配列を16個作った後、最後の16番目のカスタムボタンのVisibleプロパティをfalseにして非表示にします。配列の添え字は0から始まるので、配列の要素としてはmyCustomButton[15]となります。
これらを追加した結果、Form1のロードイベントによって実行される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()メソッドの次に書きましょう。
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]との位置を交換するには、次のように書けばよいことになります。
sender.Move(myCustomButton[15])
しかしこのコードはエラーになります。理由は、クリックイベントハンドラからメソッドに処理を移すときに、オブジェクトがobject型に変換されて渡されるからです。そこで実際のコードは、senderをCustomButtonに戻す型変換をして処理することになります。myCustomButton_Click()メソッドは次のように記述します。
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にします。
foreach (CustomButton allCustomButton in myCustomButton) { allCustomButton.isMovable = true; }
すべてのカスタムボタン配列に対してisMovableフィールドの値を設定し、移動可能かどうかを判定するsetMovable()メソッドを作りましょう。コードは次のようになります。このコードではすべてのカスタムボタン配列に対してisMovableフィールドの値をtrueにし、表示する文字をテスト用のコードで書き替えています。
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であること、です。
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」になったことを示しています。
ここまでできればゲームの基本的機能が完成です。
ここまでのコードは次のようになります。
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; } } } }