C# – Viết chương trình ghép hình – P2

Trong phần 1 tôi đã giới thiệu với các bạn cách thực hiện một trò chơi ghép ảnh bằng cách di chuyển các khung ảnh theo 4 hướng chỉ trong một panel. Nếu bạn là một người có đòi hỏi cao sẽ cảm thấy cách chơi này khá nhàm chán. Vì thế trong bài này tôi sẽ hướng dẫn cách làm một chương trình ghép hình tương tự, nhưng cách chơi sẽ có một chút thay đổi (như đã nói ở phần trước): thay vì dùng các mũi tên di chuyển khung ảnh ta sẽ dùng chuột di chuyển khung ảnh đến các vị trí tùy ý trên form.

Cuối bài viết tôi sẽ cung cấp mã nguồn và chương trình demo của cả 2 ví dụ này.

I)      Phân tích

Trước tiên khi load file ảnh, chương trình sẽ sắp xếp các khung ảnh nhỏ vào vị trí bên phải form, đồng thời gọi phần phương thức xáo trộn ảnh (hình bên dưới)

Nhiệm vụ của người chơi là kéo thả các khung ảnh này vào panel bên trái theo đúng vị trí. Việc kéo thả ảnh cần chú ý hai vấn đề:

-          Một là khi ảnh được thả vào trong panel bên trái, chương trình sẽ tự động canh vị trí của khung ảnh đó khít với lưới được vẽ.

-          Hai là khi 2 khung ảnh được xếp chồng lên nhau trong panel, chương trình sẽ hoán đổi vị trí khung ảnh phía dưới về vị trí cũ của khung ảnh phía trên.

I)     Thiết kế

XepHinh2

1)      Các thuộc tính

private Bitmap ImageFile;

private const int CELL_SIZE = 60;

private const int PIECE_COUNT = 4;

private PictureBox[] picCell = new PictureBox[PIECE_COUNT * PIECE_COUNT];

// tọa độ để xác định vị trí các khung ảnh sẽ tạo ra

private const int OffSetX=300;

private const int OffSetY=10;

// Vị trí chuột trên khung ảnh khi bắt đầu khi bắt đầu drag

// Dùng để điều chỉnh vị trí chuột luôn tương đối với khung ảnh

private Point startDragPoint;

// Vị trí khung ảnh gốc khi bắt đầu drag

// dùng để hoán đổi vị trí ảnh nếu 2 ảnh mới đè lên ảnh cũ

private Point picLocation;

Trong danh sách thuộc tính này có vài “nhân vật” mới cần quan tâm, bạn hãy đọc chú thích mô tả của chúng để xem chúng có vai trò gì. Nếu chưa hiểu ngay được thì bạn cũng không cần lo lắng, hãy bỏ qua vì phần sau tôi sẽ giải thích kĩ hơn.

2)      Khởi tạo Form

Trong chương trình này chúng ta sẽ làm công việc khởi tạo mảng các PictureBox như trong phần 1, ngoài ra có một điểm khác biệt là chúng ta sẽ thêm sự kiện cho các PictureBox này để phục vụ cho chức năng kéo thả. Các sự kiện này là MouseUp, MouseMoveMouseDown.

public FormGame2()

{

InitializeComponent();

for (int i = 0; i < picCell.Length; i++)

{

picCell[i] = new PictureBox();

picCell[i].MouseUp += new MouseEventHandler(picCell_MouseUp);

picCell[i].MouseMove += new MouseEventHandler(picCell_MouseMove);

picCell[i].MouseDown += new MouseEventHandler(picCell_MouseDown);

panel1.Controls.Add(picCell[i]);

}

}

Dĩ nhiên bạn có thể thêm các sự kiện này sau khi viết ra các đoạn code xử lý sự kiện. Hãy nhớ ba phương thức chúng ta thêm vào cho các picCell ở trên, bạn sẽ phải viết chúng ra ở phía dưới.

3)      Vẽ lưới cho Panel

Trên hình minh họa bạn có thể panel của chúng ta có các đường kẻ ngang dọc, chúng được vẽ ra bởi các phương thức đồ họa của lớp Graphics, cụ thể là DrawLine. Bạn có thể xem ví dụ của phương thức vẽ lưới sau đây:

void DrawGrid(object obj, PaintEventArgs pe)

{

Graphics g = panelImage.CreateGraphics();

Pen p = Pens.White;

int length = CELL_SIZE * PIECE_COUNT;

for (int i = 0; i <= PIECE_COUNT; i++)

{

int pos = CELL_SIZE * i;

// Vẽ lưới ngang

Point p1 = new Point(0,pos);

Point p2 = new Point(length,pos);

g.DrawLine(p, p1, p2);

// Vẽ lưới dọc

p1 = new Point(pos,0);

p2 = new Point(pos,length);

g.DrawLine(p, p1, p2);

}

}

