基于pywin32、ctypes以及PIL实现扫雷脚本

窗口

01 窗口操作类

窗口操作类的实现主要基于两个第三方库:pywin32、ctypes。

pywin32 是 针对 Microsoft Windows 的 Python扩展,提供了对大部分 Win32 API 的访问、创建、和使用 COM对象 的能力以及 Pythonwin 环境。

对于上述描述是不是很懵,其实我也是。不纠结,这里我们只要知道一点,就是pywin32能够帮助我们完成对窗口的操作。

这里我们只讲最重要的一个接口:win32api.SendMessage

SendMessage函数:将指定的消息发送到窗口。
其包含四个参数:HWND、UINT、WPARAM、LPARAM
- HWND:窗口句柄(窗口的唯一标识)
- UINT:要发送的消息(要进行的操作,比如WM_LBUTTONDOWN就是按下鼠标左键)
- WPARAM、LPARAM:消息参数(对于鼠标点击就是对应的坐标)

关于pywin32的更多操作可以查看:
win32api中文文档:http://www.yfvb.com/help/win32sdk/
pywin32库安装及使用说明:https://blog.csdn.net/freeking101/article/details/88231952

ctypes是Python的一个外部库,提供和C语言兼容的数据类型,可以很方便地调用DLL中输出的C接口函数,简单来说就是可以通过ctypes直接调用c语言写好的接口。

ctypes可以在python中实现结构体、指针等数据类型,但是这并不是我们需要关注的点,有兴趣的小伙伴可以查看:https://blog.csdn.net/bianchengxueseng/article/details/115262954

发送消息使用的接口是:Windll.user32.PoseMessageW

参数与win32api.SendMessage一致,不过这里需要注意一点,就是坐标参数并不像SendMessage中是直接传入的,而是经过特定的位移运算得到对应的坐标参数。(由于看不到源码,不知道其具体实现是怎样的,按照它的方式使用即可)

基于使用的经验,ctypes的效果会更好,win32api在很多情况下发送消息会没有效果。

02 窗口管理类

顾名思义,就是为了进行窗口管理而设置的类。其实在这里只实现了一个功能,就是设置主窗口。如果有必要,可以再加入其他功能。当然也可以对设置主窗口的方法进行重写。

03 补充

然后再讲一下截图和比图,这两个接口就是图像识别的核心。为什么和窗口放在一起讲呢?因为它们放在一起就完成了这一类脚本的“地基”(所以我把它命名为base)。

截图这里不多赘述,在源码中已经对每一行的含义进行了标注,简单来说就是通过在DC间传递位图信息实现copy的效果。

对于比图,我们其实只需要明确一点,就是图片对象是否存在唯一特征,基于这个,我大致看了一下Image类中所有的属性和方法,其中有一个生成对应直方图的方法,我们就可以利用它来实现比图的目标。

最后,在窗口这一块还有一点内容需要补充,就是窗口句柄。我们已经知道窗口句柄是窗口的唯一标识,那么我们要怎么获取它呢?基于win32gui中的EnumWindows和EnumChildWindows就可以实现,但是它们会无差别的获取到所有的窗口,因此我们通过窗口的属性来设置“关卡”。窗口属性的获取需要借助一个外部工具–大漠。

关于大漠综合工具如何使用,百度一下就知道了。

核心

写到这里我发现自己挺喜欢使用核心这个词的,不过这里我所要讲的是这个脚本代码的核心部分。

01 映射

映射虽然听起来很高大上,其实只需要一个“字典”就可以实现。具体实现这里就不讲了,就是一个三层循环。那么问题来了,我们都知道循环的层数达到三层后时间复杂度会达到一个恐怖的量级,在测试的过程中也是深有感触,当然这里也要考虑网络的原因(我自己的网络是比较差的,时不时断网的那种);仅仅是完成一次操作就花2s,这个速率简直就是龟速。

So…怎么去解决这个问题呢?我的第一反应肯定就是去百度,但是结果可想而知,对于多层循环的优化,千篇一律的都是在讲hashmap,放在python中其实就是字典,大概说的就是,hashmap的查找效率是常量级的,将遍历转化为hashmap的查找…很无语,对于我这里的问题可以说是毫无帮助。

那么我是怎么找到优化方案的呢?说实话,有点小幸运。我刚好在看Head First设计模式这本书,里面就提到了一个设计原则:找出应用中可能需要变化之处,把它们独立出来,不要和那些不需要变化的代码混在一起。看到这个,我就知道该怎么做了,在完成第一次映射之后,其实已经不需要再对整个游戏界面进行映射了,只需要考虑每一次操作可能引起变化的区域,对其进行映射。Perfect,问题就这样解决了…

关于Head First设计模式,有一句话我感触很深–使用模式最好的方式就是把模式装进脑子里,然后在你的设计和已有的应用中,寻找何处可以使用它们;以往是代码的复用,现在是经验的复用。所以真的不要妄自菲薄,如果发现自己力不能及,就要学会使用外力。

02 计算

计算的动画演示可以查看:<付链接>

关于计算的两种策略,这里我不再赘述。我来说说计算代码部分隐藏的一个小bug,在计算失败的部分,会生成一个随机坐标,但是只有在这个坐标对应的单元格是空白单元格时才会进行点击操作,否则会继续生成。好像没什么问题,但是我们不妨想一下,在什么情况下会出现这种状况?

其实有两种情况:第一种就是游戏开局或者点到地雷的时候,这个时候生成随机坐标,无论是什么都不会存在问题。第二种是“死局”,这就很有意思了,生成“死局”之后,往往空白单元格会集中在很小的一块区域,而生成的坐标又是随机的,所以自然会有概率进入死循环。那么解决方法也不难,只要针对第二种情况记录空白单元格的坐标,再随机选取一个即可。这里有余力的小伙伴可以自己补充这部分代码。

关于计算,有一关点需要补充–电脑屏幕的坐标系。
在这里插入图片描述
我们要知道电脑屏幕的坐标系和数学中的坐标系是不同的,要特别注意x,y轴与行和列的对应关系,行对应的是y轴,而列对应的是x轴。

运行

运行模块大致有四个部分:截图、拆解、映射、计算。实际上就是对窗口文件和核心文件的调用。

这里讲一下“重新映射”的部分。正如上面提到的,我们只关注可能发生改变的位置,那么我们只需要遍历coordinateList(操作序列),判断映射出来的数值是不是“?”(可以在Map自行设置,表示映射失败),然后加入到againList中,检测到againList不为空,就重新进行截图、拆解,并遍历againList数组,对againList数组涉及的坐标进行重新映射,如果一直失败,就会一直重复这个过程。是什么导致了这个情况,在动画演示视频中,我也提及到了–“爆金币”的动画,动画是动态的,而映射是一个静态的过程,所以在动画持续时间内,会一直映射失败。

需要源码的小伙伴,请关注下方公众号。
在这里插入图片描述

  • 6
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值