kotonebot 0.6.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.
@@ -8,7 +8,7 @@ from kotonebot.util import require_windows
8
8
  from .protocol import Device, WindowsHostConfig, RemoteWindowsHostConfig
9
9
 
10
10
  logger = logging.getLogger(__name__)
11
- WindowsRecipes = Literal['windows', 'remote_windows']
11
+ WindowsRecipes = Literal['windows', 'remote_windows', 'windows_background']
12
12
 
13
13
  # Windows 相关的配置类型联合
14
14
  WindowsHostConfigs = WindowsHostConfig | RemoteWindowsHostConfig
@@ -41,6 +41,21 @@ class CommonWindowsCreateDeviceMixin(ABC):
41
41
  d._screenshot = impl
42
42
  d._touch = impl
43
43
  return d
44
+ case 'windows_background':
45
+ if not isinstance(config, WindowsHostConfig):
46
+ raise ValueError(f"Expected WindowsHostConfig for 'windows' recipe, got {type(config)}")
47
+ from kotonebot.client.implements.windows import WindowsImpl
48
+ d = WindowsDevice()
49
+ impl = WindowsImpl(
50
+ device=d,
51
+ window_title=config.window_title,
52
+ ahk_exe_path=config.ahk_exe_path
53
+ )
54
+ from kotonebot.client.implements.windows.send_message import SendMessageImpl
55
+ from kotonebot.client.implements.windows.print_window import PrintWindowImpl
56
+ d._screenshot = PrintWindowImpl(d, config.window_title)
57
+ d._touch = SendMessageImpl(d, config.window_title)
58
+ return d
44
59
  case 'remote_windows':
45
60
  if not isinstance(config, RemoteWindowsHostConfig):
46
61
  raise ValueError(f"Expected RemoteWindowsHostConfig for 'remote_windows' recipe, got {type(config)}")
@@ -0,0 +1 @@
1
+ from .windows import *
@@ -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")
@@ -5,7 +5,6 @@ require_windows('"WindowsImpl" implementation')
5
5
  from ctypes import windll
6
6
  from typing import Literal
7
7
  from importlib import resources
8
- from functools import cached_property
9
8
  from dataclasses import dataclass
10
9
 
11
10
  import cv2
@@ -19,9 +18,9 @@ except ImportError as _e:
19
18
  raise MissingDependencyError(_e, 'windows')
20
19
  from cv2.typing import MatLike
21
20
 
22
- from ..device import Device, WindowsDevice
23
- from ..protocol import Commandable, Touchable, Screenshotable
24
- from ..registration import ImplConfig
21
+ from ...device import Device
22
+ from ...protocol import Touchable, Screenshotable
23
+ from ...registration import ImplConfig
25
24
 
26
25
  # 1. 定义配置模型
27
26
  @dataclass
@@ -160,7 +159,7 @@ class WindowsImpl(Touchable, Screenshotable):
160
159
  self.ahk.mouse_drag(x2, y2, from_position=(x1, y1), coord_mode='Client', speed=10)
161
160
 
162
161
  if __name__ == '__main__':
163
- from ..device import Device
162
+ from ...device import Device
164
163
  device = Device()
165
164
  # 在测试环境中直接使用默认路径
166
165
  ahk_path = str(resources.files('kaa.res.bin') / 'AutoHotkey.exe')
@@ -6,7 +6,7 @@ from pydantic import BaseModel, ConfigDict
6
6
 
7
7
  T = TypeVar('T')
8
8
  BackendType = Literal['custom', 'mumu12', 'mumu12v5', 'leidian', 'dmm']