Câu lệnh đầu tiên panelImage.CreateGraphics() sẽ tạo ra một đối tượng Graphisc của panelImage, mọi phương thức đồ họa chúng ta cần gọi đều nằm trong đối tượng Graphics này, và các phương thức này cũng chỉ có tác dụng trên panel đã tạo ra nó.

-Vẽ lưới: Chúng ta sẽ dùng một vòng lặp để vẽ lưới ngang dọc cho panel, sẽ không quá khó để hình dung ra cách làm việc của đoạn mã này. Bạn có thể quan tâm đến 2 phép tính dưới đây và kết quả mà nó trả về:

CELL_SIZE * i

// i đại diện cho dòng/cột, kết quả là vị trí của dòng/cột hiện tại

CELL_SIZE * PIECE_COUNT

// chiều rộng/dài của panel

Để tối ưu vòng đoạn mã, bạn không nên tính lại nhiều lần 2 phép tính trên trong vòng lặp, chỉ cần tính 1 lần duy nhất rồi sử dụng cho những lần sau.

Phương thức DrawLine tôi sử dụng yêu cầu 3 tham số:

-Một đối tượng Pen quy định nét vẽ về màu sắc, kích thước,…khi bạn tạo đối tượng Pen như câu lệnh trên, kích thước nét vẽ mặc định sẽ là 1.

-Hai đối tượng Point xác định vị trí điểm đầu và điểm cuối của đường thẳng.

Bạn có thể thắc mắc về hai tham số truyền vào cho phương thức, và nếu để ý thì bạn sẽ biết đây là một phương thức xử lý cho sự kiện Paint của một control nào đó (ở đây thì đó là panel mà ta đang xét).

Bởi vì bạn không thể chỉ vẽ đường kẻ lưới cho panel tại thời điểm ban đầu. Các đường kẻ này sẽ bị xóa đi sau khi có một control nào đó che mất panel, và khi hiển thị lại, panel sẽ được gọi lại sự kiện paint của nó để vẽ lại. Tất nhiên khi đó panel sẽ không mất công sức vẽ lại các đường kẻ mà bạn đã tạo ra lúc đầu. Chính vì thế bạn cần phải can thiệp và ”ra lệnh” cho panel phải vẽ lại những gì cần thiết.

Bạn có thể dùng IDE để tạo ra tự động phương thức cho sự kiện Paint này, nếu viết thủ công như trên bạn hãy thêm dòng lệnh này vào hàm khởi tạo của Form:

// Thêm sự kiện vẽ lưới vào sự kiện Paint của panel Image

panelImage.Paint+=new PaintEventHandler(DrawGrid);

4)      Nạp ảnh

Phương thức LoadPicture(string fileName) của chúng ta sẽ nạp một ảnh từ đường dẫn file, tạo ra các khung ảnh, sau đó tiến hành trộn các khung ảnh lại.

private void LoadPicture(string fileName)

{

if (!System.IO.File.Exists(fileName))

{

MessageBox.Show(“Không tìm thấy file \n” + fileName);

return;

}

try

{

ImageFile = new Bitmap(Bitmap.FromFile(fileName), panelImage.Size);

int imageSize = panelImage.Width;

imageSize = imageSize – imageSize % PIECE_COUNT;

// Tạo và sắp xếp các khung ảnh

InitPictures();

// Xáo trộn các khung ảnh

mnShuffle_Click(null, null);

}

catch (Exception ex)

{

MessageBox.Show(ex.ToString());

}

}

Vì panelImage của chúng ta có hình vuông và kích thước xác định, bạn không cần viết các chuỗi lệnh để tính toán chiều dài hay hệ số co giãn gì cả. Việc tạo một đối tượng Bitmap bằng hàm khởi tạo như trên sẽ khiến ảnh Bitmap tự canh chỉnh kích thước theo panelImage.

Thật đáng tiếc là điều này sẽ gây ra chút rắc rối nếu như ảnh bạn truyền vào có chiều rộng và cao quá chênh lệnh. Tuy nhiên hãy tạm bỏ qua điều này vì bạn đã được hướng dẫn cách xử lý trong phần 1.

Chúng ta sẽ tiếp tục với phương thức tạo và sắp xếp các khung ảnh.

5)      Phương thức InitPictures()

private void InitPictures()

