修改游戏窗口获取逻辑以及完善获取模拟点击相关逻辑

This commit is contained in:
moweishan
2026-03-20 12:11:27 +08:00
parent b67c5be2f3
commit 62f717dde4
8 changed files with 2221 additions and 31 deletions

View File

@@ -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
View 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})")