修改游戏窗口获取逻辑以及完善获取模拟点击相关逻辑
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
"""
|
||||
游戏窗口管理
|
||||
通过 exe 文件名或窗口标题获取游戏窗口
|
||||
"""
|
||||
|
||||
import os
|
||||
import win32gui
|
||||
import win32con
|
||||
import win32process
|
||||
import psutil
|
||||
from typing import Optional, Tuple
|
||||
from src.utils.logger import logger
|
||||
|
||||
@@ -11,13 +14,14 @@ from src.utils.logger import logger
|
||||
class GameWindowManager:
|
||||
"""游戏窗口管理器"""
|
||||
|
||||
# 游戏窗口类名和标题
|
||||
GAME_CLASS_NAME = "UnityWndClass" # Unity游戏常用类名
|
||||
GAME_WINDOW_TITLE = "桃源深处有人家"
|
||||
# 游戏配置
|
||||
GAME_EXE_NAME = "ycgame.exe" # 游戏exe文件名
|
||||
GAME_WINDOW_TITLE = "桃源深处有人家" # 游戏窗口标题
|
||||
|
||||
def __init__(self):
|
||||
self._hwnd: Optional[int] = None
|
||||
self._window_rect: Tuple[int, int, int, int] = (0, 0, 0, 0)
|
||||
self._process_id: Optional[int] = None
|
||||
|
||||
@property
|
||||
def is_window_captured(self) -> bool:
|
||||
@@ -31,6 +35,11 @@ class GameWindowManager:
|
||||
"""窗口句柄"""
|
||||
return self._hwnd
|
||||
|
||||
@property
|
||||
def process_id(self) -> Optional[int]:
|
||||
"""进程ID"""
|
||||
return self._process_id
|
||||
|
||||
@property
|
||||
def window_rect(self) -> Tuple[int, int, int, int]:
|
||||
"""窗口矩形 (left, top, right, bottom)"""
|
||||
@@ -46,37 +55,175 @@ class GameWindowManager:
|
||||
left, top, right, bottom = self.window_rect
|
||||
return (right - left, bottom - top)
|
||||
|
||||
def find_window(self) -> bool:
|
||||
"""查找游戏窗口"""
|
||||
# 先尝试精确匹配
|
||||
hwnd = win32gui.FindWindow(None, self.GAME_WINDOW_TITLE)
|
||||
def find_window_by_exe(self, exe_name: str = None) -> bool:
|
||||
"""
|
||||
通过exe文件名查找游戏窗口
|
||||
|
||||
# 如果没找到,尝试模糊匹配
|
||||
if hwnd == 0:
|
||||
def callback(hwnd, extra):
|
||||
if win32gui.IsWindowVisible(hwnd):
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if self.GAME_WINDOW_TITLE in title:
|
||||
extra.append(hwnd)
|
||||
Args:
|
||||
exe_name: exe文件名,默认使用 GAME_EXE_NAME
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if exe_name is None:
|
||||
exe_name = self.GAME_EXE_NAME
|
||||
|
||||
try:
|
||||
# 获取所有进程
|
||||
hwnd_list = []
|
||||
|
||||
def enum_windows_callback(hwnd, extra):
|
||||
if not win32gui.IsWindowVisible(hwnd):
|
||||
return True
|
||||
|
||||
try:
|
||||
# 获取窗口对应的进程ID
|
||||
_, pid = win32process.GetWindowThreadProcessId(hwnd)
|
||||
|
||||
# 获取进程信息
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
process_exe = process.exe()
|
||||
|
||||
# 检查exe文件名是否精确匹配
|
||||
# 使用 os.path.basename 获取文件名,避免路径包含目标名称的误匹配
|
||||
process_exe_name = os.path.basename(process_exe).lower()
|
||||
target_exe_name = exe_name.lower()
|
||||
|
||||
if process_exe_name == target_exe_name:
|
||||
extra.append((hwnd, pid, process_exe))
|
||||
logger.debug(f"[窗口枚举] 找到匹配窗口: hwnd={hwnd}, pid={pid}, exe={process_exe}")
|
||||
except psutil.NoSuchProcess:
|
||||
# 进程已结束,忽略
|
||||
logger.debug(f"[窗口枚举] 进程 {pid} 已不存在,跳过")
|
||||
except psutil.AccessDenied:
|
||||
# 无权限访问进程,忽略
|
||||
logger.debug(f"[窗口枚举] 无权限访问进程 {pid},跳过")
|
||||
except psutil.Error as e:
|
||||
# 其他psutil错误
|
||||
logger.warning(f"[窗口枚举] 获取进程 {pid} 信息失败: {e}")
|
||||
|
||||
except win32process.error as e:
|
||||
# win32process 错误
|
||||
logger.warning(f"[窗口枚举] 获取窗口 {hwnd} 的进程ID失败: {e}")
|
||||
except OSError as e:
|
||||
# 系统错误
|
||||
logger.warning(f"[窗口枚举] 处理窗口 {hwnd} 时发生系统错误: {e}")
|
||||
|
||||
return True
|
||||
|
||||
windows = []
|
||||
win32gui.EnumWindows(callback, windows)
|
||||
if windows:
|
||||
hwnd = windows[0]
|
||||
|
||||
if hwnd != 0:
|
||||
self._hwnd = hwnd
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
logger.info(f"找到游戏窗口: {hwnd}, 大小: {self.client_size}")
|
||||
return True
|
||||
# 枚举所有窗口
|
||||
win32gui.EnumWindows(enum_windows_callback, hwnd_list)
|
||||
|
||||
logger.warning("未找到游戏窗口")
|
||||
return False
|
||||
if hwnd_list:
|
||||
# 优先选择标题匹配的窗口
|
||||
for hwnd, pid, exe_path in hwnd_list:
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
if self.GAME_WINDOW_TITLE in title:
|
||||
self._hwnd = hwnd
|
||||
self._process_id = pid
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
logger.info(f"通过exe找到游戏窗口: {hwnd}, PID: {pid}, 大小: {self.client_size}, 标题: {title}")
|
||||
return True
|
||||
|
||||
# 如果没有标题匹配,使用第一个找到的窗口
|
||||
hwnd, pid, exe_path = hwnd_list[0]
|
||||
self._hwnd = hwnd
|
||||
self._process_id = pid
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
title = win32gui.GetWindowText(hwnd)
|
||||
logger.info(f"通过exe找到游戏窗口(备用): {hwnd}, PID: {pid}, 大小: {self.client_size}, 标题: {title}")
|
||||
return True
|
||||
|
||||
logger.warning(f"未找到exe为 {exe_name} 的游戏窗口")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"通过exe查找窗口失败: {e}")
|
||||
return False
|
||||
|
||||
def find_window_by_title(self, title: str = None) -> bool:
|
||||
"""
|
||||
通过窗口标题查找游戏窗口
|
||||
|
||||
def capture_window(self) -> bool:
|
||||
"""捕获游戏窗口"""
|
||||
return self.find_window()
|
||||
Args:
|
||||
title: 窗口标题,默认使用 GAME_WINDOW_TITLE
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if title is None:
|
||||
title = self.GAME_WINDOW_TITLE
|
||||
|
||||
try:
|
||||
# 先尝试精确匹配
|
||||
hwnd = win32gui.FindWindow(None, title)
|
||||
|
||||
# 如果没找到,尝试模糊匹配
|
||||
if hwnd == 0:
|
||||
def callback(hwnd, extra):
|
||||
if win32gui.IsWindowVisible(hwnd):
|
||||
window_title = win32gui.GetWindowText(hwnd)
|
||||
if title in window_title:
|
||||
extra.append(hwnd)
|
||||
return True
|
||||
|
||||
windows = []
|
||||
win32gui.EnumWindows(callback, windows)
|
||||
if windows:
|
||||
hwnd = windows[0]
|
||||
|
||||
if hwnd != 0:
|
||||
self._hwnd = hwnd
|
||||
self._window_rect = win32gui.GetWindowRect(hwnd)
|
||||
|
||||
# 获取进程ID
|
||||
try:
|
||||
_, self._process_id = win32process.GetWindowThreadProcessId(hwnd)
|
||||
except Exception:
|
||||
self._process_id = None
|
||||
|
||||
logger.info(f"通过标题找到游戏窗口: {hwnd}, PID: {self._process_id}, 大小: {self.client_size}")
|
||||
return True
|
||||
|
||||
logger.warning(f"未找到标题为 {title} 的游戏窗口")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"通过标题查找窗口失败: {e}")
|
||||
return False
|
||||
|
||||
def find_window(self, use_exe: bool = True) -> bool:
|
||||
"""
|
||||
查找游戏窗口(综合方法)
|
||||
|
||||
Args:
|
||||
use_exe: 优先使用exe名查找,失败后再尝试标题查找
|
||||
|
||||
Returns:
|
||||
bool: 是否找到窗口
|
||||
"""
|
||||
if use_exe:
|
||||
# 先尝试通过exe查找
|
||||
if self.find_window_by_exe():
|
||||
return True
|
||||
# 失败后尝试标题查找
|
||||
logger.info("通过exe查找失败,尝试通过标题查找")
|
||||
return self.find_window_by_title()
|
||||
else:
|
||||
return self.find_window_by_title()
|
||||
|
||||
def capture_window(self, use_exe: bool = True) -> bool:
|
||||
"""
|
||||
捕获游戏窗口
|
||||
|
||||
Args:
|
||||
use_exe: 是否优先使用exe名查找
|
||||
|
||||
Returns:
|
||||
bool: 是否成功捕获
|
||||
"""
|
||||
return self.find_window(use_exe=use_exe)
|
||||
|
||||
def bring_to_front(self):
|
||||
"""将窗口置前"""
|
||||
@@ -95,3 +242,41 @@ class GameWindowManager:
|
||||
|
||||
# TODO: 使用pyautogui或win32api发送点击
|
||||
logger.debug(f"点击坐标: ({screen_x}, {screen_y})")
|
||||
|
||||
def get_window_info(self) -> dict:
|
||||
"""
|
||||
获取窗口详细信息
|
||||
|
||||
Returns:
|
||||
dict: 窗口信息字典
|
||||
"""
|
||||
if not self.is_window_captured:
|
||||
return {
|
||||
"captured": False,
|
||||
"hwnd": None,
|
||||
"title": None,
|
||||
"process_id": None,
|
||||
"exe_path": None,
|
||||
"rect": None,
|
||||
"size": None
|
||||
}
|
||||
|
||||
title = win32gui.GetWindowText(self._hwnd)
|
||||
exe_path = None
|
||||
|
||||
if self._process_id:
|
||||
try:
|
||||
process = psutil.Process(self._process_id)
|
||||
exe_path = process.exe()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
pass
|
||||
|
||||
return {
|
||||
"captured": True,
|
||||
"hwnd": self._hwnd,
|
||||
"title": title,
|
||||
"process_id": self._process_id,
|
||||
"exe_path": exe_path,
|
||||
"rect": self.window_rect,
|
||||
"size": self.client_size
|
||||
}
|
||||
|
||||
629
src/core/input_simulator.py
Normal file
629
src/core/input_simulator.py
Normal file
@@ -0,0 +1,629 @@
|
||||
"""
|
||||
输入模拟器
|
||||
提供鼠标和键盘的模拟操作,包括点击、长按、滑动等
|
||||
使用 SendInput 模拟真实硬件输入,绕过游戏防护
|
||||
|
||||
# 捕获窗口
|
||||
window = GameWindowManager()
|
||||
window.capture_window()
|
||||
|
||||
# 创建模拟器
|
||||
sim = InputSimulator(window)
|
||||
|
||||
# 点击操作(统一使用 SendInput)
|
||||
sim.click(x, y) # 短按
|
||||
sim.click(x, y, duration=1.0) # 长按
|
||||
sim.double_click(x, y) # 双击
|
||||
sim.swipe(x1, y1, x2, y2, duration=0.5) # 滑动
|
||||
sim.key_press('esc') # 按键
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
from typing import Optional, Tuple
|
||||
from dataclasses import dataclass
|
||||
from src.utils.logger import logger
|
||||
from src.core.game_window import GameWindowManager
|
||||
|
||||
|
||||
# Windows API 常量
|
||||
INPUT_MOUSE = 0
|
||||
INPUT_KEYBOARD = 1
|
||||
|
||||
# 鼠标事件标志
|
||||
MOUSEEVENTF_MOVE = 0x0001
|
||||
MOUSEEVENTF_ABSOLUTE = 0x8000
|
||||
MOUSEEVENTF_LEFTDOWN = 0x0002
|
||||
MOUSEEVENTF_LEFTUP = 0x0004
|
||||
MOUSEEVENTF_RIGHTDOWN = 0x0008
|
||||
MOUSEEVENTF_RIGHTUP = 0x0010
|
||||
MOUSEEVENTF_MIDDLEDOWN = 0x0020
|
||||
MOUSEEVENTF_MIDDLEUP = 0x0040
|
||||
|
||||
# 键盘事件标志
|
||||
KEYEVENTF_KEYUP = 0x0002
|
||||
|
||||
|
||||
class MOUSEINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dx", wintypes.LONG),
|
||||
("dy", wintypes.LONG),
|
||||
("mouseData", wintypes.DWORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class KEYBDINPUT(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("wVk", wintypes.WORD),
|
||||
("wScan", wintypes.WORD),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("time", wintypes.DWORD),
|
||||
("dwExtraInfo", ctypes.POINTER(wintypes.ULONG)),
|
||||
]
|
||||
|
||||
|
||||
class INPUT_I(ctypes.Union):
|
||||
_fields_ = [
|
||||
("mi", MOUSEINPUT),
|
||||
("ki", KEYBDINPUT),
|
||||
]
|
||||
|
||||
|
||||
class INPUT(ctypes.Structure):
|
||||
_anonymous_ = ("_input",)
|
||||
_fields_ = [
|
||||
("type", wintypes.DWORD),
|
||||
("_input", INPUT_I),
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class Point:
|
||||
"""坐标点"""
|
||||
x: int
|
||||
y: int
|
||||
|
||||
def __add__(self, other):
|
||||
return Point(self.x + other.x, self.y + other.y)
|
||||
|
||||
def __iter__(self):
|
||||
yield self.x
|
||||
yield self.y
|
||||
|
||||
|
||||
class InputSimulator:
|
||||
"""
|
||||
输入模拟器 - 使用 SendInput 模拟真实硬件输入
|
||||
|
||||
对外暴露的简洁接口:
|
||||
- click(x, y, duration=0) -> 点击/长按
|
||||
- swipe(start_x, start_y, end_x, end_y, duration=0.5) -> 滑动
|
||||
- key_press(key) -> 按键
|
||||
- key_down(key) / key_up(key) -> 按住/释放按键
|
||||
"""
|
||||
|
||||
def __init__(self, window_manager: GameWindowManager):
|
||||
self.window = window_manager
|
||||
self._screen_width = ctypes.windll.user32.GetSystemMetrics(0)
|
||||
self._screen_height = ctypes.windll.user32.GetSystemMetrics(1)
|
||||
logger.info(f"InputSimulator 初始化完成,屏幕分辨率: {self._screen_width}x{self._screen_height}")
|
||||
|
||||
def _to_screen_coords(self, x: int, y: int) -> Tuple[int, int]:
|
||||
"""将窗口相对坐标转换为屏幕坐标"""
|
||||
logger.debug(f"[坐标转换] 开始转换窗口相对坐标 ({x}, {y})")
|
||||
|
||||
if not self.window.is_window_captured:
|
||||
logger.error("[坐标转换] 失败: 窗口未捕获")
|
||||
raise RuntimeError("窗口未捕获,无法执行操作")
|
||||
|
||||
import win32gui
|
||||
hwnd = self.window.hwnd
|
||||
|
||||
# 获取窗口客户区在屏幕上的位置
|
||||
client_rect = win32gui.GetClientRect(hwnd)
|
||||
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_rect[0], client_rect[1]))
|
||||
logger.debug(f"[坐标转换] 窗口客户区左上角屏幕坐标: ({client_left}, {client_top})")
|
||||
|
||||
# 计算屏幕坐标
|
||||
screen_x = client_left + x
|
||||
screen_y = client_top + y
|
||||
logger.debug(f"[坐标转换] 计算后的屏幕坐标: ({screen_x}, {screen_y})")
|
||||
|
||||
# 确保在屏幕范围内
|
||||
orig_screen_x, orig_screen_y = screen_x, screen_y
|
||||
screen_x = max(0, min(screen_x, self._screen_width - 1))
|
||||
screen_y = max(0, min(screen_y, self._screen_height - 1))
|
||||
|
||||
if (screen_x, screen_y) != (orig_screen_x, orig_screen_y):
|
||||
logger.warning(f"[坐标转换] 坐标被限制在屏幕范围内: ({orig_screen_x}, {orig_screen_y}) -> ({screen_x}, {screen_y})")
|
||||
|
||||
logger.debug(f"[坐标转换] 完成: 窗口相对 ({x}, {y}) -> 屏幕 ({screen_x}, {screen_y})")
|
||||
return screen_x, screen_y
|
||||
|
||||
def _activate_window(self):
|
||||
"""激活窗口"""
|
||||
logger.debug("[窗口激活] 开始激活窗口")
|
||||
|
||||
if not self.window.is_window_captured:
|
||||
logger.error("[窗口激活] 失败: 窗口未捕获")
|
||||
return False
|
||||
|
||||
import win32gui
|
||||
import win32con
|
||||
|
||||
hwnd = self.window.hwnd
|
||||
logger.debug(f"[窗口激活] 窗口句柄: {hwnd}")
|
||||
|
||||
# 如果窗口最小化,恢复它
|
||||
if win32gui.IsIconic(hwnd):
|
||||
logger.debug("[窗口激活] 窗口已最小化,执行恢复操作")
|
||||
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
|
||||
logger.info("[窗口激活] 窗口已从最小化恢复")
|
||||
|
||||
# 置顶窗口
|
||||
logger.debug("[窗口激活] 设置窗口为前台窗口")
|
||||
win32gui.SetForegroundWindow(hwnd)
|
||||
|
||||
# 验证窗口是否在前台
|
||||
foreground_hwnd = win32gui.GetForegroundWindow()
|
||||
if foreground_hwnd == hwnd:
|
||||
logger.info("[窗口激活] 窗口激活成功")
|
||||
else:
|
||||
logger.warning(f"[窗口激活] 窗口可能未成功激活,前台窗口句柄: {foreground_hwnd}")
|
||||
|
||||
return True
|
||||
|
||||
def _send_input(self, inputs):
|
||||
"""发送输入事件"""
|
||||
nInputs = len(inputs)
|
||||
LPINPUT = INPUT * nInputs
|
||||
pInputs = LPINPUT(*inputs)
|
||||
cbSize = ctypes.sizeof(INPUT)
|
||||
|
||||
logger.debug(f"[SendInput] 准备发送 {nInputs} 个输入事件,结构体大小: {cbSize}")
|
||||
|
||||
result = ctypes.windll.user32.SendInput(nInputs, pInputs, cbSize)
|
||||
|
||||
if result == 0:
|
||||
error = ctypes.get_last_error()
|
||||
logger.error(f"[SendInput] 发送失败,错误码: {error}")
|
||||
else:
|
||||
logger.debug(f"[SendInput] 发送成功,成功发送 {result} 个事件")
|
||||
|
||||
return result
|
||||
|
||||
def _get_system_time(self) -> int:
|
||||
"""获取系统时间(毫秒)"""
|
||||
return ctypes.windll.kernel32.GetTickCount()
|
||||
|
||||
def _move_mouse_absolute(self, x: int, y: int):
|
||||
"""移动鼠标到绝对坐标(屏幕坐标)"""
|
||||
logger.debug(f"[鼠标移动] 开始移动到屏幕坐标 ({x}, {y})")
|
||||
|
||||
abs_x = int(x * 65535 / (self._screen_width - 1))
|
||||
abs_y = int(y * 65535 / (self._screen_height - 1))
|
||||
logger.debug(f"[鼠标移动] 转换为 SendInput 绝对坐标: ({abs_x}, {abs_y})")
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=abs_x,
|
||||
dy=abs_y,
|
||||
mouseData=0,
|
||||
dwFlags=MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_MOVE,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标移动] 发送 MOUSEINPUT 事件: dx={abs_x}, dy={abs_y}, flags=MOUSEEVENTF_ABSOLUTE|MOUSEEVENTF_MOVE")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标移动] 成功移动到屏幕坐标 ({x}, {y})")
|
||||
else:
|
||||
logger.error(f"[鼠标移动] 移动到屏幕坐标 ({x}, {y}) 失败")
|
||||
|
||||
def _mouse_down(self, button: str = "left"):
|
||||
"""鼠标按下"""
|
||||
logger.debug(f"[鼠标按下] 开始按下 {button} 键")
|
||||
|
||||
if button == "left":
|
||||
flags = MOUSEEVENTF_LEFTDOWN
|
||||
button_name = "左键"
|
||||
elif button == "right":
|
||||
flags = MOUSEEVENTF_RIGHTDOWN
|
||||
button_name = "右键"
|
||||
elif button == "middle":
|
||||
flags = MOUSEEVENTF_MIDDLEDOWN
|
||||
button_name = "中键"
|
||||
else:
|
||||
logger.error(f"[鼠标按下] 不支持的按钮类型: {button}")
|
||||
return
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=0,
|
||||
dy=0,
|
||||
mouseData=0,
|
||||
dwFlags=flags,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标按下] 发送 MOUSEINPUT 事件: flags={flags}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标按下] {button_name} 按下成功")
|
||||
else:
|
||||
logger.error(f"[鼠标按下] {button_name} 按下失败")
|
||||
|
||||
def _mouse_up(self, button: str = "left"):
|
||||
"""鼠标释放"""
|
||||
logger.debug(f"[鼠标释放] 开始释放 {button} 键")
|
||||
|
||||
if button == "left":
|
||||
flags = MOUSEEVENTF_LEFTUP
|
||||
button_name = "左键"
|
||||
elif button == "right":
|
||||
flags = MOUSEEVENTF_RIGHTUP
|
||||
button_name = "右键"
|
||||
elif button == "middle":
|
||||
flags = MOUSEEVENTF_MIDDLEUP
|
||||
button_name = "中键"
|
||||
else:
|
||||
logger.error(f"[鼠标释放] 不支持的按钮类型: {button}")
|
||||
return
|
||||
|
||||
timestamp = self._get_system_time()
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_MOUSE
|
||||
input_event.mi = MOUSEINPUT(
|
||||
dx=0,
|
||||
dy=0,
|
||||
mouseData=0,
|
||||
dwFlags=flags,
|
||||
time=timestamp,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[鼠标释放] 发送 MOUSEINPUT 事件: flags={flags}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[鼠标释放] {button_name} 释放成功")
|
||||
else:
|
||||
logger.error(f"[鼠标释放] {button_name} 释放失败")
|
||||
|
||||
def _key_down(self, vk_code: int):
|
||||
"""按键按下"""
|
||||
logger.debug(f"[按键按下] 开始按下按键,虚拟键码: {vk_code}")
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_KEYBOARD
|
||||
input_event.ki = KEYBDINPUT(
|
||||
wVk=vk_code,
|
||||
wScan=0,
|
||||
dwFlags=0,
|
||||
time=0,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[按键按下] 发送 KEYBDINPUT 事件: wVk={vk_code}")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[按键按下] 按键按下成功,虚拟键码: {vk_code}")
|
||||
else:
|
||||
logger.error(f"[按键按下] 按键按下失败,虚拟键码: {vk_code}")
|
||||
|
||||
def _key_up(self, vk_code: int):
|
||||
"""按键释放"""
|
||||
logger.debug(f"[按键释放] 开始释放按键,虚拟键码: {vk_code}")
|
||||
|
||||
input_event = INPUT()
|
||||
input_event.type = INPUT_KEYBOARD
|
||||
input_event.ki = KEYBDINPUT(
|
||||
wVk=vk_code,
|
||||
wScan=0,
|
||||
dwFlags=KEYEVENTF_KEYUP,
|
||||
time=0,
|
||||
dwExtraInfo=None
|
||||
)
|
||||
|
||||
logger.debug(f"[按键释放] 发送 KEYBDINPUT 事件: wVk={vk_code}, flags=KEYEVENTF_KEYUP")
|
||||
result = self._send_input([input_event])
|
||||
|
||||
if result > 0:
|
||||
logger.info(f"[按键释放] 按键释放成功,虚拟键码: {vk_code}")
|
||||
else:
|
||||
logger.error(f"[按键释放] 按键释放失败,虚拟键码: {vk_code}")
|
||||
|
||||
# ==================== 对外接口 ====================
|
||||
|
||||
def _human_like_delay(self, base_delay: float = 0.05):
|
||||
"""模拟人类操作的随机延迟"""
|
||||
delay = base_delay + random.uniform(0.01, 0.03)
|
||||
logger.debug(f"[延迟] 人类模拟延迟: {delay:.3f}s (基础: {base_delay}s)")
|
||||
time.sleep(delay)
|
||||
|
||||
def _add_jitter(self, x: int, y: int, max_jitter: int = 3) -> Tuple[int, int]:
|
||||
"""添加随机抖动,使坐标不完全精确"""
|
||||
jitter_x = random.randint(-max_jitter, max_jitter)
|
||||
jitter_y = random.randint(-max_jitter, max_jitter)
|
||||
new_x, new_y = x + jitter_x, y + jitter_y
|
||||
|
||||
if jitter_x != 0 or jitter_y != 0:
|
||||
logger.debug(f"[抖动] 添加随机抖动: ({x}, {y}) -> ({new_x}, {new_y}), 抖动量: ({jitter_x}, {jitter_y})")
|
||||
|
||||
return new_x, new_y
|
||||
|
||||
def click(self, x: int, y: int, duration: float = 0, button: str = "left", human_like: bool = True):
|
||||
"""
|
||||
点击指定位置
|
||||
|
||||
Args:
|
||||
x, y: 窗口内的相对坐标
|
||||
duration: 按住时长(秒),0表示短按,>0表示长按
|
||||
button: 鼠标按钮,"left"/"right"/"middle"
|
||||
human_like: 是否模拟人类操作(随机延迟和抖动)
|
||||
"""
|
||||
action = "长按" if duration > 0 else "点击"
|
||||
logger.info(f"[点击操作] 开始{action}: 窗口相对坐标 ({x}, {y}), 按钮: {button}, 时长: {duration}s, 人类模拟: {human_like}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[点击操作] 步骤 1/5: 激活窗口")
|
||||
self._activate_window()
|
||||
self._human_like_delay(0.1)
|
||||
|
||||
# 转换为屏幕坐标
|
||||
logger.debug("[点击操作] 步骤 2/5: 坐标转换")
|
||||
screen_x, screen_y = self._to_screen_coords(x, y)
|
||||
|
||||
# 添加人为抖动
|
||||
if human_like:
|
||||
logger.debug("[点击操作] 步骤 3/5: 添加随机抖动")
|
||||
screen_x, screen_y = self._add_jitter(screen_x, screen_y)
|
||||
else:
|
||||
logger.debug("[点击操作] 步骤 3/5: 跳过随机抖动")
|
||||
|
||||
# 使用 SendInput 移动鼠标到目标位置
|
||||
logger.debug("[点击操作] 步骤 4/5: 移动鼠标到目标位置")
|
||||
self._move_mouse_absolute(screen_x, screen_y)
|
||||
self._human_like_delay(0.05)
|
||||
|
||||
# 按下
|
||||
logger.debug(f"[点击操作] 步骤 5/5: 执行{action}")
|
||||
self._mouse_down(button)
|
||||
|
||||
if duration > 0:
|
||||
logger.debug(f"[点击操作] 长按等待: {duration}s")
|
||||
time.sleep(duration)
|
||||
else:
|
||||
self._human_like_delay(0.05)
|
||||
|
||||
# 释放
|
||||
logger.debug("[点击操作] 释放鼠标按钮")
|
||||
self._mouse_up(button)
|
||||
|
||||
logger.info(f"[点击操作] {action}完成: ({x}, {y}) 按钮: {button}, 时长: {duration}s")
|
||||
|
||||
def swipe(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
||||
duration: float = 0.5, button: str = "left"):
|
||||
"""
|
||||
从起点滑动到终点
|
||||
|
||||
Args:
|
||||
start_x, start_y: 起点坐标(窗口内相对坐标)
|
||||
end_x, end_y: 终点坐标(窗口内相对坐标)
|
||||
duration: 滑动持续时间(秒)
|
||||
button: 使用的鼠标按钮
|
||||
"""
|
||||
logger.info(f"[滑动操作] 开始滑动: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s, 按钮: {button}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[滑动操作] 步骤 1/4: 激活窗口")
|
||||
self._activate_window()
|
||||
time.sleep(0.1)
|
||||
|
||||
# 转换坐标
|
||||
logger.debug("[滑动操作] 步骤 2/4: 转换起点和终点坐标为屏幕坐标")
|
||||
start_screen = self._to_screen_coords(start_x, start_y)
|
||||
end_screen = self._to_screen_coords(end_x, end_y)
|
||||
logger.info(f"[滑动操作] 屏幕坐标: ({start_screen[0]}, {start_screen[1]}) -> ({end_screen[0]}, {end_screen[1]})")
|
||||
|
||||
# 计算步进
|
||||
steps = max(int(duration * 60), 10)
|
||||
step_delay = duration / steps
|
||||
logger.debug(f"[滑动操作] 滑动参数: 总步数={steps}, 每步延迟={step_delay:.4f}s")
|
||||
|
||||
dx = (end_screen[0] - start_screen[0]) / steps
|
||||
dy = (end_screen[1] - start_screen[1]) / steps
|
||||
logger.debug(f"[滑动操作] 每步移动量: dx={dx:.2f}, dy={dy:.2f}")
|
||||
|
||||
# 移动到起点并按下
|
||||
logger.debug("[滑动操作] 步骤 3/4: 移动到起点并按下鼠标")
|
||||
self._move_mouse_absolute(start_screen[0], start_screen[1])
|
||||
time.sleep(0.05)
|
||||
self._mouse_down(button)
|
||||
time.sleep(0.05)
|
||||
|
||||
# 滑动过程
|
||||
logger.debug("[滑动操作] 开始滑动过程")
|
||||
for i in range(1, steps + 1):
|
||||
new_x = int(start_screen[0] + dx * i)
|
||||
new_y = int(start_screen[1] + dy * i)
|
||||
self._move_mouse_absolute(new_x, new_y)
|
||||
time.sleep(step_delay)
|
||||
|
||||
# 确保到达终点
|
||||
logger.debug("[滑动操作] 步骤 4/4: 确保到达终点并释放鼠标")
|
||||
self._move_mouse_absolute(end_screen[0], end_screen[1])
|
||||
time.sleep(0.05)
|
||||
|
||||
# 释放
|
||||
self._mouse_up(button)
|
||||
|
||||
logger.info(f"[滑动操作] 滑动完成: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s")
|
||||
|
||||
def key_press(self, key: str, duration: float = 0.05):
|
||||
"""
|
||||
按下并释放按键
|
||||
|
||||
Args:
|
||||
key: 按键名称,如 'a', 'enter', 'esc', 'space' 等
|
||||
duration: 按住时长(秒)
|
||||
"""
|
||||
logger.info(f"[按键操作] 开始按键: '{key}', 按住时长: {duration}s")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键操作] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键操作] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
# 激活窗口
|
||||
logger.debug("[按键操作] 步骤 1/3: 激活窗口")
|
||||
self._activate_window()
|
||||
time.sleep(0.05)
|
||||
|
||||
# 按下
|
||||
logger.debug("[按键操作] 步骤 2/3: 按下按键")
|
||||
self._key_down(vk_code)
|
||||
time.sleep(duration)
|
||||
|
||||
# 释放
|
||||
logger.debug("[按键操作] 步骤 3/3: 释放按键")
|
||||
self._key_up(vk_code)
|
||||
|
||||
logger.info(f"[按键操作] 按键完成: '{key}', 按住时长: {duration}s")
|
||||
|
||||
def key_down(self, key: str):
|
||||
"""按住按键(不释放)"""
|
||||
logger.info(f"[按键按住] 开始按住按键: '{key}'")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键按住] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键按住] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
self._activate_window()
|
||||
self._key_down(vk_code)
|
||||
|
||||
logger.info(f"[按键按住] 按键 '{key}' 已按下(未释放)")
|
||||
|
||||
def key_up(self, key: str):
|
||||
"""释放按键"""
|
||||
logger.info(f"[按键释放] 开始释放按键: '{key}'")
|
||||
|
||||
vk_code = self._get_vk_code(key)
|
||||
if vk_code is None:
|
||||
logger.error(f"[按键释放] 未知的按键: '{key}'")
|
||||
return
|
||||
|
||||
logger.debug(f"[按键释放] 按键 '{key}' 对应的虚拟键码: {vk_code}")
|
||||
|
||||
self._key_up(vk_code)
|
||||
|
||||
logger.info(f"[按键释放] 按键 '{key}' 已释放")
|
||||
|
||||
def _get_vk_code(self, key: str) -> Optional[int]:
|
||||
"""获取按键的虚拟键码"""
|
||||
import win32con
|
||||
|
||||
key = key.lower()
|
||||
|
||||
# 字母数字
|
||||
if len(key) == 1 and key.isalnum():
|
||||
vk_code = ord(key.upper())
|
||||
logger.debug(f"[虚拟键码] 字母/数字 '{key}' -> {vk_code}")
|
||||
return vk_code
|
||||
|
||||
# 特殊按键映射
|
||||
special_keys = {
|
||||
'enter': win32con.VK_RETURN,
|
||||
'return': win32con.VK_RETURN,
|
||||
'esc': win32con.VK_ESCAPE,
|
||||
'escape': win32con.VK_ESCAPE,
|
||||
'space': win32con.VK_SPACE,
|
||||
'tab': win32con.VK_TAB,
|
||||
'backspace': win32con.VK_BACK,
|
||||
'delete': win32con.VK_DELETE,
|
||||
'del': win32con.VK_DELETE,
|
||||
'insert': win32con.VK_INSERT,
|
||||
'ins': win32con.VK_INSERT,
|
||||
'home': win32con.VK_HOME,
|
||||
'end': win32con.VK_END,
|
||||
'pageup': win32con.VK_PRIOR,
|
||||
'pagedown': win32con.VK_NEXT,
|
||||
'up': win32con.VK_UP,
|
||||
'down': win32con.VK_DOWN,
|
||||
'left': win32con.VK_LEFT,
|
||||
'right': win32con.VK_RIGHT,
|
||||
'f1': win32con.VK_F1,
|
||||
'f2': win32con.VK_F2,
|
||||
'f3': win32con.VK_F3,
|
||||
'f4': win32con.VK_F4,
|
||||
'f5': win32con.VK_F5,
|
||||
'f6': win32con.VK_F6,
|
||||
'f7': win32con.VK_F7,
|
||||
'f8': win32con.VK_F8,
|
||||
'f9': win32con.VK_F9,
|
||||
'f10': win32con.VK_F10,
|
||||
'f11': win32con.VK_F11,
|
||||
'f12': win32con.VK_F12,
|
||||
'ctrl': win32con.VK_CONTROL,
|
||||
'control': win32con.VK_CONTROL,
|
||||
'shift': win32con.VK_SHIFT,
|
||||
'alt': win32con.VK_MENU,
|
||||
'win': win32con.VK_LWIN,
|
||||
'windows': win32con.VK_LWIN,
|
||||
}
|
||||
|
||||
vk_code = special_keys.get(key)
|
||||
if vk_code:
|
||||
logger.debug(f"[虚拟键码] 特殊键 '{key}' -> {vk_code}")
|
||||
else:
|
||||
logger.warning(f"[虚拟键码] 未找到 '{key}' 对应的虚拟键码")
|
||||
|
||||
return vk_code
|
||||
|
||||
# ==================== 便捷方法 ====================
|
||||
|
||||
def double_click(self, x: int, y: int, button: str = "left"):
|
||||
"""双击"""
|
||||
logger.info(f"[双击操作] 开始在 ({x}, {y}) 双击")
|
||||
|
||||
self.click(x, y, duration=0, button=button)
|
||||
logger.debug("[双击操作] 第一次点击完成,等待 0.1s")
|
||||
time.sleep(0.1)
|
||||
self.click(x, y, duration=0, button=button)
|
||||
|
||||
logger.info(f"[双击操作] 双击完成: ({x}, {y})")
|
||||
|
||||
def long_press(self, x: int, y: int, duration: float = 1.0, button: str = "left"):
|
||||
"""长按(click的别名,更清晰)"""
|
||||
logger.info(f"[长按操作] 开始在 ({x}, {y}) 长按 {duration}s")
|
||||
self.click(x, y, duration=duration, button=button)
|
||||
logger.info(f"[长按操作] 长按完成: ({x}, {y}), 时长: {duration}s")
|
||||
|
||||
def drag(self, start_x: int, start_y: int, end_x: int, end_y: int,
|
||||
duration: float = 0.5, button: str = "left"):
|
||||
"""拖拽(swipe的别名,更清晰)"""
|
||||
logger.info(f"[拖拽操作] 开始拖拽: ({start_x}, {start_y}) -> ({end_x}, {end_y}), 时长: {duration}s")
|
||||
self.swipe(start_x, start_y, end_x, end_y, duration, button)
|
||||
logger.info(f"[拖拽操作] 拖拽完成: ({start_x}, {start_y}) -> ({end_x}, {end_y})")
|
||||
Reference in New Issue
Block a user