{

if (ImageFile != null)

{

int index = 0;

for (int j = 0; j < PIECE_COUNT; j++)

{

int posY = j * CELL_SIZE;

for (int i = 0; i < PIECE_COUNT; i++)

{

try

{

Rectangle imageRect = new Rectangle(i * CELL_SIZE, posY, CELL_SIZE, CELL_SIZE);

picCell[index].Image = null;

picCell[index].Image = ImageFile.Clone(imageRect, PixelFormat.DontCare);

picCell[index].Location = new Point(OffSetX + i * CELL_SIZE, OffSetY + posY + 1);

picCell[index].Size = new Size(CELL_SIZE, CELL_SIZE);

picCell[index].Tag = index.ToString();

index++;

}

catch (Exception)

{

}

}

}

}

mnShuffle.Enabled = true;

// Hiển thị ảnh kết quả vào picture1

pictureBox1.Image = ImageFile;

}

Không khác gì lắm so với phần trước, bạn có thể đọc sơ qua và hiểu cách làm việc của nó như thế nào. Hãy bỏ qua phần này nếu như chắc rằng bạn đã nắm được nó. Tuy nhiên như đã nói lúc đầu, bạn bắt gặp ở đây 2 thuộc tính OffSetX và OffSetY và tôi sẽ giải thích thêm một chút về chúng.

Hãy xem lại hình minh họa, bạn có thể thấy các khung ảnh được xếp ngăn nắp thành một vùng hình vuông phía bên phải form, tôi tạm gọi nó là vùng bao. Khoảng cách ngang từ tọa độ 0,0 của form đến vùng bao này là 300, và khoảng cách dọc là 10. Tức là tọa độ của vùng bao này sẽ là 300,10 tương ứng với giá trị của 2 OffSet mà ta gán lúc đầu.

Bạn có thể hình dung ra điều đó bằng cách đọc lại đoạn mã trên, vị trí các khung ảnh được cộng thêm một hằng số OffSet trên.

6)      Xáo trộn ảnh

(Xem lại hướng dẫn ở phần 1)

private void mnShuffle_Click(object sender, EventArgs e)

{

Bitmap bmp;

string temptag;

Random rnd = new Random();

int maxValue = PIECE_COUNT * PIECE_COUNT;

for (int i = 0; i < maxValue; i++)

{

int indexSource = rnd.Next(maxValue);

int indexDest = rnd.Next(maxValue);

if (indexSource == indexDest)

continue;

try

{

bmp = (Bitmap)picCell[indexSource].Image;

temptag = picCell[indexSource].Tag.ToString();

picCell[indexSource].Image = picCell[indexDest].Image;

picCell[indexSource].Tag = picCell[indexDest].Tag;

picCell[indexDest].Image = bmp;

picCell[indexDest].Tag = temptag;

}

catch (Exception)

{

}

}

}

7)      Kéo thả ảnh

Hãy hình dung những thao tác bạn thực hiện khi bạn kéo và một control nào đó. Theo thứ tự, các sự kiện xảy ra cho control sẽ như sau: MouseDown, MouseMove và MouseUp. Dựa vào đây bạn sẽ viết lệnh cho từng sự kiện.

private void picCell_MouseDown(object sender, MouseEventArgs e)

{

// Khi nhấn chuột và khung ảnh

// ta sẽ lưu vị trí nhấn chuột và vị trí khung ảnh lại

PictureBox pic=(PictureBox)sender;

startDragPoint = e.Location;

picLocation = pic.Location;

// Đưa khung ảnh lên trên cùng để ko bị che mất

pic.BringToFront();

}

Dòng đầu tiên sẽ lấy đối tượng gửi sự kiện đến và chuyển nó sang PictureBox. Bạn phải chắc rằng không thêm sự kiện này cho bất kì một control có kiểu khác PictureBox, nếu không việc ép kiểu sẽ thất bại và một Exception sẽ được ném ra.

PictureBox pic=(PictureBox)sender;

Dòng lệnh tiếp theo ta sẽ lưu lại vị trí nhấn chuột trên đối tượng pic này. Vị trí này là tương đối và có gốc tính từ điểm phía trên bên trái của khung ảnh mà ta nhấn. Khi di chuyển khung ảnh, ta sẽ sử dụng vị trí này để giữ chuột luôn nằm đúng vị trí tương đối trên khung ảnh đang kéo.

startDragPoint = e.Location;

Tiếp theo chúng ta sẽ lưu lại vị trí của khung ảnh để hoán đổi với khung ảnh khác trong trường hợp chúng đè lên nhau.

Đây là phần mã lệnh cho sự kiện MouseMove và MouseUp, các dòng lệnh đã được chú thích nên có lẽ cũng không cần giải thích thêm.

private void picCell_MouseMove(object sender, MouseEventArgs e)

