kotonebot 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kotonebot/__init__.py +39 -39
- kotonebot/backend/bot.py +312 -312
- kotonebot/backend/color.py +525 -525
- kotonebot/backend/context/__init__.py +3 -3
- kotonebot/backend/context/context.py +1002 -1002
- kotonebot/backend/context/task_action.py +183 -183
- kotonebot/backend/core.py +86 -129
- kotonebot/backend/debug/entry.py +89 -89
- kotonebot/backend/debug/mock.py +78 -78
- kotonebot/backend/debug/server.py +222 -222
- kotonebot/backend/debug/vars.py +351 -351
- kotonebot/backend/dispatch.py +227 -227
- kotonebot/backend/flow_controller.py +196 -196
- kotonebot/backend/image.py +36 -5
- kotonebot/backend/loop.py +222 -208
- kotonebot/backend/ocr.py +535 -535
- kotonebot/backend/preprocessor.py +103 -103
- kotonebot/client/__init__.py +9 -9
- kotonebot/client/device.py +369 -529
- kotonebot/client/fast_screenshot.py +377 -377
- kotonebot/client/host/__init__.py +43 -43
- kotonebot/client/host/adb_common.py +101 -107
- kotonebot/client/host/custom.py +118 -118
- kotonebot/client/host/leidian_host.py +196 -196
- kotonebot/client/host/mumu12_host.py +353 -353
- kotonebot/client/host/protocol.py +214 -214
- kotonebot/client/host/windows_common.py +73 -58
- kotonebot/client/implements/__init__.py +65 -70
- kotonebot/client/implements/adb.py +89 -89
- kotonebot/client/implements/nemu_ipc/__init__.py +11 -11
- kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py +284 -284
- kotonebot/client/implements/nemu_ipc/nemu_ipc.py +327 -327
- kotonebot/client/implements/remote_windows.py +188 -188
- kotonebot/client/implements/uiautomator2.py +85 -85
- kotonebot/client/implements/windows/__init__.py +1 -0
- kotonebot/client/implements/windows/print_window.py +133 -0
- kotonebot/client/implements/windows/send_message.py +324 -0
- kotonebot/client/implements/{windows.py → windows/windows.py} +175 -176
- kotonebot/client/protocol.py +69 -69
- kotonebot/client/registration.py +24 -24
- kotonebot/client/scaler.py +467 -0
- kotonebot/config/base_config.py +103 -96
- kotonebot/config/config.py +61 -0
- kotonebot/config/manager.py +36 -36
- kotonebot/core/__init__.py +13 -0
- kotonebot/core/entities/base.py +182 -0
- kotonebot/core/entities/compound.py +75 -0
- kotonebot/core/entities/ocr.py +117 -0
- kotonebot/core/entities/template_match.py +198 -0
- kotonebot/devtools/__init__.py +42 -0
- kotonebot/devtools/cli/__init__.py +6 -0
- kotonebot/devtools/cli/main.py +53 -0
- kotonebot/{tools → devtools}/mirror.py +354 -354
- kotonebot/devtools/project/project.py +41 -0
- kotonebot/devtools/project/scanner.py +202 -0
- kotonebot/devtools/project/schema.py +99 -0
- kotonebot/devtools/resgen/__init__.py +42 -0
- kotonebot/devtools/resgen/codegen.py +331 -0
- kotonebot/devtools/resgen/core.py +94 -0
- kotonebot/devtools/resgen/parsers.py +360 -0
- kotonebot/devtools/resgen/utils.py +158 -0
- kotonebot/devtools/resgen/validation.py +115 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BOrJxbIo.woff +0 -0
- kotonebot/devtools/web/dist/assets/bootstrap-icons-BtvjY1KL.woff2 +0 -0
- kotonebot/devtools/web/dist/assets/ext-language_tools-CD021WJ2.js +2577 -0
- kotonebot/devtools/web/dist/assets/index-B_m5f2LF.js +2836 -0
- kotonebot/devtools/web/dist/assets/index-BlEDyGGa.css +9 -0
- kotonebot/devtools/web/dist/assets/language-client-C9muzqaq.js +128 -0
- kotonebot/devtools/web/dist/assets/mode-python-CtHp76XS.js +476 -0
- kotonebot/devtools/web/dist/icons/symbol-class.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-file.svg +3 -0
- kotonebot/devtools/web/dist/icons/symbol-method.svg +3 -0
- kotonebot/devtools/web/dist/index.html +25 -0
- kotonebot/devtools/web/server/__init__.py +0 -0
- kotonebot/devtools/web/server/rest_api.py +217 -0
- kotonebot/devtools/web/server/server.py +85 -0
- kotonebot/errors.py +76 -76
- kotonebot/interop/win/__init__.py +13 -9
- kotonebot/interop/win/_mouse.py +310 -310
- kotonebot/interop/win/message_box.py +313 -313
- kotonebot/interop/win/reg.py +37 -37
- kotonebot/interop/win/shake_mouse.py +224 -0
- kotonebot/interop/win/shortcut.py +43 -43
- kotonebot/interop/win/task_dialog.py +513 -513
- kotonebot/interop/win/window.py +89 -0
- kotonebot/logging/__init__.py +2 -2
- kotonebot/logging/log.py +17 -17
- kotonebot/primitives/__init__.py +19 -17
- kotonebot/primitives/geometry.py +1067 -862
- kotonebot/primitives/visual.py +143 -63
- kotonebot/ui/file_host/sensio.py +36 -36
- kotonebot/ui/file_host/tmp_send.py +54 -54
- kotonebot/ui/pushkit/__init__.py +3 -3
- kotonebot/ui/pushkit/image_host.py +88 -88
- kotonebot/ui/pushkit/protocol.py +13 -13
- kotonebot/ui/pushkit/wxpusher.py +54 -54
- kotonebot/ui/user.py +148 -148
- kotonebot/util.py +436 -436
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/METADATA +84 -82
- kotonebot-0.7.0.dist-info/RECORD +109 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/WHEEL +1 -1
- kotonebot-0.7.0.dist-info/entry_points.txt +2 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/licenses/LICENSE +673 -673
- kotonebot/client/implements/adb_raw.py +0 -163
- kotonebot-0.5.0.dist-info/RECORD +0 -71
- /kotonebot/{tools → devtools/project}/__init__.py +0 -0
- {kotonebot-0.5.0.dist-info → kotonebot-0.7.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
from kotonebot.util import require_windows
|
|
3
|
+
require_windows('"WindowsImpl" implementation')
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import ctypes.wintypes as wt
|
|
7
|
+
from typing import TYPE_CHECKING, Literal
|
|
8
|
+
|
|
9
|
+
import cv2
|
|
10
|
+
from cv2.typing import MatLike
|
|
11
|
+
import numpy as np
|
|
12
|
+
import win32ui
|
|
13
|
+
import win32con
|
|
14
|
+
import win32gui
|
|
15
|
+
|
|
16
|
+
from ...protocol import Screenshotable
|
|
17
|
+
from kotonebot.interop.win.window import Win32Window
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from ...device import Device
|
|
20
|
+
|
|
21
|
+
PW_CLIENTONLY = 0x1
|
|
22
|
+
PW_RENDERFULLCONTENT = 0x2
|
|
23
|
+
|
|
24
|
+
# TODO: 目前每次截图都会完整创建和销毁 GDI 对象,性能较差,后续可以考虑缓存这些对象以提升性能
|
|
25
|
+
# TODO: 需要先支持 Impl 的生命周期管理
|
|
26
|
+
def capture_printwindow(hwnd: int) -> MatLike:
|
|
27
|
+
# client rect size
|
|
28
|
+
left, top, right, bottom = win32gui.GetClientRect(hwnd)
|
|
29
|
+
width, height = right - left, bottom - top
|
|
30
|
+
if width <= 0 or height <= 0:
|
|
31
|
+
raise RuntimeError("invalid client size")
|
|
32
|
+
|
|
33
|
+
hdc = win32gui.GetDC(hwnd)
|
|
34
|
+
mfc_dc = win32ui.CreateDCFromHandle(hdc)
|
|
35
|
+
mem_dc = mfc_dc.CreateCompatibleDC()
|
|
36
|
+
bmp = win32ui.CreateBitmap()
|
|
37
|
+
bmp.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
38
|
+
mem_dc.SelectObject(bmp)
|
|
39
|
+
|
|
40
|
+
flags = PW_CLIENTONLY | PW_RENDERFULLCONTENT
|
|
41
|
+
res = ctypes.windll.user32.PrintWindow(hwnd, mem_dc.GetSafeHdc(), flags)
|
|
42
|
+
if res != 1:
|
|
43
|
+
raise RuntimeError("PrintWindow failed")
|
|
44
|
+
|
|
45
|
+
# extract BGRA bits via GetDIBits (pywin32 does not expose BITMAPINFO)
|
|
46
|
+
class BITMAPINFOHEADER(ctypes.Structure):
|
|
47
|
+
_fields_ = [
|
|
48
|
+
("biSize", wt.DWORD),
|
|
49
|
+
("biWidth", wt.LONG),
|
|
50
|
+
("biHeight", wt.LONG),
|
|
51
|
+
("biPlanes", wt.WORD),
|
|
52
|
+
("biBitCount", wt.WORD),
|
|
53
|
+
("biCompression", wt.DWORD),
|
|
54
|
+
("biSizeImage", wt.DWORD),
|
|
55
|
+
("biXPelsPerMeter", wt.LONG),
|
|
56
|
+
("biYPelsPerMeter", wt.LONG),
|
|
57
|
+
("biClrUsed", wt.DWORD),
|
|
58
|
+
("biClrImportant", wt.DWORD),
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
class BITMAPINFO(ctypes.Structure):
|
|
62
|
+
_fields_ = [("bmiHeader", BITMAPINFOHEADER), ("bmiColors", wt.DWORD * 3)]
|
|
63
|
+
|
|
64
|
+
bmi = BITMAPINFO()
|
|
65
|
+
bmi.bmiHeader.biSize = ctypes.sizeof(BITMAPINFOHEADER)
|
|
66
|
+
bmi.bmiHeader.biWidth = width
|
|
67
|
+
bmi.bmiHeader.biHeight = -height # top-down
|
|
68
|
+
bmi.bmiHeader.biPlanes = 1
|
|
69
|
+
bmi.bmiHeader.biBitCount = 32
|
|
70
|
+
bmi.bmiHeader.biCompression = win32con.BI_RGB
|
|
71
|
+
|
|
72
|
+
buf = bytearray(width * height * 4)
|
|
73
|
+
bits_ok = ctypes.windll.gdi32.GetDIBits(
|
|
74
|
+
mem_dc.GetSafeHdc(),
|
|
75
|
+
bmp.GetHandle(),
|
|
76
|
+
0,
|
|
77
|
+
height,
|
|
78
|
+
ctypes.byref((ctypes.c_ubyte * len(buf)).from_buffer(buf)),
|
|
79
|
+
ctypes.byref(bmi),
|
|
80
|
+
win32con.DIB_RGB_COLORS,
|
|
81
|
+
)
|
|
82
|
+
if bits_ok == 0:
|
|
83
|
+
raise RuntimeError("GetDIBits failed")
|
|
84
|
+
|
|
85
|
+
img = np.frombuffer(buf, dtype=np.uint8).reshape((height, width, 4))
|
|
86
|
+
img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR)
|
|
87
|
+
|
|
88
|
+
# cleanup
|
|
89
|
+
win32gui.DeleteObject(bmp.GetHandle())
|
|
90
|
+
mem_dc.DeleteDC()
|
|
91
|
+
mfc_dc.DeleteDC()
|
|
92
|
+
win32gui.ReleaseDC(hwnd, hdc)
|
|
93
|
+
return img
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class PrintWindowImpl(Screenshotable):
|
|
97
|
+
def __init__(self, device: 'Device', window_title: str):
|
|
98
|
+
self.window = Win32Window.require_window('title', window_title)
|
|
99
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
100
|
+
|
|
101
|
+
def __client_rect(self) -> tuple[int, int, int, int]:
|
|
102
|
+
"""获取 Client 区域屏幕坐标"""
|
|
103
|
+
hwnd = self.window.hwnd
|
|
104
|
+
client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd)
|
|
105
|
+
client_left, client_top = win32gui.ClientToScreen(hwnd, (client_left, client_top))
|
|
106
|
+
client_right, client_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom))
|
|
107
|
+
return client_left, client_top, client_right, client_bottom
|
|
108
|
+
|
|
109
|
+
def detect_orientation(self) -> None | Literal['portrait'] | Literal['landscape']:
|
|
110
|
+
rect = self.window.get_rect()
|
|
111
|
+
if rect.w > rect.h:
|
|
112
|
+
return 'landscape'
|
|
113
|
+
else:
|
|
114
|
+
return 'portrait'
|
|
115
|
+
|
|
116
|
+
@property
|
|
117
|
+
def screen_size(self) -> tuple[int, int]:
|
|
118
|
+
left, top, right, bot = self.__client_rect()
|
|
119
|
+
w = right - left
|
|
120
|
+
h = bot - top
|
|
121
|
+
return w, h
|
|
122
|
+
|
|
123
|
+
def screenshot(self) -> MatLike:
|
|
124
|
+
if self.window.is_minimized():
|
|
125
|
+
self.window.restore()
|
|
126
|
+
return capture_printwindow(self.window.hwnd)
|
|
127
|
+
|
|
128
|
+
if __name__ == "__main__":
|
|
129
|
+
impl = PrintWindowImpl(None, "gakumas") # type: ignore
|
|
130
|
+
while True:
|
|
131
|
+
img = impl.screenshot()
|
|
132
|
+
cv2.imshow("screenshot", img)
|
|
133
|
+
cv2.waitKey(1)
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
# ruff: noqa: E402
|
|
2
|
+
from kotonebot.util import require_windows
|
|
3
|
+
require_windows('"WindowsImpl" implementation')
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from time import sleep
|
|
7
|
+
from typing_extensions import assert_never
|
|
8
|
+
from typing import Optional, Literal, TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
import win32gui
|
|
11
|
+
import win32con
|
|
12
|
+
|
|
13
|
+
from ...protocol import Touchable
|
|
14
|
+
from kotonebot.interop.win.window import Win32Window
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from ...device import Device
|
|
17
|
+
|
|
18
|
+
MouseButton = Literal['left', 'right', 'middle']
|
|
19
|
+
|
|
20
|
+
def _make_lparam(x: int, y: int) -> int:
|
|
21
|
+
"""
|
|
22
|
+
创建 LPARAM 参数,打包 x,y 坐标
|
|
23
|
+
|
|
24
|
+
:param x: X 坐标
|
|
25
|
+
:param y: Y 坐标
|
|
26
|
+
:returns: 打包的 LPARAM 值
|
|
27
|
+
"""
|
|
28
|
+
return (y << 16) | (x & 0xFFFF)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _make_wparam(button_data: int, wheel_delta: int = 0) -> int:
|
|
32
|
+
"""
|
|
33
|
+
创建 WPARAM 参数,用于滚轮消息
|
|
34
|
+
|
|
35
|
+
:param button_data: 按钮数据
|
|
36
|
+
:param wheel_delta: 滚轮增量
|
|
37
|
+
:returns: 打包的 WPARAM 值
|
|
38
|
+
"""
|
|
39
|
+
return (wheel_delta << 16) | (button_data & 0xFFFF)
|
|
40
|
+
|
|
41
|
+
def _wait_cursor_idle(max_speed: float = 50):
|
|
42
|
+
if max_speed <= 0:
|
|
43
|
+
return
|
|
44
|
+
sample_interval = 0.05
|
|
45
|
+
prev_pos = win32gui.GetCursorPos()
|
|
46
|
+
prev_t = time.monotonic()
|
|
47
|
+
|
|
48
|
+
while True:
|
|
49
|
+
sleep(sample_interval)
|
|
50
|
+
cur_pos = win32gui.GetCursorPos()
|
|
51
|
+
cur_t = time.monotonic()
|
|
52
|
+
|
|
53
|
+
dx = cur_pos[0] - prev_pos[0]
|
|
54
|
+
dy = cur_pos[1] - prev_pos[1]
|
|
55
|
+
dist = (dx * dx + dy * dy) ** 0.5
|
|
56
|
+
dt = cur_t - prev_t
|
|
57
|
+
speed = dist / dt if dt > 0 else float('inf')
|
|
58
|
+
if speed <= max_speed:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
prev_pos = cur_pos
|
|
62
|
+
prev_t = cur_t
|
|
63
|
+
|
|
64
|
+
class SendMessageWrapper:
|
|
65
|
+
def __init__(self, window: Win32Window, wait_cursor_idle: float = -1):
|
|
66
|
+
self.window = window
|
|
67
|
+
self.last_pos = (0, 0)
|
|
68
|
+
self.last_pos_set = False
|
|
69
|
+
if wait_cursor_idle == -1:
|
|
70
|
+
self.wait_cursor_idle_speed = 50 # 默认值
|
|
71
|
+
else:
|
|
72
|
+
self.wait_cursor_idle_speed = wait_cursor_idle
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def hwnd(self) -> int:
|
|
76
|
+
return self.window.hwnd
|
|
77
|
+
|
|
78
|
+
def _send_activate(self):
|
|
79
|
+
self.window.post_message(win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
|
|
80
|
+
|
|
81
|
+
def _align_window(self, target_client_x: int, target_client_y: int) -> bool:
|
|
82
|
+
"""
|
|
83
|
+
移动窗口,使得目标客户区坐标对齐到指定的光标位置。
|
|
84
|
+
如果提供 `cursor_pos` 则使用该屏幕坐标(例如预测值),否则使用真实光标位置。
|
|
85
|
+
"""
|
|
86
|
+
window_rect = self.window.get_rect()
|
|
87
|
+
if not window_rect:
|
|
88
|
+
return False
|
|
89
|
+
|
|
90
|
+
cursor_x, cursor_y = win32gui.GetCursorPos()
|
|
91
|
+
# 计算客户区偏移
|
|
92
|
+
client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(self.hwnd)
|
|
93
|
+
client_screen_left, client_screen_top = win32gui.ClientToScreen(self.hwnd, (client_left, client_top))
|
|
94
|
+
offset_x = client_screen_left - window_rect.x1
|
|
95
|
+
offset_y = client_screen_top - window_rect.y1
|
|
96
|
+
|
|
97
|
+
new_window_x = cursor_x - target_client_x - offset_x
|
|
98
|
+
new_window_y = cursor_y - target_client_y - offset_y
|
|
99
|
+
|
|
100
|
+
win32gui.SetWindowPos(
|
|
101
|
+
self.hwnd,
|
|
102
|
+
None,
|
|
103
|
+
new_window_x,
|
|
104
|
+
new_window_y,
|
|
105
|
+
window_rect.w,
|
|
106
|
+
window_rect.h,
|
|
107
|
+
win32con.SWP_NOREDRAW | win32con.SWP_NOACTIVATE |
|
|
108
|
+
win32con.SWP_NOZORDER | win32con.SWP_NOCOPYBITS |
|
|
109
|
+
win32con.SWP_NOSENDCHANGING | win32con.SWP_NOOWNERZORDER
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
def _send_mouse_button(self, x: int, y: int, button: MouseButton, down: bool) -> bool:
|
|
115
|
+
if down:
|
|
116
|
+
match button:
|
|
117
|
+
case 'left':
|
|
118
|
+
msg = win32con.WM_LBUTTONDOWN
|
|
119
|
+
w_param = win32con.MK_LBUTTON
|
|
120
|
+
case 'right':
|
|
121
|
+
msg = win32con.WM_RBUTTONDOWN
|
|
122
|
+
w_param = win32con.MK_RBUTTON
|
|
123
|
+
case 'middle':
|
|
124
|
+
msg = win32con.WM_MBUTTONDOWN
|
|
125
|
+
w_param = win32con.MK_MBUTTON
|
|
126
|
+
case _:
|
|
127
|
+
assert_never("Unknown mouse button")
|
|
128
|
+
else:
|
|
129
|
+
match button:
|
|
130
|
+
case 'left':
|
|
131
|
+
msg = win32con.WM_LBUTTONUP
|
|
132
|
+
w_param = 0
|
|
133
|
+
case 'right':
|
|
134
|
+
msg = win32con.WM_RBUTTONUP
|
|
135
|
+
w_param = 0
|
|
136
|
+
case 'middle':
|
|
137
|
+
msg = win32con.WM_MBUTTONUP
|
|
138
|
+
w_param = 0
|
|
139
|
+
case _:
|
|
140
|
+
assert_never("Unknown mouse button")
|
|
141
|
+
|
|
142
|
+
l_param = _make_lparam(x, y)
|
|
143
|
+
return self.window.post_message(msg, w_param, l_param)
|
|
144
|
+
|
|
145
|
+
def _send_mouse_move(self, x: int, y: int, button: Optional[MouseButton] = None) -> bool:
|
|
146
|
+
"""
|
|
147
|
+
发送鼠标移动消息,支持在按键按下时携带对应的 wParam 标志(例如拖拽时带 MK_LBUTTON)。
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
if button == 'left':
|
|
151
|
+
w_param = win32con.MK_LBUTTON
|
|
152
|
+
elif button == 'right':
|
|
153
|
+
w_param = win32con.MK_RBUTTON
|
|
154
|
+
elif button == 'middle':
|
|
155
|
+
w_param = win32con.MK_MBUTTON
|
|
156
|
+
else:
|
|
157
|
+
w_param = 0
|
|
158
|
+
|
|
159
|
+
l_param = _make_lparam(x, y)
|
|
160
|
+
return self.window.post_message(win32con.WM_MOUSEMOVE, w_param, l_param)
|
|
161
|
+
|
|
162
|
+
def mouse_down(self, x: int, y: int, button: MouseButton) -> bool:
|
|
163
|
+
"""
|
|
164
|
+
发送鼠标按下消息
|
|
165
|
+
button: 0=左键, 1=右键, 2=中键
|
|
166
|
+
|
|
167
|
+
:param x: X 坐标
|
|
168
|
+
:param y: Y 坐标
|
|
169
|
+
:param button: 按钮类型,0=左键, 1=右键, 2=中键
|
|
170
|
+
:returns: 操作是否成功
|
|
171
|
+
"""
|
|
172
|
+
self._send_activate()
|
|
173
|
+
self._align_window(x, y)
|
|
174
|
+
return self._send_mouse_button(x, y, button, down=True)
|
|
175
|
+
|
|
176
|
+
def mouse_up(self, x: int, y: int, button: MouseButton) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
发送鼠标释放消息
|
|
179
|
+
button: 0=左键, 1=右键, 2=中键
|
|
180
|
+
|
|
181
|
+
:param x: X 坐标
|
|
182
|
+
:param y: Y 坐标
|
|
183
|
+
:param button: 按钮类型,0=左键, 1=右键, 2=中键
|
|
184
|
+
:returns: 操作是否成功
|
|
185
|
+
"""
|
|
186
|
+
self._send_activate()
|
|
187
|
+
self._align_window(x, y)
|
|
188
|
+
return self._send_mouse_button(x, y, button, down=False)
|
|
189
|
+
|
|
190
|
+
def click(self, x: int, y: int, *, button: MouseButton = 'left') -> bool:
|
|
191
|
+
"""
|
|
192
|
+
发送点击事件。
|
|
193
|
+
|
|
194
|
+
:param x: X 坐标
|
|
195
|
+
:param y: Y 坐标
|
|
196
|
+
:param button: 按钮类型,0=左键, 1=右键, 2=中键
|
|
197
|
+
:returns: 操作是否成功
|
|
198
|
+
"""
|
|
199
|
+
# 为避免在一次点击操作中重复激活 -> 先激活一次,然后在 down/up 中禁用额外激活
|
|
200
|
+
self._send_activate()
|
|
201
|
+
_wait_cursor_idle(self.wait_cursor_idle_speed)
|
|
202
|
+
self._align_window(x, y)
|
|
203
|
+
if self._send_mouse_button(x, y, button, down=True):
|
|
204
|
+
return self._send_mouse_button(x, y, button, down=False)
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
def keyboard_down(self, key_code: int) -> bool:
|
|
208
|
+
"""
|
|
209
|
+
发送键盘按下消息
|
|
210
|
+
|
|
211
|
+
:param key_code: 虚拟键码,例如 win32con.VK_RETURN 等
|
|
212
|
+
:returns: 操作是否成功
|
|
213
|
+
"""
|
|
214
|
+
# 发送激活消息
|
|
215
|
+
self._send_activate()
|
|
216
|
+
|
|
217
|
+
# 发送 WM_KEYDOWN 消息
|
|
218
|
+
result = self.window.post_message(win32con.WM_KEYDOWN, key_code, 0)
|
|
219
|
+
return result
|
|
220
|
+
|
|
221
|
+
def keyboard_up(self, key_code: int) -> bool:
|
|
222
|
+
"""
|
|
223
|
+
发送键盘释放消息
|
|
224
|
+
|
|
225
|
+
:param key_code: 虚拟键码,例如 win32con.VK_RETURN 等
|
|
226
|
+
:returns: 操作是否成功
|
|
227
|
+
"""
|
|
228
|
+
# 发送激活消息
|
|
229
|
+
self._send_activate()
|
|
230
|
+
return self.window.post_message(win32con.WM_KEYUP, key_code, 0)
|
|
231
|
+
|
|
232
|
+
def drag(
|
|
233
|
+
self,
|
|
234
|
+
x1: int, y1: int,
|
|
235
|
+
x2: int, y2: int,
|
|
236
|
+
*,
|
|
237
|
+
button: MouseButton = 'left',
|
|
238
|
+
duration: float | None = None
|
|
239
|
+
) -> bool:
|
|
240
|
+
"""
|
|
241
|
+
从指定点拖拽到指定点。
|
|
242
|
+
|
|
243
|
+
:param x1: 起始点 X 坐标,相对于客户区。
|
|
244
|
+
:param y1: 起始点 Y 坐标,相对于客户区。
|
|
245
|
+
:param x2: 结束点 X 坐标,相对于客户区。
|
|
246
|
+
:param y2: 结束点 Y 坐标,相对于客户区。
|
|
247
|
+
:param button: 按钮类型,'left'、'right'、'middle'。
|
|
248
|
+
:returns: 操作是否成功
|
|
249
|
+
"""
|
|
250
|
+
if duration is None:
|
|
251
|
+
duration = 0.5
|
|
252
|
+
|
|
253
|
+
self._send_activate()
|
|
254
|
+
# 将窗口对齐到起点,确保起始客户区坐标与当前光标对齐
|
|
255
|
+
self._align_window(x1, y1)
|
|
256
|
+
|
|
257
|
+
# 发送按下
|
|
258
|
+
if not self._send_mouse_button(x1, y1, button, down=True):
|
|
259
|
+
return False
|
|
260
|
+
|
|
261
|
+
# 如果 duration 为 0 或负数,直接跳到结束点并释放
|
|
262
|
+
if not duration or duration <= 0:
|
|
263
|
+
# 最后对齐到结束点以保证位置精确
|
|
264
|
+
self._align_window(x2, y2)
|
|
265
|
+
return self._send_mouse_button(x2, y2, button, down=False)
|
|
266
|
+
|
|
267
|
+
# 分段发送移动事件,避免直接跳到结束点
|
|
268
|
+
# 使用 60Hz 作为默认帧率,至少 1 步
|
|
269
|
+
fps = 60
|
|
270
|
+
steps = max(1, int(duration * fps))
|
|
271
|
+
interval = duration / steps - (13 / 1000) # 减去约 13ms 的消息处理时间
|
|
272
|
+
|
|
273
|
+
dx = x2 - x1
|
|
274
|
+
dy = y2 - y1
|
|
275
|
+
|
|
276
|
+
# 从 1 到 steps(包含终点)进行插值并发送 WM_MOUSEMOVE
|
|
277
|
+
for i in range(1, steps + 1):
|
|
278
|
+
t = i / steps
|
|
279
|
+
xi = int(x1 + dx * t)
|
|
280
|
+
yi = int(y1 + dy * t)
|
|
281
|
+
# 发送移动事件,保留当前按键状态
|
|
282
|
+
self._send_mouse_move(xi, yi, button=button)
|
|
283
|
+
self._align_window(xi, yi)
|
|
284
|
+
if interval > 0:
|
|
285
|
+
sleep(interval)
|
|
286
|
+
|
|
287
|
+
# 对齐到结束点并发送释放
|
|
288
|
+
self._align_window(x2, y2)
|
|
289
|
+
return self._send_mouse_button(x2, y2, button, down=False)
|
|
290
|
+
|
|
291
|
+
def drag_by(
|
|
292
|
+
self,
|
|
293
|
+
x: int, y: int,
|
|
294
|
+
dx: int, dy: int,
|
|
295
|
+
*,
|
|
296
|
+
button: MouseButton = 'left',
|
|
297
|
+
duration: float | None = None
|
|
298
|
+
) -> bool:
|
|
299
|
+
end_x = x + dx
|
|
300
|
+
end_y = y + dy
|
|
301
|
+
return self.drag(x, y, end_x, end_y, button=button, duration=duration)
|
|
302
|
+
|
|
303
|
+
class SendMessageImpl(Touchable):
|
|
304
|
+
def __init__(self, device: 'Device', window_title: str, *, wait_cursor_idle: float = -1) -> None:
|
|
305
|
+
self.device = device
|
|
306
|
+
window = Win32Window.require_window('title', window_title)
|
|
307
|
+
self.wrapper = SendMessageWrapper(window, wait_cursor_idle)
|
|
308
|
+
|
|
309
|
+
def click(self, x: int, y: int) -> None:
|
|
310
|
+
self.wrapper.click(x, y, button='left')
|
|
311
|
+
|
|
312
|
+
def swipe(self, x1: int, y1: int, x2: int, y2: int, duration: float | None = None) -> None:
|
|
313
|
+
ret = self.wrapper.drag(x1, y1, x2, y2, button='left', duration=duration)
|
|
314
|
+
if not ret:
|
|
315
|
+
raise RuntimeError('Swipe operation failed')
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
if __name__ == '__main__':
|
|
319
|
+
# impl = SendMessageImpl(None, window_title='gakumas') # type: ignore
|
|
320
|
+
# impl.click(0, 0)
|
|
321
|
+
|
|
322
|
+
while True:
|
|
323
|
+
_wait_cursor_idle()
|
|
324
|
+
print("Cursor idle detected")
|