从零实现Python自动扫雷(二):窗口识别、截图与点击

引言

上一篇文章中介绍了扫雷游戏的开发,本文将详细介绍如何通过Python实现扫雷游戏的窗口识别、屏幕截图和鼠标点击操作,这是实现扫雷的基础组件。

系统架构

我们的实现分为两个核心模块:

  1. CursorClick.py - 负责底层鼠标操作
  2. WindowManager.py - 负责窗口管理和图像处理

一、鼠标点击模块实现

1.1 核心功能

CursorClick.py模块封装了Windows系统级的鼠标操作,主要特点包括:

  • 支持左键和右键两种点击方式
  • 点击后自动恢复鼠标原位置
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
import win32api
import win32con
import time
from mylogger import logger

def click(px, py, type):
try:
Pos = win32api.GetCursorPos() # 记录原位置
win32api.SetCursorPos((int(px), int(py)))

# 根据按钮类型选择点击事件
if type == 'left':
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
time.sleep(0.1) # 100ms
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
elif type == 'right':
win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTDOWN, 0, 0, 0, 0)
time.sleep(0.1) # 100ms
win32api.mouse_event(win32con.MOUSEEVENTF_RIGHTUP, 0, 0, 0, 0)

win32api.SetCursorPos(Pos) # 回到原位置
time.sleep(0.1) # 100ms
logger.info(f"点击成功!坐标: ({px}, {py})")
except Exception as e:
logger.error(f"点击失败!错误信息: {e}")

1.2 技术细节

  1. 坐标系统:使用Windows API获取和设置鼠标位置,坐标单位为屏幕像素
  2. 点击实现:通过mouse_event函数模拟鼠标按下和释放动作
  3. 稳定性保障
    • 每次操作前后添加100ms延迟
    • 点击后恢复鼠标原位置避免干扰用户
  4. 错误处理:捕获所有异常并通过日志系统记录

二、窗口管理模块实现

2.1 核心功能

WindowManager.py模块负责游戏窗口的识别和管理,主要功能包括:

  • 查找和激活游戏窗口
  • 检测游戏状态(胜利/失败)
  • 截取游戏区域图像
  • 将游戏区域切分为16×16的网格
  • 在指定网格位置执行点击操作
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
import sys
import win32gui
from PIL import ImageGrab
from CursorClick import click
from mylogger import logger

zoomRate = 1.25 # 应对屏幕缩放问题
grid_origin_width = 20
grid_origin_height = 20
grid_width = int(zoomRate * grid_origin_width)
grid_height = int(zoomRate * grid_origin_height)

def crop_grid_img(img, x, y): # 切分雷块返回图像
rect = (x * grid_width, y * grid_height,
(x + 1) * grid_width, (y + 1) * grid_height)
return img.crop(rect)

"""窗口管理器,负责管理游戏窗口和点击"""
class WindowManager:
# 初始化代码...

def set_window_foreground(self):
if not self.hwndMain:
self.find_main_window()
try:
win32gui.SetForegroundWindow(self.hwndMain)
except Exception as e:
logger.error("置顶窗口过程发生错误", e)
sys.exit()

# 其他方法...

2.2 窗口管理实现

2.2.1 窗口查找与置顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def find_main_window(self):
self.hwndMain = win32gui.FindWindow(self.class_name, self.title_name)
if not self.hwndMain:
logger.error("未找到主窗口")
sys.exit()
logger.info("找到主窗口")

leftMain, topMain, rightMain, bottomMain = win32gui.GetWindowRect(self.hwndMain)
logger.info(f"主窗口坐标:{leftMain} {rightMain} {topMain} {bottomMain}")

def find_game_window(self):
hwndChild = win32gui.FindWindowEx(self.hwndMain, 0, "TkChild", None)
if not hwndChild:
logger.error("未找到子窗口")
sys.exit()
logger.info("找到子窗口")

self.leftGame, self.topGame, self.rightGame, self.bottomGame = win32gui.GetWindowRect(hwndChild)
logger.info(f"子窗口坐标:{self.leftGame} {self.rightGame} {self.topGame} {self.bottomGame}")

技术要点

  • 使用FindWindowFindWindowEx查找窗口句柄
  • 通过GetWindowRect获取窗口位置和尺寸
  • 分层查找:先找主窗口,再找游戏区域子窗口

2.2.2 游戏状态检测

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
def check_main_window(self):
flag = self.find_fail_window()
if flag:
return True
flag = self.find_success_window()
if flag:
return True
self.set_window_foreground()
return False

