【VB.NET】打造一个象棋魔法学校的老师——谨以此文献给象棋爱好者——编写界面和功能的最终实现
首先,回顾一下我们遇到的“旋转棋盘”的问题。当然,这个问题只是题外话,在本程序中暂时涉及不到。但实际上,只要知道我们是执红还是执黑就可以很容易的“旋转”它,基本思路是这样的:
1、判断将或帅的位置——得到我们执红还是执黑
2、旋转棋盘
对于第一个问题,解决方法很多,可以识别棋子颜色,可以在LookAll方法中填充完毕Table时判断,也可以采用判断Look方法的返回值来解决(Table(x, y) = Look(TestBmp)这里,取得是否没有执红,并用一个布尔变量表示,如果为真,则需要“旋转”)。
对于第二个问题,解决方法也不少,可以再获取Table之后,遍历表格来处理,也可在当前代码获得FEN之后,直接处理FEN字符串。这就要根据你的喜好了,但要注意——我们是决定是否把黑色当成红色去处理,而不是真正的旋转,当然你可以选择修改GO语句的参数-w 来达到你的目的。
但我解决时,都不是采用这些办法,同样这个问题还想留给你思考一下——如何仅移动程序中的一行代码来实现它。
好了,这个部分就叙述到这里——因为它只在你扩展这个应用时才被用到。下面讨论一下如何实现主窗体和它的代码:
1、如何得到象棋魔法学校程序窗体
2、如何取得当前图像
3、如何将UCCI或UCI协议的走法格式转化为用户易于识别的表示
我们将逐个解决这些问题:
1、得到象棋魔法学校程序窗体
实际上,我们最基本的要求是是获得它在屏幕上的位置,以便我们进一步获取其图像;在这里,我们再加入一些小的考虑:当需要我们的程序进行帮助时能否不要切换到我们的程序,提示用户走法能否做成在象棋魔法学校界面上出现图形化提示。其实都可以,可能不切换到我们的程序这一点需要的小技巧你不是很了解,让我们从头说起:
A、要得到窗体图像也好,要在上面画图也好我们都需要取得它的句柄——这是第一步。这里我们仿照SPY++的做法,好处还是很多的(当然,你可以用遍历窗体的方法,但是为了获取你所点击的窗体,需要一些鼠标钩子,鼠标钩子的代码可以再我的另一个博客上找到:http://blog.csdn.net/zcsor,但这里我们不使用它)。我们在窗体上绘制一个控件,我绘制了一个Panel——只要这个控件支持MouseUp事件就可以,而后在它的MouseUp事件中加入以下处理:
Private Sub Panel1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Panel1.MouseUp
'这个句柄不能用Process.MainFormHandle来获取,你可以用SPY++来查看或者也许你知道VB6(假定象棋魔法学校是用VB6开发的)在创建窗体时的一些特性
whandle = (WindowFromPoint(Cursor.Position.X, Cursor.Position.Y))
这个whandle是象棋魔法学校的窗体句柄了——如果你在Panel1上按下鼠标并拖动到象棋魔法学校的棋盘上并松开了鼠标(是不是与SPY++很相似呢),当然你应该看看注释,为什么没有用Process.GetProcessByName来取得象棋魔法学校的进程并使用其MainFormHandle来取得这个值,当然也许你还习惯于用MousePoint来代替代码中的Cursor.Pointion,但这是无关紧要的。
好了,我们用一个巧妙的方法得到了对方窗体的句柄,在取得图像之前,我想继续应用另外一个小技巧,以实现在象棋魔法学校的界面上添加一个按钮,并且让这个按钮的事件被我们的程序所处理,这样就在用户想要得到帮助时可以很方便的找到这个帮助按钮,而无需切换到我们的程序或者我们需要一个键盘钩子来定义一组快捷键来支持用户使用——但是这不直观还需要大量编码(键盘钩子的代码也可以在我另外一个博客找到,如果没有找到它们你可以在这里向我索取)。那么如何实现这个看起来挺“玄乎”的功能呢,其实很简单,用WIN32 API——SetParent就可以了,我在窗体上绘制了一个button并把它命名为ButHelp,并在Panel1.MouseUp事件处理时,加入了如下代码:
ButHelp.Location = Point.Empty
SetParent(ButHelp.Handle, whandle)
好了,再次测试你的代码,怎么样?我们的按钮神奇的跑到了其他程序的界面上——并且它在对方客户取的位置与在我们的客户区的位置完全一样——Point.Empty,不过别高兴的太早,你还应该继续修改这个按钮的大小,以使得它不遮挡窗体上对我们识别棋子有用的部分。
另外,在这个事件处理中,我还加入了其他的一些代码:
Private Sub Panel1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Panel1.MouseUp
'这个句柄不能用Process.MainFormHandle来获取,你可以用SPY++来查看或者也许你知道VB6(假定象棋魔法学校是用VB6开发的)在创建窗体时的一些特性
whandle = (WindowFromPoint(Cursor.Position.X, Cursor.Position.Y))
ButHelp.Location = Point.Empty
Dim bmp As Bitmap = My.Resources.S.Clone
eye = New Eyes(bmp, cStart, cOffset, cTestSize)
Dim fen As String = eye.LookAll(bmp)
If fen <> "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 3" Then '这个FEN字符串代表了我方执红但未开局(且未让子)时的棋盘
eye.LookAll(bmp, True)
bmp.Save("c:\s1.bmp")
MsgBox("请将 c:\s1.bmp 发送给作者", , "初始化失败")
End If
eg = New Engine("XXXX", My.Application.Info.DirectoryPath & "XXXXXXX")
SetParent(ButHelp.Handle, whandle)
wgr = Graphics.FromHwnd(whandle)
Cursor.Current = Cursors.Default
End Sub
很明显,在这个处理中,我加入了对引擎的初始化,由于我使用的这个符合UCI协议的引擎的作者并没有公开申明此引擎可以被自由使用,所以这里不便公开他。代码中用于初始化的BMP是自己制作的——因为我们需要一个普通开局棋盘来做初始化但象棋魔法学校中都是残局,所以我截取了象棋巫师的开局图像并拼接过来:)初始化完成以后,检查一下是否成功,并创建了wgr这个Graphics对象用来在象棋魔法学校界面上呈现走法。
在最后,我们只需要处理ButHelp.Click和eg.EngineBestmove事件就可以完成我们的程序了:
Private Sub ButHelp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButHelp.Click
RedrawWindow(whandle, 0, 0, 5) '重绘象棋魔法学校窗体,以使其删除上一次绘制的图形
Threading.Thread.Sleep(1000)
My.Application.DoEvents()
Dim wsource As Point '象棋魔法学校窗体当前左上角坐标——相对于屏幕坐标
Dim wsize As Size '象棋魔法学校窗体大小
Dim win32rect As RECT '象棋魔法学校窗体RECT(WIN32 API——RECT)
GetWindowRect(whandle, win32rect)
wsource = New Point(win32rect.left, win32rect.top)
wsize = New Size(win32rect.right - win32rect.left, win32rect.bottom - win32rect.top)
Dim bmp As Bitmap = New Bitmap(wsize.Width, wsize.Height) '创建存储屏幕复制的图像
Dim gr As Graphics = Graphics.FromImage(bmp)
gr.CopyFromScreen(wsource, Point.Empty, wsize) '复制屏幕内容到图像
Dim Fen As String = eye.LookAll(bmp) '图像分析
'bmp.Save("c:\s2.bmp")
If eg.EngIDInfo.idType = "uci" Then '兼容UCI协议
eg.Go("fen " & Fen, egGoStep)
ElseIf eg.EngIDInfo.idType = "ucci" Then
eg.Go("position fen " & Fen, egGoStep)
Else '兼容未知是UCCI还是UCI的协议
eg.Go("fen " & Fen, egGoStep)
eg.Go("position fen " & Fen, egGoStep)
End If
End Sub
这个处理中,需要解释的只是为什么每次都重新取象棋魔法学校窗口位置(在代码中是取得RECT),因为用户可能移动了窗体,如果不重取,则图像将是错误的。实际上代码中还应该判断象棋魔法学校的窗体是否移出了屏幕等,但是我没有写。所以我的代码在你把象棋魔法学校移出屏幕外一部分时,可能无法正确给出走法。
Private Sub eg_EngineBestmove(ByVal move As String, ByVal usermove As String) Handles eg.EngineBestmove
Dim p1 = Chessboard2Point(Mid(move, 1, 2)) '在9*10的数组中的位置
Dim p2 = Chessboard2Point(Mid(move, 3, 2))
Dim ps As Point = New Point(p1.X * cOffset.X, p1.Y * cOffset.Y)
Dim pd As Point = New Point(p2.X * cOffset.X, p2.Y * cOffset.Y)
ps.Offset(35, 35)
pd.Offset(35, 35)
wgr.DrawLine(Pens.Red, ps, pd) '绘制走棋方法,方块是起点,圆形是终点
wgr.FillRectangle(Brushes.Blue, New Rectangle(ps - New Point(10, 10), New Size(20, 20)))
wgr.FillEllipse(Brushes.Red, New Rectangle(pd - New Point(10, 10), New Size(20, 20)))
End Sub
在这个处理中,需要注意一个问题,这里所有的非引用型量,都必须采用常数表示——这个奇怪的限制来源于我们做了奇怪的事情,具体原因在这里我不想详细解释——因为与我们的编码的关系只在于你注意到这个问题就可以。这里涉及到了一个函数:Chessboard2Point,这个函数把一个UCCI标准表示的走法转化到二维数组对应位置,这种位置很有利于我们在对方程序上正确的绘制走法。它的内容如下:
Dim PointEmpty = New Point(-1, -1)
Private Function Chessboard2Point(ByVal s As String) As Point '把用UCCI协议表示的棋子位置转化为9*10数组中的位置
Try
If s.Length = 2 Then
Return New Point(Asc(Mid(s, 1, 1)) - Asc("a"), 9 - CInt(Mid(s, 2, 1)))
Else
Return PointEmpty
End If
Catch ex As Exception
Return PointEmpty
End Try
End Function
很简单不是吗?但也许你应该仔细思考一下它是如何工作的。
好了,到这里我们遇到的多数问题都解决了,我是说,程序可以跑起来了,如果让他更健壮你需要做很多工作,但是我懒我不做。
把完整的代码贴在这里,其中的引擎你可以用象眼来代替,查找象眼所在目录实际上很简单,可以用我们前面提到的Process.GetProcessByName来取得象棋魔法学校进程对象,然后里面有很多属性可以告诉你它在哪个目录里,于是我们就可以找到象眼了。
Imports System.Runtime.InteropServices
Public Class FrmHero
'刷新指定窗体
Private Declare Function RedrawWindow Lib "user32" (ByVal hwnd As IntPtr, ByVal lprcUpdate As Integer, ByVal hrgnUpdate As Integer, ByVal fuRedraw As Integer) As Integer
'重定义父窗
Private Declare Function SetParent Lib "user32" Alias "SetParent" (ByVal hWndChild As Integer, ByVal hWndNewParent As Integer) As Integer
'取指定屏幕坐标上的窗体句柄
Private Declare Function WindowFromPoint Lib "user32" Alias "WindowFromPoint" (ByVal xPoint As Integer, ByVal yPoint As Integer) As Integer
'取指定窗体的矩形区域
<DllImport("user32.dll")> Public Shared Function GetWindowRect(ByVal hWnd As IntPtr, ByRef rect As RECT) As IntPtr
End Function
<StructLayout(LayoutKind.Sequential)> Public Structure RECT
Public left As Integer
Public top As Integer
Public right As Integer
Public bottom As Integer
End Structure
'象棋魔法学校窗口句柄(这里应该加入窗体标题,窗体类等判定条件以判定是否为象棋魔法学校窗口句柄,但代码中未实现)
Dim whandle As IntPtr
Private Sub Panel1_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Panel1.MouseMove
Cursor.Current = Cursors.Hand
End Sub
Private Sub Panel1_MouseUp(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles Panel1.MouseUp
'这个句柄不能用Process.MainFormHandle来获取,你可以用SPY++来查看或者也许你知道VB6(假定象棋魔法学校是用VB6开发的)在创建窗体时的一些特性
whandle = (WindowFromPoint(Cursor.Position.X, Cursor.Position.Y))
ButHelp.Location = Point.Empty
Dim bmp As Bitmap = My.Resources.S.Clone
eye = New Eyes(bmp, cStart, cOffset, cTestSize)
Dim fen As String = eye.LookAll(bmp)
If fen <> "rnbakabnr/9/1c5c1/p1p1p1p1p/9/9/P1P1P1P1P/1C5C1/9/RNBAKABNR w - - 0 3" Then '这个FEN字符串代表了我方执红但未开局(且未让子)时的棋盘
eye.LookAll(bmp, True)
bmp.Save("c:\s1.bmp")
MsgBox("请将 c:\s1.bmp 发送给作者", , "初始化失败")
End If
eg = New Engine("象眼", "你所得到的象眼的完整路径,包括扩展名")
SetParent(ButHelp.Handle, whandle)
wgr = Graphics.FromHwnd(whandle)
Cursor.Current = Cursors.Default
End Sub
Public cStart As Point = New Point(35, 80) '开始点
Public cOffset As Point = New Point(56, 56) '偏移量
Public cTestSize As Size = New Size(10, 10) '测试矩形半边长
Public egGoStep As Integer = 8
Dim wgr As Graphics
Dim eye As Eyes
Dim WithEvents eg As Engine
Private Sub ButHelp_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles ButHelp.Click
RedrawWindow(whandle, 0, 0, 5) '重绘象棋魔法学校窗体,以使其删除上一次绘制的图形
Threading.Thread.Sleep(1000)
My.Application.DoEvents()
Dim wsource As Point '象棋魔法学校窗体当前左上角坐标——相对于屏幕坐标
Dim wsize As Size '象棋魔法学校窗体大小
Dim win32rect As RECT '象棋魔法学校窗体RECT(WIN32 API——RECT)
GetWindowRect(whandle, win32rect)
wsource = New Point(win32rect.left, win32rect.top)
wsize = New Size(win32rect.right - win32rect.left, win32rect.bottom - win32rect.top)
Dim bmp As Bitmap = New Bitmap(wsize.Width, wsize.Height) '创建存储屏幕复制的图像
Dim gr As Graphics = Graphics.FromImage(bmp)
gr.CopyFromScreen(wsource, Point.Empty, wsize) '复制屏幕内容到图像
Dim Fen As String = eye.LookAll(bmp) '图像分析
'bmp.Save("c:\s2.bmp")
If eg.EngIDInfo.idType = "uci" Then '兼容UCI协议
eg.Go("fen " & Fen, egGoStep)
ElseIf eg.EngIDInfo.idType = "ucci" Then
eg.Go("position fen " & Fen, egGoStep)
Else '兼容未知是UCCI还是UCI的协议
eg.Go("fen " & Fen, egGoStep)
eg.Go("position fen " & Fen, egGoStep)
End If
End Sub
Private Sub eg_EngineBestmove(ByVal move As String, ByVal usermove As String) Handles eg.EngineBestmove
Dim p1 = Chessboard2Point(Mid(move, 1, 2)) '在9*10的数组中的位置
Dim p2 = Chessboard2Point(Mid(move, 3, 2))
Dim ps As Point = New Point(p1.X * cOffset.X, p1.Y * cOffset.Y)
Dim pd As Point = New Point(p2.X * cOffset.X, p2.Y * cOffset.Y)
ps.Offset(35, 35)
pd.Offset(35, 35)
wgr.DrawLine(Pens.Red, ps, pd) '绘制走棋方法,方块是起点,圆形是终点
wgr.FillRectangle(Brushes.Blue, New Rectangle(ps - New Point(10, 10), New Size(20, 20)))
wgr.FillEllipse(Brushes.Red, New Rectangle(pd - New Point(10, 10), New Size(20, 20)))
End Sub
Private Sub eg_EngineResign(ByVal Name As String) Handles eg.EngineResign
MsgBox("引擎认输")
End Sub
Private Sub eg_EngineLoadOver(ByVal Name As String, ByVal IDInfo As Engine.EngineIDInfo) Handles eg.EngineLoadOver
MsgBox("引擎名称:" & Name & vbCrLf & vbCrLf & IDInfo.ToString, MsgBoxStyle.OkOnly, "引擎信息")
End Sub
Dim PointEmpty = New Point(-1, -1)
Private Function Chessboard2Point(ByVal s As String) As Point '把用UCCI协议表示的棋子位置转化为9*10数组中的位置
Try
If s.Length = 2 Then
Return New Point(Asc(Mid(s, 1, 1)) - Asc("a"), 9 - CInt(Mid(s, 2, 1)))
Else
Return PointEmpty
End If
Catch ex As Exception
Return PointEmpty
End Try
End Function
Private Sub Form1_FormClosing(ByVal sender As Object, ByVal e As System.Windows.Forms.FormClosingEventArgs) Handles Me.FormClosing
If eg IsNot Nothing Then eg.close() '通知引擎退出
End Sub
End Class
最后,附上我的程序。当然,这里我把它改成了使用象眼这一引擎,你可以把其他引擎复制过来并重命名为ELEEYE.EXE来使用它们。