前回はJavaScriptで15パズルをつくりましたが、ブログ名が鳩でもわかるC#なのでC#(WindowsForms)でも作ってみます。
Contents
Pieceクラスの定義
まず、ピースを描画するためのPieceクラスを定義します。使用する定数はForm1クラスで以下のように定義されています。
1 2 3 4 5 6 7 8 9 |
public partial class Form1 : Form { // 定数の定義 public const int PieceSize = 80; public const int MarginLeft = 20; public const int MarginTop = 20; public const int RowMax = 4; public const int ColMax = 4; } |
コンストラクタ
Pieceクラスを以下のように定義します。
コンストラクタにはピースに描画するBitmapオブジェクトとそのピースがどの位置に配置されるべきか(行と列番号)を渡します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Piece { public int X = 0; public int Y = 0; public int Number = 0; Bitmap _image; public Piece(Bitmap image, int row, int col) { _image = image; X = Form1.PieceSize * col + Form1.MarginLeft; Y = Form1.PieceSize * row + Form1.MarginTop; Number = row * Form1.RowMax + col + 1; } } |
描画処理
ピースの描画の処理をする部分を示します。
Bitmapを描画したあとピース番号と外枠を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
public class Piece { Pen _pen = new Pen(Color.White, 1); Font _font = new Font("MS ゴシック", 28, FontStyle.Bold); Brush _textBrush = Brushes.White; public void Draw(Graphics graphics) { if (Number != Form1.RowMax * Form1.ColMax) { graphics.DrawImage(_image, new Rectangle(X, Y, Form1.PieceSize, Form1.PieceSize)); graphics.DrawString(Number.ToString(), _font, _textBrush, new Point(X + 10, Y + 10)); graphics.DrawRectangle(_pen, new Rectangle(X, Y, Form1.PieceSize, Form1.PieceSize)); } } } |
外枠は描画しないでBitmapのみを描画する処理を示します。
1 2 3 4 5 6 7 |
public class Piece { public void DrawOnlyImage(Graphics graphics) { graphics.DrawImage(_image, new Rectangle(X, Y, Form1.PieceSize, Form1.PieceSize)); } } |
座標のセット
ピースを固定する位置にセットする処理を示します。
1 2 3 4 5 6 7 8 |
public class Piece { public void SetPosition(int row, int col) { X = Form1.PieceSize * col + Form1.MarginLeft; Y = Form1.PieceSize * row + Form1.MarginTop; } } |
クリック時への対応
クリックされた座標が固定されているピースの内部かどうかを調べる処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Piece { public bool IsClicked(int x, int y) { Rectangle rect = new Rectangle(X, Y, Form1.PieceSize, Form1.PieceSize); if (x < rect.Left) return false; if (y < rect.Top) return false; if (x > rect.Right) return false; if (y > rect.Bottom) return false; return true; } } |
ピースは正しい位置にあるかどうか?
固定されているピースが本来あるべき位置にあるかどうかを調べる処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class Piece { public bool Check() { int col = (X - Form1.MarginLeft) / Form1.PieceSize; int row = (Y - Form1.MarginTop) / Form1.PieceSize; // 左上を1として順番に番号を振っていた場合の数とNumberが一致していればよい if (Number == row * Form1.RowMax + col + 1) return true; else return false; } } |
Form1クラスの定義
Form1クラスを定義します。デザイナで以下のようなものをつくります。
初期化の処理
もとのイメージをリソースから読み込んで16分割してそれぞれのピースを生成します。そして生成されたPieceオブジェクトをリストに格納します。そのあとパズルが解けるまでの時間を計測するためのタイマーと描画更新処理用のタイマーのふたつを初期化します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
public partial class Form1 : Form { public const int PieceSize = 80; public const int MarginLeft = 20; public const int MarginTop = 20; public const int RowMax = 4; public const int ColMax = 4; List<Piece> Pieces = new List<Piece>(); Timer _updateTimer = new Timer(); Timer _timer = new Timer(); public Form1() { InitializeComponent(); Bitmap sourceBitmap = Properties.Resources.image; for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { Bitmap bitmap = new Bitmap(PieceSize, PieceSize); Graphics g = Graphics.FromImage(bitmap); g.DrawImage(sourceBitmap, new Rectangle(0, 0, PieceSize, PieceSize), new Rectangle(PieceSize * col, PieceSize * row, PieceSize, PieceSize), GraphicsUnit.Pixel); g.Dispose(); Pieces.Add(new Piece(bitmap, row, col)); } } BackColor = Color.Black; // 背景を黒にしてスタイリッシュに DoubleBuffered = true; // ちらつき防止 // タイマーの初期化 _updateTimer.Interval = 50; _updateTimer.Tick += UpdateTimer_Tick; _updateTimer.Start(); _timer.Interval = 1000; _timer.Tick += Timer_Tick; // フォーム上にボタンを配置するがそれでもForm.OnKeyDownでKeyDownイベントを補足できるようにする KeyPreview = true; } } |
Timer.Tickイベント時の処理
Timer.Tickイベントが発生したら消費時間のインクリメントまたは描画更新処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { int _secondsConsumed = 0; // 消費時間 private void Timer_Tick(object sender, EventArgs e) { _secondsConsumed++; } private void UpdateTimer_Tick(object sender, EventArgs e) { Invalidate(); } } |
描画処理
描画をする処理の部分を示します。
パズルが完成していない場合はピースには画像だけでなく枠線や番号を描画します。完成時には画像だけを描画します。if文で処理を切り分けています。そのあと消費時間を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public partial class Form1 : Form { Font _timeFont = new Font("MS UI Gothic", 12, FontStyle.Bold); SolidBrush _timeBrush = new SolidBrush(Color.White); Point _timePosition = new Point(352, 60); // 消費時間を表示させるのはこのあたりがよさそう SolidBrush _piecesBackBrush = new SolidBrush(Color.White); Rectangle _piecesBackRectangle = new Rectangle(MarginLeft, MarginTop, PieceSize * ColMax, PieceSize * RowMax); bool _isCleared = false; protected override void OnPaint(PaintEventArgs e) { if (!_isCleared) { // 16番ピースは描画されないのでその位置の背景が白になるようにピースがある位置全体を白で e.Graphics.FillRectangle(_piecesBackBrush, _piecesBackRectangle); foreach (Piece piece in Pieces) piece.Draw(e.Graphics); } else { foreach (var piece in Pieces) piece.DrawOnlyImage(e.Graphics); } // 消費時間を表示 e.Graphics.DrawString($"{_secondsConsumed} 秒", _timeFont, _timeBrush, _timePosition); base.OnPaint(e); } } |
ピースを移動させる処理
マウスボタンが押下されたらピースを移動させます。ただしピースが移動中の場合とすでにパズルが完成している場合はなにもしません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public partial class Form1 : Form { bool _isMoving = false; // 現在移動中か? WMPLib.WindowsMediaPlayer player = new WMPLib.WindowsMediaPlayer(); // 効果音再生用 protected override async void OnMouseDown(MouseEventArgs e) { if (_isMoving || _isCleared) return; Piece piece = Pieces.FirstOrDefault(p => p.IsClicked(e.X, e.Y)); if(piece == null) return; Piece piece16 = Pieces.FirstOrDefault(p => p.Number == RowMax * ColMax); // 移動可能か調べる bool noMove = false; if (piece == piece16) noMove = true; if (piece.X != piece16.X && piece.Y != piece16.Y) noMove = true; if (Math.Abs(piece.X - piece16.X) > PieceSize || Math.Abs(piece.Y - piece16.Y) > PieceSize) noMove = true; // 移動不可能であれば警告音 if (noMove) { player.URL = Application.StartupPath + "\\bad.mp3"; return; } // 移動可能であれば16番ピースと場所を入れ替える await MoveTo16(piece); base.OnMouseDown(e); } } |
MoveTo16メソッドはピースを16番ピースがある位置へすべるように移動させます。そして16番ピースを第一引数のピースがあった位置に移動させます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
public partial class Form1 : Form { async Task MoveTo16(Piece piece) { player.URL = Application.StartupPath + "\\move.mp3"; _isMoving = true; int oldX = piece.X; int oldY = piece.Y; Piece piece16 = Pieces.FirstOrDefault(p => p.Number == RowMax * ColMax); int speed = 10; for (int i = 0; i < PieceSize / speed; i++) { if (piece16.X < piece.X) piece.X -= speed; if (piece16.X > piece.X) piece.X += speed; if (piece16.Y < piece.Y) piece.Y -= speed; if (piece16.Y > piece.Y) piece.Y += speed; await Task.Delay(30); } piece.X = piece16.X; piece.Y = piece16.Y; piece16.X = oldX; piece16.Y = oldY; _isMoving = false; // パズルが完成しているかもしれないのでチェックする if (Pieces.All(p => p.Check())) { _isCleared = true; _timer.Stop(); player.URL = Application.StartupPath + "\\clear.mp3"; } } } |
キー操作でも移動できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
public partial class Form1 : Form { protected override bool ProcessDialogKey(Keys keyData) { return false; } protected override async void OnKeyDown(KeyEventArgs e) { if (_isMoving || _isCleared) return; Piece piece16 = Pieces.FirstOrDefault(p => p.Number == RowMax * ColMax); int old16X = piece16.X; int old16Y = piece16.Y; Piece piece = null; // キーに応じて16番ピースの上下左右にピースが存在するか調べる if (e.KeyCode == Keys.Up) piece = Pieces.FirstOrDefault(p => p.X == piece16.X && p.Y == piece16.Y + PieceSize); if (e.KeyCode == Keys.Down) piece = Pieces.FirstOrDefault(p => p.X == piece16.X && p.Y == piece16.Y - PieceSize); if (e.KeyCode == Keys.Left) piece = Pieces.FirstOrDefault(p => p.X == piece16.X + PieceSize && p.Y == piece16.Y); if (e.KeyCode == Keys.Right) piece = Pieces.FirstOrDefault(p => p.X == piece16.X - PieceSize && p.Y == piece16.Y); // 移動できるピースがある場合はそれを移動させる // 存在しない場合は警告音 if (piece != null) await MoveTo16(piece); else player.URL = Application.StartupPath + "\\bad.mp3"; base.OnKeyDown(e); } } |
ピースをシャッフルする
[スタート]ボタンが押されたらピースをシャッフルし、消費時間を0にリセットしたあとタイマーをスタートさせます。
ピースをシャッフルしたあと置換パリティーをチェックします。置換パリティーが偶でないなら解けないパズルになっているのでシャッフルの処理をやり直します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { private void button1_Click(object sender, EventArgs e) { GameStart(); } void GameStart() { while (true) { if (Shuffle() % 2 == 0) // 置換パリティーが偶でないならやり直し break; } _secondsConsumed = 0; _timer.Start(); _isCleared = false; } } |
ピースをシャッフルする処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
public partial class Form1 : Form { Random _random = new Random(); int Shuffle() { // 16番ピースを右下に配置 Piece piece16 = Pieces.First(p => p.Number == RowMax * ColMax); piece16.SetPosition(RowMax - 1, ColMax - 1); // 16番ピース以外をlist内にコピー List<Piece> list = Pieces.Where(p => p.Number != RowMax * ColMax).ToList(); // 各位置に配置するピースをlistのなかからランダムに抜き出す for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { if (row == RowMax - 1 && col == ColMax - 1) break; int r = _random.Next(list.Count); list[r].SetPosition(row, col); list.RemoveAt(r); } } return ParityCheck(Pieces); } // 置換パリティーのチェック int ParityCheck(List<Piece> pieces) { // 左上から順番に各ピースの番号を取得して配列に格納する List<int> numbers = new List<int>(); for (int row = 0; row < RowMax; row++) { for (int col = 0; col < ColMax; col++) { Piece piece = pieces.First(p => p.X / PieceSize == col && p.Y / PieceSize == row); numbers.Add(piece.Number); } } // 配列内の要素を1~16まで順番に並ぶようにするには何回2つの要素を入れ替えればいいか数える int parity = 0; for (int i = 1; i <= RowMax * ColMax; i++) { if (numbers[i - 1] == i) continue; int ret = numbers.IndexOf(i); int n = numbers[i - 1]; numbers[i - 1] = i; numbers[ret] = n; parity++; } return parity; } } |
初めまして。
記事を読みながらとりあえず見様見真似でコーディングしていたのですが、下記の一文だけどうしてもエラーが出てしまいます。
Bitmap sourceBitmap = Properties.Resources.image;
エラー内容
CS0117:Resources に image の定義がありません。
この場合はどう対処すればいいのでしょうか?
よろしければご教示頂けると幸いです。
説明が足りなくてすみません。
まずメニューの[プロジェクト] [○○のプロパティ]を選択
[リソース]の部分に適当な画像(PieceSize=80で4行4列なので幅高さ320ピクセル以上のもの)をドラッグアンドドロップ
ファイル名はimage.pngとかimage.jpgとか。それ以外の名前のときはProperties.Resources.image;のimageの部分を変える。
これでできるはず。
指摘の通りにリソースに画像を追加してファイル名を指定したところ、無事動きました!
ありがとうございました!!!