C# WindowsFormsでジグソーパズルを作ります。
画像を切り抜くには?
ジグソーパズルを作るにはひとつの画像をピースに分解しなければなりません。ピースのような複雑な画像を生成するにはどうすればいいでしょうか?
Regionクラスを使うと画像を自由な形に切り抜くことができます。
これでジグソーパズルのピースっぽい画像を出力することができます。
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 |
void SavePieceImage() { GraphicsPath pathLeft = new GraphicsPath(); pathLeft.AddEllipse(new Rectangle(0, 60, 60, 60)); GraphicsPath pathRight = new GraphicsPath(); pathRight.AddEllipse(new Rectangle(120, 60, 60, 60)); GraphicsPath pathTop = new GraphicsPath(); pathTop.AddEllipse(new Rectangle(60, 0, 60, 60)); GraphicsPath pathBottom = new GraphicsPath(); pathBottom.AddEllipse(new Rectangle(60, 120, 60, 60)); Region region = new Region(new Rectangle(30, 30, 120, 120)); region.Union(pathTop); region.Exclude(pathLeft); region.Union(pathBottom); region.Exclude(pathRight); pathLeft.Dispose(); pathRight.Dispose(); pathTop.Dispose(); pathBottom.Dispose(); Bitmap bitmap = new Bitmap(180, 180); Graphics graphics = Graphics.FromImage(bitmap); graphics.IntersectClip(region); region.Dispose(); graphics.FillRectangle(Brushes.Black, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); graphics.Dispose(); bitmap.Save(@"画像を保存するパスを指定する"); bitmap.Dispose(); } |
ピースを生成する
ピースは辺や角の部分でなければ出っ張っている部分とへこんでいる部分がそれぞれ向かい合っています。そこで以下のような列挙体を定義します。
1 2 3 4 5 6 |
public enum Unevenness { Convex, // 凸型 Concave, // 凹型 None, } |
GraphicsPath.AddEllipseメソッドにRectangleオブジェクトを渡すのですが、左上の座標と辺の長さよりも円の中心と半径で指定したほうがやりやすいので、円の中心と半径を渡すとRectangleオブジェクトを返すメソッドを定義します。
1 2 3 4 5 6 7 |
public partial class Form1 : Form { Rectangle GetEllipse(int centerX, int centerY, int radius) { return new Rectangle(centerX- radius, centerY - radius, radius * 2, radius * 2); } } |
上下左右が凸なのか凹なのか直線なのかを指定するとRegionを取得できるようにメソッドを定義します。最後の引数は境界線分の1ピクセルを取得するのかどうかを指定するためのものです。
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 |
public partial class Form1 : Form { public const int PieceSize = 80; // 引数は左、上、右、下の順 Region GetPieceRegion(Unevenness lu, Unevenness tu, Unevenness ru, Unevenness bu, bool containContour) { int left = PieceSize / 4, top = PieceSize / 4; int lineWidth = 0; if (!containContour) lineWidth = 1; GraphicsPath pathLeft = new GraphicsPath(); Rectangle rectLeft = GetEllipse(left, top + PieceSize / 2, PieceSize / 4 - lineWidth); pathLeft.AddEllipse(rectLeft); GraphicsPath pathRight = new GraphicsPath(); Rectangle rectRight = GetEllipse(left + PieceSize, top + PieceSize / 2, PieceSize / 4 - lineWidth); pathRight.AddEllipse(rectRight); GraphicsPath pathTop = new GraphicsPath(); Rectangle rectTop = GetEllipse(left + PieceSize / 2, top, PieceSize / 4 - lineWidth); pathTop.AddEllipse(rectTop); GraphicsPath pathBottom = new GraphicsPath(); Rectangle rectBottom = GetEllipse(left + PieceSize / 2, top + PieceSize, PieceSize / 4 - lineWidth); pathBottom.AddEllipse(rectBottom); Region region = new Region(new Rectangle(left + lineWidth, top + lineWidth, PieceSize - lineWidth * 2, PieceSize - lineWidth * 2)); if (tu == Unevenness.Convex) region.Union(pathTop); if (tu == Unevenness.Concave) region.Exclude(pathTop); if (ru == Unevenness.Convex) region.Union(pathRight); if (ru == Unevenness.Concave) region.Exclude(pathRight); if (bu == Unevenness.Convex) region.Union(pathBottom); if (bu == Unevenness.Concave) region.Exclude(pathBottom); if (lu == Unevenness.Convex) region.Union(pathLeft); if (lu == Unevenness.Concave) region.Exclude(pathLeft); pathLeft.Dispose(); pathRight.Dispose(); pathTop.Dispose(); pathBottom.Dispose(); return region; } } |
もとの画像を指定したらそれをもとにピースに描画される画像を返すメソッドを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public partial class Form1 : Form { Bitmap GetPiece(Bitmap sourceImage, int left, int top, Unevenness lu, Unevenness tu, Unevenness ru, Unevenness bu) { Region region = GetPieceRegion(lu, tu, ru, bu, true); int size = (int)(PieceSize * 1.5); Bitmap bitmap = new Bitmap(size, size); Graphics graphics = Graphics.FromImage(bitmap); graphics.IntersectClip(region); region.Dispose(); graphics.DrawImage(sourceImage, new Point(-left + PieceSize / 4, -top + PieceSize / 4)); graphics.Dispose(); return bitmap; } } |
ピースの輪郭部分の画像を返すメソッドを指定します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { Bitmap GetOutline(Unevenness lu, Unevenness tu, Unevenness ru, Unevenness bu) { int size = (int)(PieceSize * 1.5); Bitmap bitmap = new Bitmap(size, size); Graphics graphics = Graphics.FromImage(bitmap); Region region1 = GetPieceRegion(lu, tu, ru, bu, true); Region region2 = GetPieceRegion(lu, tu, ru, bu, false); graphics.IntersectClip(region1); graphics.ExcludeClip(region2); region1.Dispose(); region2.Dispose(); graphics.FillRectangle(Brushes.White, new Rectangle(0, 0, bitmap.Width, bitmap.Height)); graphics.Dispose(); return bitmap; } } |
ピースはドラッグで移動できるようにすることにします。そこでピースを描画するためのクラスを定義します。
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 |
public class Piece { // 本来の位置 public int OriginalRow = 0; public int OriginalCol = 0; // 現在の位置 public int Row = 0; public int Col = 0; public int Angle = 0; // 難易度を上げるために回転の要素もいれる Bitmap Image0; Bitmap Image90; Bitmap Image180; Bitmap Image270; Bitmap Outline0; Bitmap Outline90; Bitmap Outline180; Bitmap Outline270; public Piece(int row, int col, Bitmap image, Bitmap outline) { OriginalRow = Row = row; OriginalCol = Col = col; Outline = outline; Image = image; Image90 = new Bitmap(image); Image180 = new Bitmap(image); Image270 = new Bitmap(image); Image90.RotateFlip(RotateFlipType.Rotate90FlipNone); Image180.RotateFlip(RotateFlipType.Rotate180FlipNone); Image270.RotateFlip(RotateFlipType.Rotate270FlipNone); Outline90 = new Bitmap(outline); Outline180 = new Bitmap(outline); Outline270 = new Bitmap(outline); Outline90.RotateFlip(RotateFlipType.Rotate90FlipNone); Outline180.RotateFlip(RotateFlipType.Rotate180FlipNone); Outline270.RotateFlip(RotateFlipType.Rotate270FlipNone); } } |
ImageプロパティとOutlineプロパティはピースに描画される画像とピースの輪郭部分のイメージを返します。
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 |
public class Piece { public Bitmap Image { get { Bitmap image = Image0; Angle = Angle % 360; if (Angle == 90) image = Image90; if (Angle == 180) image = Image180; if (Angle == 270) image = Image270; return image; } } public Bitmap Outline { get { Bitmap image = Outline0; Angle = Angle % 360; if (Angle == 90) image = Outline90; if (Angle == 180) image = Outline180; if (Angle == 270) image = Outline270; return image; } } } |
DrawImageメソッドとDrawOutlineメソッドはピースの内部と輪郭部分を描画するためのものです。Form1.MarginLeftとForm1.MarginTopは左上に描画されるピースとフォームの余白です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { public const int MarginLeft = 20; public const int MarginTop = 40; } public class Piece { public void DrawImage(Graphics graphics) { int x = Col * Form1.PieceSize - (int)(0.25 * Form1.PieceSize) + Form1.MarginLeft; int y = Row * Form1.PieceSize - (int)(0.25 * Form1.PieceSize) + Form1.MarginTop; graphics.DrawImage(Image, new Point(x, y)); } public void DrawOutline(Graphics graphics) { int x = Col * Form1.PieceSize - (int)(0.25 * Form1.PieceSize) + Form1.MarginLeft; int y = Row * Form1.PieceSize - (int)(0.25 * Form1.PieceSize) + Form1.MarginTop; graphics.DrawImage(Outline, new Point(x, y)); } } |
IsClickedメソッドは引数として渡された座標がピースの内部かどうかを調べます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
public class Piece { public bool IsClicked(Point point) { int x = Col * Form1.PieceSize + Form1.MarginLeft; int y = Row * Form1.PieceSize + Form1.MarginTop; if (point.X < x) return false; if (point.Y < y) return false; if (point.X > x + Form1.PieceSize) return false; if (point.Y > y + Form1.PieceSize) return false; return true; } } |
ピースを描画する
フォームにボタンとチェックボックスを配置します。ボタンはピースをシャッフルするためのものです。チェックボックスはピースの輪郭部分を描画するかどうかを設定するためのものです。
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 { List<Piece> Pieces = new List<Piece>(); public const int PieceSize = 80; public const int MarginLeft = 20; public const int MarginTop = 40; public Form1() { InitializeComponent(); CreatePieces(); checkBox1.Checked = true; this.DoubleBuffered = true; // 再描画時のちらつきを防ぐ } void CreatePieces() { Bitmap sourceImage = Properties.Resources.cat; Pieces.Clear(); int rowMax = 480 / PieceSize; int colMax = 640 / PieceSize; for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) { Unevenness tu = (row + col) % 2 == 0 ? Unevenness.Convex : Unevenness.Concave; Unevenness bu = tu; Unevenness lu = (row + col) % 2 == 0 ? Unevenness.Concave : Unevenness.Convex; Unevenness ru = lu; if (row == 0) tu = Unevenness.None; if (col == 0) lu = Unevenness.None; if (row == rowMax - 1) bu = Unevenness.None; if (col == colMax - 1) ru = Unevenness.None; Bitmap image = GetPiece(sourceImage, PieceSize * col, PieceSize * row, lu, tu, ru, bu); Bitmap outline = GetOutline(lu, tu, ru, bu); Pieces.Add(new Piece(row, col, image, outline)); } } } } |
ピースを描画する処理を示します。
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 |
public partial class Form1 : Form { bool drawOutline = false; // ドラッグされて移動しているピースとその座標 Piece DraggedPiece = null; Point DraggedPiecePosition = new Point(); protected override void OnPaint(PaintEventArgs e) { foreach (Piece piece in Pieces) { // ドラッグされて移動しているピースはもとの位置には描画しない if (DraggedPiece == piece) continue; piece.DrawImage(e.Graphics); if (drawOutline) piece.DrawOutline(e.Graphics); } if (DraggedPiece != null) e.Graphics.DrawImage(DraggedPiece.Image, DraggedPiecePosition); base.OnPaint(e); } } |
描画するとこんな感じになります。
チェックボックスのONOFFが切り替わったら輪郭部分の描画を有効または無効にします。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { private void checkBox1_CheckedChanged(object sender, EventArgs e) { drawOutline = checkBox1.Checked; Invalidate(); } } |
ピースをシャッフルする
ボタンがクリックされたらピースをシャッフルします。
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 |
public partial class Form1 : Form { private void button1_Click(object sender, EventArgs e) { int colMax = Pieces.Max(_ => _.Col) + 1; int rowMax = Pieces.Max(_ => _.Row) + 1; List<Piece> list = new List<Piece>(Pieces); Random random = new Random(); // 各位置に配置されるピースを未選択のもののなかから選ぶ // 回転要素は入れていない。現状、難しくなりすぎる for (int row = 0; row < rowMax; row++) { for (int col = 0; col < colMax; col++) { int r = random.Next(list.Count); list[r].Row = row; list[r].Col = col; list.RemoveAt(r); } } Invalidate(); } } |
ピースを移動させる
フォームがクリックされたら対応するピースが存在するか調べます。見つかった場合はフィールド変数内に格納します。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { protected override void OnMouseDown(MouseEventArgs e) { DraggedPiece = Pieces.FirstOrDefault(_ => _.IsClicked(new Point(e.X, e.Y))); base.OnMouseDown(e); } } |
DraggedPieceがnullではないときにマウスが移動したらマウスポインタの座標にピースの中央がくるように移動中のピースを描画できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 |
public partial class Form1 : Form { protected override void OnMouseMove(MouseEventArgs e) { if (DraggedPiece != null) { DraggedPiecePosition = new Point(e.X - PieceSize * 3 / 4, e.Y - PieceSize * 3 / 4); Invalidate(); } base.OnMouseMove(e); } } |
ピースの移動処理が行なわれているときにマウスボタンが離されたら、その座標に別のピースがあるか調べます。ある場合はこれまでドラッグされていたピースと位置を入れ替えます。そしてDraggedPieceをnullにして再描画をおこないます。
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 |
public partial class Form1 : Form { protected override void OnMouseUp(MouseEventArgs e) { Piece piece = Pieces.FirstOrDefault(_ => _.IsClicked(new Point(e.X, e.Y))); if (piece != null && DraggedPiece != null) { if (piece != DraggedPiece) { // 位置を入れ替える int row1 = DraggedPiece.Row; int col1 = DraggedPiece.Col; DraggedPiece.Row = piece.Row; DraggedPiece.Col = piece.Col; piece.Row = row1; piece.Col = col1; } } DraggedPiece = null; Invalidate(); base.OnMouseDown(e); } } |
課題
やってみると意外に難しいです。隣にあるピースと出っ張っている部分が重なっている場合、必要なピースを見つけにくくなっています。ゲームとして成立させるためには組み合わせていないピースの描画位置を変えるなどの工夫が必要です。