def find_fail_window(self):
hwndFail = win32gui.FindWindow("#32770", "失败")
if hwndFail:
hwndButton = win32gui.FindWindowEx(hwndFail, 0, "Button", "确定")
if hwndButton:
left, top, right, bottom = win32gui.GetWindowRect(hwndButton)
click((left + right) / 2, (top + bottom) / 2, "left")
logger.info("扫雷失败")
sys.exit()
return False

def find_success_window(self):
hwndSuccess = win32gui.FindWindow("#32770", "成功")
if hwndSuccess:
hwndButton = win32gui.FindWindowEx(hwndSuccess, 0, "Button", "确定")
if hwndButton:
left, top, right, bottom = win32gui.GetWindowRect(hwndButton)
click((left + right) / 2, (top + bottom) / 2, "left")
logger.info("扫雷成功")
sys.exit()
return False

技术要点

  • 通过检查失败和成功窗口识别游戏状态

2.3 图像处理实现

2.3.1 游戏区域截图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def get_img_blob(self):
try:
self.set_window_foreground()
self.find_game_window()
(left, top, right, bottom) = rect = (
round(self.leftGame * zoomRate + 17 * zoomRate),
round(self.topGame * zoomRate + 46 * zoomRate),
round(self.leftGame * zoomRate + 17 * zoomRate + grid_width * 16),
round(self.topGame * zoomRate + 46 * zoomRate + grid_height * 16)
)
img = ImageGrab.grab()
cropped_img = img.crop(rect)
except Exception as e:
logger.error(f"处理窗口或截图过程发生错误:{e}")
return

blocks_x = (right - left) // grid_width
blocks_y = (bottom - top) // grid_height
blocks_img = [[crop_grid_img(cropped_img, x, y) for x in range(blocks_x)] for y in range(blocks_y)]
return blocks_img

技术要点

  • 使用PIL库的ImageGrab截取屏幕
  • 根据游戏窗口位置计算游戏区域坐标
  • 考虑屏幕缩放因素(zoomRate)
  • 将游戏区域切分为16×16网格

2.3.2 网格点击操作

1
2
3
4
5
6
7
8
9
10
11
12
def click_grid(self, x, y, type):
logger.info(f"点击格子:{x} {y} {type}")
self.set_window_foreground()
self.find_game_window()
(left, top, right, bottom) = rect = (
round(self.leftGame + 17),
round(self.topGame + 46),
round(self.leftGame + 17 + grid_origin_width * 16),
round(self.topGame + 46 + grid_origin_height * 16)
)
click(left + (y+0.5) * grid_origin_width, top + (x+0.5) * grid_origin_height, type)
self.check_main_window()

技术要点

  • 将网格坐标转换为屏幕坐标
  • 点击网格中心点(坐标+0.5)
  • 操作后检查游戏状态

三、关键技术点

3.1 屏幕缩放处理

现代高分辨率显示器常使用缩放功能,我们通过zoomRate参数(1.25)来解决这个问题:

1
2
3
4
5
zoomRate = 1.25  # 应对屏幕缩放问题
grid_origin_width = 20
grid_origin_height = 20
grid_width = int(zoomRate * grid_origin_width)
grid_height = int(zoomRate * grid_origin_height)

3.2 坐标计算

游戏区域定位基于经验值(17,46),这是扫雷游戏的标准布局:

1
2
3
# 游戏区域左上角坐标计算
leftGame * zoomRate + 17 * zoomRate
topGame * zoomRate + 46 * zoomRate

3.3 错误处理与日志

整个系统采用统一的错误处理策略:

  1. 使用try-catch捕获所有异常
  2. 通过logger记录操作日志和错误信息
  3. 关键错误直接退出程序(sys.exit)

四、总结

本文实现的扫雷游戏自动化模块具有以下特点:

  1. 稳定性:完善的错误处理和操作延迟确保可靠运行
  2. 灵活性:可适应不同的屏幕缩放设置
  3. 模块化:清晰的职责分离,便于扩展

下一篇将介绍使用TensorFlow实现扫雷游戏中雷块内容的识别

本文窗口管理、截图和点击操作的实现思路也可以应用于其他Windows应用的自动化测试和操作。


从零实现Python自动扫雷(二):窗口识别、截图与点击
https://blog.cngo.xyz/posts/6058.html
作者
cngo
发布于
2024年12月18日
许可协议