9
- DeviceRecipes = Literal['adb', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc']
9
+ DeviceRecipes = Literal['adb', 'uiautomator2', 'windows', 'remote_windows', 'nemu_ipc', 'windows_background']
10
10
 
11
11
  class ConfigBaseModel(BaseModel):
12
12
  model_config = ConfigDict(use_attribute_docstrings=True)
@@ -52,6 +52,13 @@ class BackendConfig(ConfigBaseModel):
52
52
  """MuMu12 模拟器后台保活模式"""
53
53
  target_screenshot_interval: float | None = None
54
54
  """最小截图间隔,单位为秒。为 None 时不限制截图速度。"""
55
+ cursor_wait_speed: float = -1
56
+ """
57
+ 使用 DMM 版后台挂机功能时,在点击前会尝试等待光标静止,以避免发生点击偏移。
58
+ 此项规定了速度小于多少时认为光标静止,单位为像素/秒。
59
+
60
+ -1 表示使用内置默认值,0 表示禁用该功能。
61
+ """
55
62
 
56
63
  class PushConfig(ConfigBaseModel):
57
64
  """推送配置。"""
@@ -5,8 +5,10 @@ require_windows('kotonebot.interop.win module')
5
5
 
6
6
  from . import _mouse as mouse
7
7
  from .shake_mouse import ShakeMouse
8
+ from .window import Win32Window, FindWindowMethod
8
9
 
9
10
  __all__ = [
10
11
  'mouse',
11
12
  'ShakeMouse',
13
+ 'Win32Window', 'FindWindowMethod'
12
14
  ]
@@ -0,0 +1,89 @@
1
+ from typing import Literal
2
+ from typing_extensions import assert_never
3
+
4
+ import win32gui
5
+ import win32con
6
+ import win32api
7
+
8
+ from kotonebot.primitives import Rect
9
+
10
+ FindWindowMethod = Literal['title']
11
+
12
+ class Win32Window:
13
+ def __init__(self, hwnd: int) -> None:
14
+ self.hwnd = hwnd
15
+
16
+ @staticmethod
17
+ def find_window(method: FindWindowMethod, title: str) -> 'Win32Window | None':
18
+ """查找窗口。
19
+
20
+ :param method: 查找依据。
21
+ :param title: 窗口标题
22
+ :return: 若找到窗口则返回 Win32Window 实例,否则返回 None。
23
+ """
24
+ match method:
25
+ case 'title':
26
+ hwnd = win32gui.FindWindow(None, title)
27
+ if hwnd == 0:
28
+ return None
29
+ return Win32Window(hwnd)
30
+ case _:
31
+ assert_never(method)
32
+
33
+ @staticmethod
34
+ def require_window(method: FindWindowMethod, title: str) -> 'Win32Window':
35
+ """查找窗口,未找到则抛出异常。
36
+
37
+ 参数同 :ref:`find_window`。
38
+ """
39
+ window = Win32Window.find_window(method, title)
40
+ if window is None:
41
+ raise RuntimeError(f'Window not found: {title}')
42
+ return window
43
+
44
+ def get_rect(self) -> Rect:
45
+ """取得窗口范围"""
46
+ left, top, right, bottom = win32gui.GetWindowRect(self.hwnd)
47
+ return Rect(left, top, right - left, bottom - top)
48
+
49
+ def get_client_rect(self) -> Rect:
50
+ """取得窗口客户区域范围"""
51
+ left, top, right, bottom = win32gui.GetClientRect(self.hwnd)
52
+ return Rect(left, top, right - left, bottom - top)
53
+
54
+ def is_active(self) -> bool:
55
+ """检查窗口是否为前台窗口"""
56
+ active_hwnd = win32gui.GetForegroundWindow()
57
+ return active_hwnd == self.hwnd
58
+
59
+ def is_minimized(self) -> bool:
60
+ """检查窗口是否最小化"""
61
+ return win32gui.IsIconic(self.hwnd) != 0
62
+
63
+ def restore(self) -> None:
64
+ """还原最小化的窗口"""
65
+ win32gui.ShowWindow(self.hwnd, win32con.SW_RESTORE)
66
+
67
+ def set_position(self, x: int, y: int, *, flags: int | None = None) -> None:
68
+ """
69
+ 设置窗口位置。
70
+
71
+ :param flags: SetWindowPos 的 `flags` 参数。默认参数为 SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE。
72
+ """
73
+ if flags is None:
74
+ flags = win32con.SWP_NOSIZE | win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE
75
+ win32gui.SetWindowPos(
76
+ self.hwnd, None, x, y, 0, 0,
77
+ flags
78
+ )
79
+
80
+ def bring_foreground(self) -> None:
81
+ """将窗口置于前台"""
82
+ win32gui.SetForegroundWindow(self.hwnd)
83
+
84
+ def send_message(self, msg: int, wparam: int, lparam: int) -> int:
85
+ return win32gui.SendMessage(self.hwnd, msg, wparam, lparam)
86
+
87
+ def post_message(self, msg: int, wparam: int, lparam: int) -> bool:
88
+ win32gui.PostMessage(self.hwnd, msg, wparam, lparam)
89
+ return win32api.GetLastError() == 0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kotonebot
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: Kotonebot is game automation library based on computer vision technology, works for Windows and Android.
5
5
  Requires-Python: >=3.10
6
6
  Description-Content-Type: text/markdown
@@ -31,17 +31,20 @@ kotonebot/client/host/custom.py,sha256=zYI4tZl5xGxdUTvF2li7c315252_hijdwMXwGMCXR
31
31
  kotonebot/client/host/leidian_host.py,sha256=O8BUx61HDJs3lsdhoJcuYKEYImG9jWM6mqrOec0oSlc,7538
32
32
  kotonebot/client/host/mumu12_host.py,sha256=CB-O7z3B-dWGVxiI78eK78hhyqileswa1DYXAftvjU8,14253
33
33
  kotonebot/client/host/protocol.py,sha256=fnb9EqlTnSEfPlJ37HMYU6We4_YemX8mQFxcy2gsP8U,8173
34
- kotonebot/client/host/windows_common.py,sha256=_Hf4CvHKNgiMIVY3ZPr3wF71r6nMErNi84Y_fswUjBc,2431
34
+ kotonebot/client/host/windows_common.py,sha256=RDjn7DtNE-jqB5ih1h7NYTJf7J3CgEeD3rPWRoc3BIc,3331
35
35
  kotonebot/client/implements/__init__.py,sha256=OaSvmYTwxasdnYCS0kKrrcmmWPHjfMpYdmZ9siP03m8,2137
36
36
  kotonebot/client/implements/adb.py,sha256=0Xs5o014SG9dNDIJYgI2_QyOMcZghuscolfK8eQnA40,3288
37
37
  kotonebot/client/implements/remote_windows.py,sha256=SRq97cuXdUVvFIhsazZt4djMeGSLfYS88IUyzmlpaww,6756
38
38
  kotonebot/client/implements/uiautomator2.py,sha256=Hal_DDWRCoLjL854xqDRq1X4Gm6O_SCBCUhqM2p1ykw,2722
39
- kotonebot/client/implements/windows.py,sha256=bnASXFE8xHzY9Ue90r0ppYNnIVezhfMHAA0X9deXTNM,6889
40
39
  kotonebot/client/implements/nemu_ipc/__init__.py,sha256=atIZjwnxhKEOndikoHDYhPrkvb7iC3zscBC457MQPz4,321
41
40
  kotonebot/client/implements/nemu_ipc/external_renderer_ipc.py,sha256=YsfKf0-qorfAf2YvNuxpLb9af-HJFsu97bnXABshhbA,10643
42
41
  kotonebot/client/implements/nemu_ipc/nemu_ipc.py,sha256=LhUUyfB28MDnRg8z2FyGah1hTeOMFiX7w8LZLAAjLF8,12082
42
+ kotonebot/client/implements/windows/__init__.py,sha256=LTV4QpWocfTG1blVDyhRnwcuBlqWl_P3VWpIrXVFLL4,22
43
+ kotonebot/client/implements/windows/print_window.py,sha256=_4wWKgMoAI5OMy5AZ1XaSEd070HjJf4TYWZ7NHYZLU4,4602
44
+ kotonebot/client/implements/windows/send_message.py,sha256=rG0USG-fMCz5fBq2WeDi7sFotEJCmdCTYuIYcoXv1Pw,11309
45
+ kotonebot/client/implements/windows/windows.py,sha256=vlNQDuC2vqPVEJ-o44_tS3_kFyetcLxmSteWH8LYdoo,6826
43
46
  kotonebot/config/__init__.py,sha256=-jATUOdrpUrBRT9RiTRQho2-2zeet50qQggsVMVpqNE,35
44
- kotonebot/config/base_config.py,sha256=oJMoCEDUU15S7iO8WJg_qqL4YbXSr9qZqlYE82KJSl0,3427
47
+ kotonebot/config/base_config.py,sha256=60sqxyd3XTM2Er2Mvu6SK4pN9z_vsXH77LfIchYdRAM,3770
45
48
  kotonebot/config/config.py,sha256=0MeumpADN8PneyeqVGutByL-39jZdJioJWL8DM5xtdU,1951
46
49
  kotonebot/config/manager.py,sha256=XBtriAU9eo-wv2iKOwyDqu8tzhbKqFCuy0jsAM9T9uU,1061
47
50
  kotonebot/core/__init__.py,sha256=r2d-3TLGKvr3H8q3sfDZRtRaq0xPE9gUN4aHnqa7Ofs,410
@@ -77,13 +80,14 @@ kotonebot/devtools/web/dist/icons/symbol-method.svg,sha256=KGS0YF4X4jyzU9PB0NHe6
77
80
  kotonebot/devtools/web/server/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
81
  kotonebot/devtools/web/server/rest_api.py,sha256=Kk_bc3T3Qz_8okxOWd4url3ImXJh-V5x9t3vir3muKA,8249
79
82
  kotonebot/devtools/web/server/server.py,sha256=e4M99fDAR3lWGG7k3FBzWDye2O8yTDjYt6A-tXug5wQ,2694
80
- kotonebot/interop/win/__init__.py,sha256=20_Oxkv9h6tqfw59CFLaCMCbRL-k-82kv-tY6KtivxQ,234
83
+ kotonebot/interop/win/__init__.py,sha256=UaJrj8ZCwPmHOnC1N3y4qbI2H6RKknLJiFkQl1UIaWM,324
81
84
  kotonebot/interop/win/_mouse.py,sha256=SdkgVyxRnZQbR47yQWqmWzSQw3lcG9eNN1hITgiSu2c,10441
82
85
  kotonebot/interop/win/message_box.py,sha256=R06GSu936Bx_Wg7ddn6LOvazD9_Gt3mhz4_oUuIoYO0,8635
83
86
  kotonebot/interop/win/reg.py,sha256=xw35d1xl8ucITT4bOMFgHmMkAUhak7x3lzegR3g3S48,1347
84
87
  kotonebot/interop/win/shake_mouse.py,sha256=SdJ94NyScAgF_IDr7E4DPDE1DVpo_vyFLaAf0SQCgG0,7266
85
88
  kotonebot/interop/win/shortcut.py,sha256=f1u6IWvpw6Kxt014wnHz5Z94rVK1qf4kLtRsI9bJYnk,1764
86
89
  kotonebot/interop/win/task_dialog.py,sha256=Ezi1CsjFSbnKccvYfIRaJoCGHUsJnnIpHb0gtKR603E,20191
90
+ kotonebot/interop/win/window.py,sha256=DC_tZxH6QfqI1YbYrQiExfYosyBc1zKGw0Fx9L60fd8,3104
87
91
  kotonebot/logging/__init__.py,sha256=r0q4z59yYy_bQnHTwJYsiPGwOGIgEOrcXH1mNs1h_N0,142
88
92
  kotonebot/logging/log.py,sha256=PLb6r_hlW1mvqU_kx6_89zaQ1IpamgHWG3sQ0ZnlrCI,555
89
93
  kotonebot/primitives/__init__.py,sha256=Gsuo5NSTo81aDi3HDkAj2gFqSTWtY-k2PSJ4fc8w5FA,392
@@ -97,9 +101,9 @@ kotonebot/ui/pushkit/__init__.py,sha256=xDUctRUL3euvge-yl8IhFYMlxIxQXsjxcyGN5tUw
97
101
  kotonebot/ui/pushkit/image_host.py,sha256=4ZEptuowUJJ-b9WwXaEfbVes2b-aJBK-JDI4bP8A9VM,2591
98
102
  kotonebot/ui/pushkit/protocol.py,sha256=KVZ-xr0sMdiuri7AiYqugpZRRtefBsosXm6zouScUR4,266
99
103
  kotonebot/ui/pushkit/wxpusher.py,sha256=Px-7pUvUMsgJLKzJXRJBVv8rPZMnELTYrTBhhllR8tM,1750
100
- kotonebot-0.6.0.dist-info/licenses/LICENSE,sha256=gcuuhKKc5-dwvyvHsXjlC9oM6N5gZ6umYbC8ewW1Yvg,35821
101
- kotonebot-0.6.0.dist-info/METADATA,sha256=03B-8qSqwe0SbRoxdRtRYrQjQxtjz3PwRxNFPSz0UOU,3351
102
- kotonebot-0.6.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
103
- kotonebot-0.6.0.dist-info/entry_points.txt,sha256=0TRRHk88fKrlrGGuxTMfa80P618wK3kLspjTrRAVoy0,53
104
- kotonebot-0.6.0.dist-info/top_level.txt,sha256=QUWAZdbBndoojkrs6RcNytLAn7a0ns4YNF4tLx2Nc4s,10
105
- kotonebot-0.6.0.dist-info/RECORD,,
104
+ kotonebot-0.7.0.dist-info/licenses/LICENSE,sha256=gcuuhKKc5-dwvyvHsXjlC9oM6N5gZ6umYbC8ewW1Yvg,35821
105
+ kotonebot-0.7.0.dist-info/METADATA,sha256=Ivfdne2Tyhwl_AEZ1r7SPGF3Ky76Kc-fBcooxiWsS3o,3351
106
+ kotonebot-0.7.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
107
+ kotonebot-0.7.0.dist-info/entry_points.txt,sha256=0TRRHk88fKrlrGGuxTMfa80P618wK3kLspjTrRAVoy0,53
108
+ kotonebot-0.7.0.dist-info/top_level.txt,sha256=QUWAZdbBndoojkrs6RcNytLAn7a0ns4YNF4tLx2Nc4s,10
109
+ kotonebot-0.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5