TròchơixếphìnhvớiVB6 Chương trình sẽ cắt một hình cho trước ra thành nhiều mảnh có hình dạng ngẫu nhiên, và người chơi sẽ phải ráp từng mảnh lại. Tròchơi thật đơn giản nhưng lập trình để tạo tròchơi này không đơn giản chút nào. Bài viết giới thiệu một chương trình như vậy được thực hiện với VB6. Tạo Activex control Tạo một Standard Project mới. Vào menu Project/Add Usercontrol để thêm một Usercontrol mới (đặt tên tùy thích, ở đây tôi đặt là ShapeControl, AutoRedraw=True). Để tạo ShapeControl (SC) có hình dạng đặc biệt cần dùng 4 hàm API: CreateRectRgn, CreateEllipseticRgn, CombineRgn và SetWindowRgn. Khai báo các hàm trên trong SC. Khai báo thêm hàm DeleteObject dùng để hủy đối tượng đã tạo để giải phóng bộ nhớ. Để thuận tiện cho việc "tạo hình" cho SC, ta sử dụng cấu trúc để lưu trữ dữ liệu các cạnh: Private Type CauTruc Top As Long Bottom As Long Left As Long Right As Long End Type Function sau tạo hình cho SC: Private Function CreateFormRegion(ScaleX As Single, ScaleY As Single, OffsetX As Integer, OffsetY As Integer, DrawStyle As CauTruc) As Long Hình chữ nhật chính có tọa độ (22,22)-(77,77), hình ellipse có bán kính lớn=22 và bán kính nhỏ=13 (H.1). Do hình ảnh chúng ta muốn cắt có chiều rộng và chiều dài bất kì nên ta phải nhân tỉ lệ này cho chiều dài, rộng thực của mỗi miếng hình nhỏ (bằng với chiều dài và chiều rộng của SC, do SC sẽ là mỗi miếng hình nhỏ). Các giá trị Top, Left, Right, Bottom trong CauTruc có thể nhận các giá trị -1, 0, 1. Nếu Top nhận giá trị 1 có nghĩa là hình chữ nhật sẽ kết hợp vớihình ellipse (H.2), nếu nhận giá trị 0 nghĩa là không có hình ellipse, còn giá trị -1 thì ellipse sẽ cắt hình chữ nhật (H.3) (tương tự cho Left, Right và Bottom). Bạn sẽ thấy cách qui định giá trị này rất hữu ích trong các bước sau. Sở dĩ khai báo Function CreateFormRegion là Private vì nếu bạn chuyển qua Public thì khi chạy chương trình, VB sẽ báo lỗi là kiểu người dùng định nghĩa (CauTruc) không được làm đối số (chỉ khi nào bạn tạo ActiveX Control riêng và biên dịch thành *.ocx mới không gặp lỗi này). Do đó ta phải tạo 1 Sub có tính Public gọi Function này. Sub này sẽ trở thành một Method của SC. Public Sub DrawShape(vLeft As Long, vTop As Long, vRight As Long, vBottom As Long) Dim DrawStyle As CauTruc Dim nRet As Long DrawStyle.Left = vLeft DrawStyle.Top = vTop DrawStyle.Right = vRight DrawStyle.Bottom = vBottom nRet = SetWindowRgn(UserControl.hwnd, CreateFormRegion(1, 1, 0, 0, DrawStyle), True) End Sub Bây giờ thêm các Property phục vụ cho việc đồ họa: Public Property get hWnd() as Long hWnd=Usercontrol.hWnd End Property Public Property get hDC() as Long hDC=Usercontrol.hDC End Property Để di chuyển SC (không có TitleBar) dễ dàng, bạn phải "capture" chuột và mô phỏng việc nhấn và rê chuột trái (khai báo thêm 2 API là ReleaseCapture và SendMessage ở phần khai báo các hàm API): Private Sub UserControl_MouseDown(Button As Integer, Shift As Integer, X As Single, Y As Single) ReleaseCapture SendMessage UserControl.hwnd, &HA1, 2, 0& End Sub Ngoài ra, để có thêm nhiều sự kiện (Event) như MouseMove, MouseUp, Resize, . bạn khai báo các sự kiện này ở đầu code: Public <Tên sự kiện> (Các đối số) Và khi UserControl xảy ra sự kiện nào thì bạn báo hiệu (RaiseEvent) sự kiện đó Phần Form Vấn đề kế tiếp là phải tạo các SC sao cho hợp lí vì các cạnh của SC (hay nói chính xác là các hình ellipse) mang các giá trị ngẫu nhiên (nếu không, các SC sẽ có cùng hình dạng).Trước hết tạo một hàm trả về giá trị ngẫu nhiên cho các cạnh: Private Function NgauNhien() As Long Randomize If Rnd < 0.3333 Then NgauNhien = -1 ElseIf Rnd > 0.3333 And Rnd < 0.6666 Then NgauNhien = 0 Else NgauNhien = 1 End If End Function Hàm Rnd sẽ trả về một con số trong khoảng giá trị [0-1] và Randomize sẽ sinh một con số mới phục vụ cho việc lấy Rnd. Nếu không dùng Randomize thì mỗi lần form Load, Rnd sẽ trả về các con số y hệt cũ. Trở lại vấn đề tạo các cạnh sao cho hợp lí. Mời bạn xem hình 4. Ở đây tôi lần lượt đánh dấu các hình theo thứ tự Trái-Đỉnh-Phải-Đáy. Theo đó, hai SC xếp trên cùng hàng sẽ có Phải trước+Trái sau=0. Hai SC trên cùng cột sẽ có Đáy trên+Đỉnh dưới=0. Cách thuận tiện nhất là lưu các dữ liệu này vào mảng 2 chiều cấu trúc (mỗi phần tử mảng là 1 cấu trúc theo kiểu CauTruc), sau đó tạo các SC theo các phần tử này. Chúng ta khai báo lại kiểu CauTruc Private Type CauTruc Top as Long Bottom as Long Left as Long Right as Long End Type Dim a(100,100) As CauTruc 'mảng có tối đa 101*101 phần tử Sau cùng là chia hình ra thành nhiều mảnh nhỏ (thực chất mỗi mảnh nhỏ là một SC) và dùng hàm BitBlt để copy vùng ảnh bên ảnh đích (ảnh cần cắt) qua SC. Private Declare Function BitBlt Lib "gdi32" (ByVal hDestDC As Long, ByVal X As Long, ByVal Y As Long, ByVal nWidth As Long, ByVal nHeight As Long, ByVal hSrcDC As Long, ByVal xSrc As Long, ByVal ySrc As Long, ByVal dwRop As Long) As Long Private Const SRCCOPY = &HCC0020 - hDestDC là hDC của thiết bị nhận khối hình, X là hoành độ trên trái điểm bắt đầu nhận khối hình, Y là tung độ trên trái điểm bắt đầu nhận khối hình, nWidth là chiều rộng khối hình, nHeight là chiều cao khối hình (tất cả các đối số này là của SC). - hSrcDC là hDC của hình gốc, xSrc là hoành độ trên trái của khối hình truyền đi, ySrc là tung độ trên trái của khối hình truyền đi (của hình đích, cụ thể là một PictureBox). - dwRop là cờ xác định hoạt động quét, ở đây là chỉ cần ki ểu quét y nguyên như cũ (SRCCOPY). Sub sau sẽ sinh ra các mảnh hình bất kì: Private Sub SinhHinh(ByVal wCount As Integer, ByVal hCount As Integer) wCount là số mảnh muốn cắt theo chiều ngang, hCount là số mảnh muốn cắt theo chiều dọc. picPicture là một PictureBox chứa hình đích. Nếu bạn nghĩ muốn chia tấm hình ra thành wCount*hCount mảnh, mỗi mảnh(hay SC control) có chiều dài = chiều dài tấm hình/wCount và chiều rộng = chiều rộng tấm hình/hCount, thì kết quả bạn đạt được sẽ không như ý muốn. Phần chúng ta nhìn thấy được sau khi SC đã tạo hình chỉ là hình chữ nhật trong cùng (= 5/9 chiều dài của SC) và các hình ellipse (nếu có). Do đó mỗi SC sẽ có chiều dài (hoặc rộng) thực = (9/5*chiều dài tấm hình)/wCount (hoặc /hCount). Vì ta không biết người sử dụng sẽ cắt ra bao nhiêu mảnh nên đầu tiên phải đưa lên form một SC và đặt index=0. Sau đó sẽ nạp các SC còn lại tương ứng với số mảnh. Hàm ChieuDai(sLen) sẽ trả về 0 nếu sLen<=0 (cụ thể ở đây là =-1,0), trường hợp khác trả về 1. Private Function ChieuDai(ByVal sLen) As Integer If sLen <= 0 Then ChieuDai = 0 Else ChieuDai = 1 End If End Function Sở dĩ có hàm ChieuDai này là vì khi copy mảnh hình từ picPicture ta không biết đưa khối hình vào SC ở tọa độ X nào cho hợp lí (vì hình ellipse trái khi có khi không do tạo ngẫu nhiên), do đó tọa độ X ở đây sẽ là tọa độ X của hình chữ nhật trong cùng (2/9*uW) trừ chiều dài hình ellipse trái (nếu có, cũng =2/9*uW), tương tự cho tọa độ Y của SC, tọa độ X,Y của picPicture. Còn chiều dài và cao phụ thuộc vào ellipse phải, trái, đỉnh, đáy, nên ngoài kích thước của hình chữ nhật trong cùng (5/9*uW) phải cộng thêm chiều dài các ellipse (nếu có). Bây giờ tròchơi cơ bản đã hoàn tất. Việc còn lại là thiết kế giao diện tùy vào thẩm mỹ của mỗi người. Điều căn bản của tròchơi này là bạn phải có một khu vực chính dành cho các SC để tạo lại hình, một PictureBox của hình nguồn làm nhiệm vụ hướng dẫn người chơi (hình này nhỏ hơn hình gốc) và một vùng chứa các hình đã cắt. THỦ TỤC SINH RA CÁC MẢNH HÌNH BẤT KÌ: Private Sub SinhHinh(ByVal wCount As Integer, ByVal hCount As Integer) On Error Resume Next Dim i As Integer Dim j As Integer Dim uW As Long Dim uH As Long uW = (9 / 5) * (picPicture.Width / wCount) uH = (9 / 5) * (picPicture.Height / hCount) sc(0).Width = uW sc(0).Height = uH Static nCount nCount = -1 For i = 1 To wCount * hCount - 1 Load sc(i) sc(i).Visible=False Next For i = 0 To hCount - 1 For j = 0 To wCount - 1 a(i, j).Top = -a(i - 1, j).Bottom a(i, j).Left = -a(i, j - 1).Right If j = wCount - 1 Then a(i, j).Right = 0 Else a(i, j).Right = NgauNhien End If If i = hCount - 1 Then a(i, j).Bottom = 0 Else a(i, j).Bottom = NgauNhien End If Next Next For i = 0 To hCount - 1 For j = 0 To wCount - 1 nCount = nCount + 1 BitBlt sc(nCount).hDc, _ (2 / 9) * uW - (2 / 9) * uW * ChieuDai(a(i, j).Left), (2 / 9) * uH - (2 / 9) - (2 / 9) * uH * ChieuDai(a(i, j).Top), _ (5 / 9) * uW + (2 / 9) * uW * ChieuDai(a(i, j).Right) + (2 / 9) * uW * ChieuDai(a(i, j).Left), _ (5 / 9) * uH + (2 / 9) * uH * ChieuDai(a(i, j).Bottom) + (2 / 9) * uH * ChieuDai(a(i, j).Top), _ picPicture.hDc, _ (5 / 9) * uW * j - (2 / 9) * uW * ChieuDai(a(i, j).Left), _ (5 / 9) * uH * i - (2 / 9) * uH * ChieuDai(a(i, j).Top), SRCCOPY sc(nCount).DrawShape a(i, j).Left, a(i, j).Top, a(i, j).Right, a(i, j).Bottom sc(nCount).Visible = True Next j Next i End Sub Mẹo vặt 1. Cũng như bao tròchơi khác, tròchơi của chúng ta cũng phải có "cheat". Trong ShapeControl, bạn tạo một Label và đặt vào chính giữa của SC với thuộc tính Visible=False. Mỗi khi sub SinhHinh trong Form được gọi thì bạn thêm vào sc(i).Label1.caption=nCount để đánh dấu. Mỗi khi gọi cheat (do bạn quy định) thì chỉ việc set thuộc tính Visible của Label thành True. 2. Đối với những hình ảnh lớn (800x600), nếu không đủ diện tích màn hình thì bạn dùng hàm API CopyImage với thông số chiều rộng và chiều cao mới để tạo hình nhỏ hơn ban đầu cho picPicture và cũng dùng hàm này cho hình hướng dẫn. 3. Khi nạp các SC trong Sub SinhHinh thì các SC này nằm chồng lên nhau, do đó bạn phải sắp xếp lại các SC này một cách ngẫu nhiên. Để tiết kiệm không gian thì các SC này nên nằm trong một PictureBox (PB), PB này phải đủ rộng để chứa tất cả các SC bạn tạo ra và PB này phải nằm trong một Container (1 PB khác, 1 Usercontrol khác, .) kết hợp với ScrollBar. Để hình dung việc này thì bạn cứ xem thanh TaskPane (Ctrl+F1) của bộ Office và làm theo. ScrollBar sẽ xác định tọa độ Top của PB chứa các SC. Về việc sắp xếp các SC ngẫu nhiên để không gây nhàm chán, bạn tạo 2 mảng một chiều. Giả sử chúng ta có 20 SC thì bạn sinh mảng a(0 to 19) và giá trị các phần tử = trị số (a(0)=0, a(1)=1, .), sau đó đổi chỗ ngẫu nhiên các phần tử thì ta có mảng ngẫu nhiên. Mảng b(0 to 19) sẽ mang lần lượt các giá trị của mảng a() (các giá trị này chính là thứ tự của các SC, b(0)=b(a(0))=6) rồi định lại tọa độ Top và Left. Muốn đưa các SC từ PB ra ngoài vùng sắp xếp chính thì dùng hàm SetParent <hWnd cũ>,<hWnd mới>. Bạn có thể tải về mã nguồn chương trình mẫu trên website của TGVT-PCW VN Online. FUNTION ĐẢM NHIỆM VIỆC TẠO HÌNH CHO SC: Private Function CreateFormRegion(ScaleX As Single, ScaleY As Single, OffsetX As Integer, OffsetY As Integer, DrawStyle As CauTruc) As Long Dim HolderRegion As Long, ObjectRegion As Long, nRet As Long, Counter As Integer Dim uW As Long Dim uH As Long ResultRegion = CreateRectRgn(0, 0, 0, 0) HolderRegion = CreateRectRgn(0, 0, 0, 0) uW = UserControl.Width / 15 uH = UserControl.Height / 15 'Hình chữ nhật chính '22/99,22/99,77/99,77/99 ObjectRegion = CreateRectRgn((2 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (2 / 9) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, (7 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (7 / 9) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY) nRet = CombineRgn(ResultRegion, ObjectRegion, ObjectRegion, RGN_COPY) DeleteObject ObjectRegion 'Hình ellipse trái If DrawStyle.Left <> 0 Then ObjectRegion = CreateEllipseticRgn(0 * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (37 / 99) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, (4 / 9) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, (7 / 11) * uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY) nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, OffsetX, 0 * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * (7 / 11) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (4 / 9) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY) nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY) nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Top) DeleteObject ObjectRegion End If 'Hình ellipse cạnh phải If DrawStyle.Right <> 0 Then ObjectRegion = CreateEllipseticRgn(uW * (5 / 9) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (37 / 99) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (7 / 11) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY) nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY) nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Right) DeleteObject ObjectRegion End If 'Hình ellipse cạnh đáy If DrawStyle.Bottom <> 0 Then ObjectRegion = CreateEllipseticRgn(uW * (37 / 99) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * (5 / 9) * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY, uW * (7 / 11) * ScaleX * 15 / Screen.TwipsPerPixelX + OffsetX, uH * ScaleY * 15 / Screen.TwipsPerPixelY + OffsetY) RGN_COPY) nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Left) DeleteObject ObjectRegion End If 'Hình ellipse trên đỉnh If DrawStyle.Top <> 0 Then ObjectRegion = CreateEllipseticRgn((37 / 99) * uW * ScaleX * 15 / Screen.TwipsPerPixelX + nRet = CombineRgn(HolderRegion, ResultRegion, ResultRegion, RGN_COPY) nRet = CombineRgn(ResultRegion, HolderRegion, ObjectRegion, 3 - DrawStyle.Bottom) DeleteObject ObjectRegion End If DeleteObject HolderRegion CreateFormRegion = ResultRegion End Function . Trò chơi xếp hình với VB6 Chương trình sẽ cắt một hình cho trước ra thành nhiều mảnh có hình dạng ngẫu nhiên, và người chơi sẽ phải ráp. lại. Trò chơi thật đơn giản nhưng lập trình để tạo trò chơi này không đơn giản chút nào. Bài viết giới thiệu một chương trình như vậy được thực hiện với VB6.