{

PictureBox pic = (PictureBox)sender;

// Di chuyển khung ảnh theo chuột khi kéo

if (e.Button == MouseButtons.Left)

{

pic.Location = new Point(pic.Left + e.X – startDragPoint.X,

pic.Top + e.Y – startDragPoint.Y);

}

// Tính vị trí dòng và cột với đơn vị là 1 CELL_SIZE

int col = pic.Location.X / CELL_SIZE;

int row = pic.Location.Y / CELL_SIZE;

// Nếu nằm ngoài panel Image thì thoát hàm

if (col >= PIECE_COUNT || row >= PIECE_COUNT)

return;

// Vẽ đường biên màu đỏ xác định vị trí mới của ảnh trên panel

Graphics g = panelImage.CreateGraphics();

g.DrawRectangle(Pens.Red,new Rectangle(col*CELL_SIZE,row*CELL_SIZE,CELL_SIZE,CELL_SIZE));

}

private void picCell_MouseUp(object sender, MouseEventArgs e)

{

PictureBox pic = (PictureBox)sender;

// Tính vị trí mới của ảnh khít với dòng, cột trên panel

int col = pic.Location.X / CELL_SIZE;

int row = pic.Location.Y / CELL_SIZE;

if (col >= PIECE_COUNT || row >= PIECE_COUNT)

return;

// Lấy control tại ví trí mới

Control ctl = panel1.GetChildAtPoint(new Point(col * CELL_SIZE, row * CELL_SIZE));

// Nếu đã có một khung ảnh tại ô này

// thì chuyển vị trí của khung ảnh này về vị trí của khung ảnh vừa drop

if (ctl != null && ctl is PictureBox)

{

ctl.Location = picLocation;

}

// Gán vị trí mới cho khung ảnh

pic.Location = new Point(col * CELL_SIZE, row * CELL_SIZE);

if (CheckWin)

{

MessageBox.Show(“Finish!”);

}

}

I)     Phần kết

Chương trình của chúng ta đã hoàn thành mặc dù còn khá đơn giản. Bạn có thể tải chương trình demo và mã nguồn bên dưới. Chúc thành công!

GhepHinh_source.rar (117 KB)

GhepHinh_demo.rar (35 KB)

http://yinyang-it.tk

1/7/2009


About these ads

20 thoughts on “C# – Viết chương trình ghép hình – P2

  1. Bạn có thể thấy là phương thức Clone của đối tượng bitmap nhận vào 1 tham số kiểu Rectangle quy định kích thước ảnh sẽ cắt theo hình chữ nhật. Việc cắt theo các hình dạng khác có thể rất phức tạp chưa kể đến thuật toán để sắp xếp chúng. Vấn đề này bạn có thể tham khảo các đề tài về GDI hoặc DirectX

    Trả lời
  2. Ý bạn có phải là hoán vị 2 phần tử trong mảng (bất kì). Giả sử mảng có tên là arr Như ví dụ của bạn thì phần tử mang giá trị 4 là arr[0,1] vaf 2 là arr[1,1]. Bạn chỉ cần làm phép hoán vị đơn giản thông qua một biến trung gian.

    int tmp = arr[0,1];
    arr[0,1] = arr[1,1];
    arr[1,1] = tmp;

    Trả lời
  3. Em đang làm game kiểu như tangram nhưng đang bí phần kiểm tra người chơi đã xếp hình đúng như thế nào?. Theo em nghĩ thì em sẽ kiểm tra các hình con nằm trong hình cần xếp và chúng không bị chồng đè lên nhau thì là xếp đúng nhưng em tìm mà không thấy có hàm nào trong C# hỗ trợ việc kiểm tra này. Việc kiểm tra trong trò này không thể áp dụng cách kiểm tra trong trò xếp hình mà anh làm đc vì nó có thể có nhiều cách xếp cho 1 hình. Anh có thể giúp em không ạ? Thanks anh nhiều.

    Trả lời
  4. Mình nghĩ là việc kiểm tra hoàn tất không cần thiết trong trò chơi mà bạn nói. Bởi vì cách xếp là tự do nên người dùng sẽ tự xác định khi nào nó hoàn thành (khi đó họ có thể print, save, cancel…). Nếu bạn sử dụng lớp GraphicsPath thì sẽ đơn giản hóa được nhiều việc và có thể giải quyết được các vấn đề của bạn.

    Trả lời

Gửi phản hồi

Mời bạn điền thông tin vào ô dưới đây hoặc kích vào một biểu tượng để đăng nhập:

WordPress.com Logo

Bạn đang bình luận bằng tài khoản WordPress.com Log Out / Thay đổi )

Twitter picture

Bạn đang bình luận bằng tài khoản Twitter Log Out / Thay đổi )

Facebook photo

Bạn đang bình luận bằng tài khoản Facebook Log Out / Thay đổi )

Google+ photo

Bạn đang bình luận bằng tài khoản Google+ Log Out / Thay đổi )

Connecting to %s