ascript 1.3.27__py3-none-win_amd64.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.
- ascript/__init__.py +8 -0
- ascript/hooks/__init__.py +0 -0
- ascript/hooks/hook-ascript.py +2 -0
- ascript/windows/__init__.py +29 -0
- ascript/windows/daos/__init__.py +0 -0
- ascript/windows/daos/screen.py +143 -0
- ascript/windows/hardware/__init__.py +5 -0
- ascript/windows/hardware/keyboard.py +279 -0
- ascript/windows/hardware/logitech_driver.py +179 -0
- ascript/windows/hardware/mouse.py +308 -0
- ascript/windows/license/__init__.py +16 -0
- ascript/windows/license/achttp.cp310-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp311-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp312-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp313-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp314-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp38-win_amd64.pyd +0 -0
- ascript/windows/license/achttp.cp39-win_amd64.pyd +0 -0
- ascript/windows/media/__init__.py +0 -0
- ascript/windows/resources/__init__.py +0 -0
- ascript/windows/resources/libs/logitech/x64/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/libs/logitech/x86/LogitechGkeyEnginesWrapper.dll +0 -0
- ascript/windows/resources/templates/__init__.py +0 -0
- ascript/windows/resources/templates/__init__py.win.template +51 -0
- ascript/windows/resources/templates/build.as.win.template +14 -0
- ascript/windows/resources/templates/build.py +71 -0
- ascript/windows/resources/templates/logo_windows.ico +0 -0
- ascript/windows/resources/templates/windows/main.py +50 -0
- ascript/windows/resources/templates/windows/pyproject.toml +44 -0
- ascript/windows/resources/templates/windows/res/img/logo.ico +0 -0
- ascript/windows/screen/__init__.py +13 -0
- ascript/windows/screen/as_cv.py +382 -0
- ascript/windows/screen/canny.py +128 -0
- ascript/windows/screen/code_scanner.py +72 -0
- ascript/windows/screen/color_algorithms.py +277 -0
- ascript/windows/screen/find_colors.py +251 -0
- ascript/windows/screen/find_images.py +217 -0
- ascript/windows/screen/ocr/__init__.py +4 -0
- ascript/windows/screen/ocr/ocr.py +223 -0
- ascript/windows/screen/ocr/rapidocr_hook.py +301 -0
- ascript/windows/screen/ocrx.py +169 -0
- ascript/windows/screen/screen_core.py +139 -0
- ascript/windows/store/__init__.py +21 -0
- ascript/windows/system/__init__.py +8 -0
- ascript/windows/system/device.py +355 -0
- ascript/windows/system/easy_print.py +115 -0
- ascript/windows/system/env.py +492 -0
- ascript/windows/system/exception/__init__.py +1 -0
- ascript/windows/system/key_value.py +145 -0
- ascript/windows/system/pip.py +175 -0
- ascript/windows/system/r.py +165 -0
- ascript/windows/system/runtime.py +62 -0
- ascript/windows/tools/__init__.py +27 -0
- ascript/windows/tools/__main__.py +21 -0
- ascript/windows/tools/action.json +33 -0
- ascript/windows/tools/action.py +168 -0
- ascript/windows/tools/colors_tool.py +391 -0
- ascript/windows/tools/config.py +26 -0
- ascript/windows/tools/dumptool.py +18 -0
- ascript/windows/tools/eel_api.py +495 -0
- ascript/windows/tools/gpplug/__init__.py +0 -0
- ascript/windows/tools/gpplug/plug.py +70 -0
- ascript/windows/tools/mouse_catch.py +168 -0
- ascript/windows/tools/plug_manager.py +38 -0
- ascript/windows/tools/tool_api.py +906 -0
- ascript/windows/tools/tool_window.py +56 -0
- ascript/windows/tools/uia_worker.py +365 -0
- ascript/windows/tools/web/app_store_window.html +481 -0
- ascript/windows/tools/web/colors.html +532 -0
- ascript/windows/tools/web/css/screen.css +1342 -0
- ascript/windows/tools/web/css/viewtree.css +357 -0
- ascript/windows/tools/web/flot_control.html +195 -0
- ascript/windows/tools/web/img/ico/favicon.png +0 -0
- ascript/windows/tools/web/img/ico/paintpalette.png +0 -0
- ascript/windows/tools/web/img/svg/find_colors.svg +1 -0
- ascript/windows/tools/web/img/svg/find_images.svg +1 -0
- ascript/windows/tools/web/img/svg/illustration-6.svg +89 -0
- ascript/windows/tools/web/img/svg/img_default.svg +1 -0
- ascript/windows/tools/web/img/svg/ocr.svg +1 -0
- ascript/windows/tools/web/js/screen/colors.js +466 -0
- ascript/windows/tools/web/js/screen/gp_plug.js +108 -0
- ascript/windows/tools/web/js/screen/image_mouse.js +464 -0
- ascript/windows/tools/web/js/screen/img_view.js +558 -0
- ascript/windows/tools/web/js/screen/plug.js +246 -0
- ascript/windows/tools/web/js/screen/screen.js +690 -0
- ascript/windows/tools/web/js/screen/screen_api.js +778 -0
- ascript/windows/tools/web/js/screen/screen_maker.js +10 -0
- ascript/windows/tools/web/js/utils/map.js +73 -0
- ascript/windows/tools/web/js/utils/upload.js +157 -0
- ascript/windows/tools/web/js/viewtree/niv_js.js +27 -0
- ascript/windows/tools/web/js/viewtree/vtree.js +1510 -0
- ascript/windows/tools/web/libs/bootstrap/css/bootstrap.min.css +6 -0
- ascript/windows/tools/web/libs/bootstrap/js/bootstrap.bundle.min.js +7 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-circle.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/0-square.svg +4 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.css +2078 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.json +2052 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.min.css +5 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/bootstrap-icons.scss +2090 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff +0 -0
- ascript/windows/tools/web/libs/bootstrap-icons/font/fonts/bootstrap-icons.woff2 +0 -0
- ascript/windows/tools/web/libs/feather-icons/dist/feather.min.js +13 -0
- ascript/windows/tools/web/libs/feather-icons/dist/icons.json +1 -0
- ascript/windows/tools/web/libs/jquery/core.js +399 -0
- ascript/windows/tools/web/libs/jquery/jquery.js +10598 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js +4 -0
- ascript/windows/tools/web/libs/jquery/jquery.min.js.zip +0 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.js +8495 -0
- ascript/windows/tools/web/libs/jquery/jquery.slim.min.js +2 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.js +2134 -0
- ascript/windows/tools/web/libs/jquery-menu/jquery.contextMenu.min.css +16 -0
- ascript/windows/tools/web/libs/jquery-zTree/css/zTreeStyle.css +97 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_close.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/1_open.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/2.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/3.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/4.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/5.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/6.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/7.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/8.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/diy/9.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/line_conn.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/loading.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/tree-node-btn.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.gif +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/img/zTreeStandard.png +0 -0
- ascript/windows/tools/web/libs/jquery-zTree/js/jquery.ztree.all.min.js +3 -0
- ascript/windows/tools/web/libs/treeview/css/jquery.treeview.css +79 -0
- ascript/windows/tools/web/libs/treeview/img/file.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/ico_loading.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon4.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon5.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/icon6.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/py.png +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-black.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-default.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-famfamfam.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-gray.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red-line.gif +0 -0
- ascript/windows/tools/web/libs/treeview/img/treeview-red.gif +0 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.edit.js +37 -0
- ascript/windows/tools/web/libs/treeview/js/jquery.treeview.js +250 -0
- ascript/windows/tools/web/license_window.html +255 -0
- ascript/windows/tools/web/plugs/a.html +653 -0
- ascript/windows/tools/web/plugs/demo.html +52 -0
- ascript/windows/tools/web/plugs/find_colors.html +629 -0
- ascript/windows/tools/web/plugs/find_images.html +699 -0
- ascript/windows/tools/web/plugs/rapidocr.html +954 -0
- ascript/windows/tools/web/vtree.html +496 -0
- ascript/windows/tools/webview/assets/index-HymBPl-I.css +1 -0
- ascript/windows/tools/webview/assets/index-PqA_m8tL.js +17 -0
- ascript/windows/tools/webview/index.html +13 -0
- ascript/windows/ui/__init__.py +6 -0
- ascript/windows/ui/app_store_window.py +272 -0
- ascript/windows/ui/draw_window.py +231 -0
- ascript/windows/ui/flot_test.py +297 -0
- ascript/windows/ui/flot_window.py +488 -0
- ascript/windows/ui/license_window.py +111 -0
- ascript/windows/ui/web_window.py +466 -0
- ascript/windows/utils/__init__.py +0 -0
- ascript/windows/utils/node_utils.py +20 -0
- ascript/windows/utils/screen_utils.py +280 -0
- ascript/windows/window/Selector.py +1015 -0
- ascript/windows/window/Window.py +747 -0
- ascript/windows/window/__init__.py +5 -0
- ascript/windows/window/launcher.py +213 -0
- ascript/windows/window/uia_runtime.py +26 -0
- ascript-1.3.27.dist-info/METADATA +41 -0
- ascript-1.3.27.dist-info/RECORD +177 -0
- ascript-1.3.27.dist-info/WHEEL +5 -0
- ascript-1.3.27.dist-info/entry_points.txt +2 -0
- ascript-1.3.27.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
"""Native Windows window discovery, control, input and capture helpers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import ctypes
|
|
6
|
+
import math
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import time
|
|
10
|
+
from ctypes import wintypes
|
|
11
|
+
from enum import Enum, auto
|
|
12
|
+
from typing import List, NamedTuple, Optional, Sequence, Union
|
|
13
|
+
|
|
14
|
+
import psutil
|
|
15
|
+
import win32con
|
|
16
|
+
import win32gui
|
|
17
|
+
import win32process
|
|
18
|
+
import win32ui
|
|
19
|
+
from PIL import Image, ImageGrab
|
|
20
|
+
from ..hardware import Mouse
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_USER32 = ctypes.windll.user32
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _enable_dpi_awareness() -> None:
|
|
27
|
+
try:
|
|
28
|
+
setter = _USER32.SetProcessDpiAwarenessContext
|
|
29
|
+
setter.argtypes = (ctypes.c_void_p,)
|
|
30
|
+
setter.restype = wintypes.BOOL
|
|
31
|
+
setter(ctypes.c_void_p(-4))
|
|
32
|
+
return
|
|
33
|
+
except (AttributeError, OSError):
|
|
34
|
+
pass
|
|
35
|
+
try:
|
|
36
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
37
|
+
except (AttributeError, OSError):
|
|
38
|
+
try:
|
|
39
|
+
_USER32.SetProcessDPIAware()
|
|
40
|
+
except (AttributeError, OSError):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
_enable_dpi_awareness()
|
|
45
|
+
|
|
46
|
+
_IS_HUNG_APP_WINDOW = getattr(_USER32, "IsHungAppWindow", None)
|
|
47
|
+
if _IS_HUNG_APP_WINDOW is not None:
|
|
48
|
+
_IS_HUNG_APP_WINDOW.argtypes = (wintypes.HWND,)
|
|
49
|
+
_IS_HUNG_APP_WINDOW.restype = wintypes.BOOL
|
|
50
|
+
|
|
51
|
+
_DWM_GET_WINDOW_ATTRIBUTE = None
|
|
52
|
+
try:
|
|
53
|
+
_DWM_GET_WINDOW_ATTRIBUTE = ctypes.windll.dwmapi.DwmGetWindowAttribute
|
|
54
|
+
_DWM_GET_WINDOW_ATTRIBUTE.argtypes = (
|
|
55
|
+
wintypes.HWND, wintypes.DWORD, ctypes.c_void_p, wintypes.DWORD
|
|
56
|
+
)
|
|
57
|
+
_DWM_GET_WINDOW_ATTRIBUTE.restype = ctypes.c_long
|
|
58
|
+
except (AttributeError, OSError):
|
|
59
|
+
pass
|
|
60
|
+
|
|
61
|
+
_DWMWA_EXTENDED_FRAME_BOUNDS = 9
|
|
62
|
+
_PW_CLIENTONLY = 0x1
|
|
63
|
+
_PW_RENDERFULLCONTENT = 0x2
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class _GUITHREADINFO(ctypes.Structure):
|
|
67
|
+
_fields_ = [
|
|
68
|
+
("cbSize", wintypes.DWORD),
|
|
69
|
+
("flags", wintypes.DWORD),
|
|
70
|
+
("hwndActive", wintypes.HWND),
|
|
71
|
+
("hwndFocus", wintypes.HWND),
|
|
72
|
+
("hwndCapture", wintypes.HWND),
|
|
73
|
+
("hwndMenuOwner", wintypes.HWND),
|
|
74
|
+
("hwndMoveSize", wintypes.HWND),
|
|
75
|
+
("hwndCaret", wintypes.HWND),
|
|
76
|
+
("rcCaret", wintypes.RECT),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class WindowRect(NamedTuple):
|
|
81
|
+
"""A rectangle in physical screen coordinates."""
|
|
82
|
+
|
|
83
|
+
left: int
|
|
84
|
+
top: int
|
|
85
|
+
right: int
|
|
86
|
+
bottom: int
|
|
87
|
+
width: int
|
|
88
|
+
height: int
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_ltrb(cls, left: int, top: int, right: int, bottom: int) -> "WindowRect":
|
|
92
|
+
return cls(left, top, right, bottom, right - left, bottom - top)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class WindowCaptureFormat(Enum):
|
|
96
|
+
PIL = auto()
|
|
97
|
+
NUMPY = auto()
|
|
98
|
+
OPENCV = auto()
|
|
99
|
+
|
|
100
|
+
def convert(self, pil_image: Image.Image):
|
|
101
|
+
if self is WindowCaptureFormat.PIL:
|
|
102
|
+
return pil_image
|
|
103
|
+
import numpy as np
|
|
104
|
+
|
|
105
|
+
array = np.asarray(pil_image)
|
|
106
|
+
if self is WindowCaptureFormat.NUMPY:
|
|
107
|
+
return array
|
|
108
|
+
if self is WindowCaptureFormat.OPENCV:
|
|
109
|
+
return array[:, :, ::-1].copy()
|
|
110
|
+
raise ValueError(f"Unsupported capture format: {self!r}")
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Window:
|
|
114
|
+
"""A lightweight wrapper around a top-level Windows HWND."""
|
|
115
|
+
|
|
116
|
+
def __init__(self, hwnd: int):
|
|
117
|
+
if not isinstance(hwnd, int) or isinstance(hwnd, bool):
|
|
118
|
+
raise TypeError("hwnd must be an integer")
|
|
119
|
+
self.hwnd = hwnd
|
|
120
|
+
self._pid: Optional[int] = None
|
|
121
|
+
|
|
122
|
+
def __repr__(self) -> str:
|
|
123
|
+
try:
|
|
124
|
+
return f"<Window title={self.title!r} process={self.process_name!r} hwnd={self.hwnd}>"
|
|
125
|
+
except (OSError, win32gui.error):
|
|
126
|
+
return f"<Window hwnd={self.hwnd} invalid>"
|
|
127
|
+
|
|
128
|
+
def __bool__(self) -> bool:
|
|
129
|
+
return self.is_valid
|
|
130
|
+
|
|
131
|
+
def __eq__(self, other) -> bool:
|
|
132
|
+
return isinstance(other, Window) and self.hwnd == other.hwnd
|
|
133
|
+
|
|
134
|
+
def __hash__(self) -> int:
|
|
135
|
+
return hash(self.hwnd)
|
|
136
|
+
|
|
137
|
+
@staticmethod
|
|
138
|
+
def _compile_title_pattern(title_re: Optional[str]):
|
|
139
|
+
if title_re is None:
|
|
140
|
+
return None
|
|
141
|
+
if not isinstance(title_re, str):
|
|
142
|
+
raise TypeError("title_re must be a string or None")
|
|
143
|
+
try:
|
|
144
|
+
return re.compile(title_re)
|
|
145
|
+
except re.error as exc:
|
|
146
|
+
raise ValueError(f"invalid title regular expression: {exc}") from exc
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def active(cls) -> Optional["Window"]:
|
|
150
|
+
hwnd = win32gui.GetForegroundWindow()
|
|
151
|
+
return cls(hwnd) if hwnd else None
|
|
152
|
+
|
|
153
|
+
@staticmethod
|
|
154
|
+
def screen_size() -> tuple[int, int]:
|
|
155
|
+
"""Return the virtual desktop size, including secondary monitors."""
|
|
156
|
+
return (
|
|
157
|
+
int(_USER32.GetSystemMetrics(78)),
|
|
158
|
+
int(_USER32.GetSystemMetrics(79)),
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def find_all(
|
|
163
|
+
cls,
|
|
164
|
+
title_re: Optional[str] = None,
|
|
165
|
+
process_name: Optional[str] = None,
|
|
166
|
+
class_name: Optional[str] = None,
|
|
167
|
+
visible_only: bool = True,
|
|
168
|
+
) -> List["Window"]:
|
|
169
|
+
pattern = cls._compile_title_pattern(title_re)
|
|
170
|
+
process_filter = process_name.casefold() if process_name else None
|
|
171
|
+
results: List[Window] = []
|
|
172
|
+
|
|
173
|
+
def callback(hwnd, _):
|
|
174
|
+
try:
|
|
175
|
+
if visible_only and not win32gui.IsWindowVisible(hwnd):
|
|
176
|
+
return True
|
|
177
|
+
candidate = cls(hwnd)
|
|
178
|
+
if class_name is not None and candidate.class_name != class_name:
|
|
179
|
+
return True
|
|
180
|
+
if process_filter is not None and process_filter not in candidate.process_name.casefold():
|
|
181
|
+
return True
|
|
182
|
+
if pattern is not None and pattern.search(candidate.title) is None:
|
|
183
|
+
return True
|
|
184
|
+
results.append(candidate)
|
|
185
|
+
except (OSError, psutil.Error, win32gui.error):
|
|
186
|
+
pass
|
|
187
|
+
return True
|
|
188
|
+
|
|
189
|
+
win32gui.EnumWindows(callback, None)
|
|
190
|
+
return results
|
|
191
|
+
|
|
192
|
+
@classmethod
|
|
193
|
+
def find(
|
|
194
|
+
cls,
|
|
195
|
+
title_re: Optional[str] = None,
|
|
196
|
+
process_name: Optional[str] = None,
|
|
197
|
+
class_name: Optional[str] = None,
|
|
198
|
+
visible_only: bool = True,
|
|
199
|
+
timeout: float = 0,
|
|
200
|
+
) -> Optional["Window"]:
|
|
201
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
|
|
202
|
+
raise TypeError("timeout must be a number")
|
|
203
|
+
timeout = float(timeout)
|
|
204
|
+
if timeout < 0 or not math.isfinite(timeout):
|
|
205
|
+
raise ValueError("timeout must be finite and non-negative")
|
|
206
|
+
deadline = time.monotonic() + timeout
|
|
207
|
+
while True:
|
|
208
|
+
matches = cls.find_all(title_re, process_name, class_name, visible_only)
|
|
209
|
+
if matches:
|
|
210
|
+
return matches[0]
|
|
211
|
+
remaining = deadline - time.monotonic()
|
|
212
|
+
if timeout == 0 or remaining <= 0:
|
|
213
|
+
return None
|
|
214
|
+
time.sleep(min(0.2, remaining))
|
|
215
|
+
|
|
216
|
+
def find_children(
|
|
217
|
+
self,
|
|
218
|
+
title_re: Optional[str] = None,
|
|
219
|
+
class_name: Optional[str] = None,
|
|
220
|
+
visible_only: bool = True,
|
|
221
|
+
) -> List["Window"]:
|
|
222
|
+
"""Return descendant windows matching stable window attributes."""
|
|
223
|
+
if not self.is_valid:
|
|
224
|
+
return []
|
|
225
|
+
pattern = self._compile_title_pattern(title_re)
|
|
226
|
+
results: List[Window] = []
|
|
227
|
+
|
|
228
|
+
def callback(hwnd, _):
|
|
229
|
+
try:
|
|
230
|
+
if visible_only and not win32gui.IsWindowVisible(hwnd):
|
|
231
|
+
return True
|
|
232
|
+
candidate = Window(int(hwnd))
|
|
233
|
+
if class_name is not None and candidate.class_name != class_name:
|
|
234
|
+
return True
|
|
235
|
+
if pattern is not None and pattern.search(candidate.title) is None:
|
|
236
|
+
return True
|
|
237
|
+
results.append(candidate)
|
|
238
|
+
except (OSError, psutil.Error, win32gui.error):
|
|
239
|
+
pass
|
|
240
|
+
return True
|
|
241
|
+
|
|
242
|
+
win32gui.EnumChildWindows(self.hwnd, callback, None)
|
|
243
|
+
return results
|
|
244
|
+
|
|
245
|
+
def find_child(
|
|
246
|
+
self,
|
|
247
|
+
title_re: Optional[str] = None,
|
|
248
|
+
class_name: Optional[str] = None,
|
|
249
|
+
visible_only: bool = True,
|
|
250
|
+
timeout: float = 0,
|
|
251
|
+
) -> Optional["Window"]:
|
|
252
|
+
"""Find the first matching descendant, optionally waiting for it."""
|
|
253
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
|
|
254
|
+
raise TypeError("timeout must be a number")
|
|
255
|
+
timeout = float(timeout)
|
|
256
|
+
if timeout < 0 or not math.isfinite(timeout):
|
|
257
|
+
raise ValueError("timeout must be finite and non-negative")
|
|
258
|
+
deadline = time.monotonic() + timeout
|
|
259
|
+
while True:
|
|
260
|
+
matches = self.find_children(title_re, class_name, visible_only)
|
|
261
|
+
if matches:
|
|
262
|
+
return matches[0]
|
|
263
|
+
remaining = deadline - time.monotonic()
|
|
264
|
+
if timeout == 0 or remaining <= 0:
|
|
265
|
+
return None
|
|
266
|
+
time.sleep(min(0.2, remaining))
|
|
267
|
+
|
|
268
|
+
@classmethod
|
|
269
|
+
def launch(
|
|
270
|
+
cls,
|
|
271
|
+
name: Optional[str] = None,
|
|
272
|
+
path: Optional[str] = None,
|
|
273
|
+
allow_multiple: bool = False,
|
|
274
|
+
timeout: float = 15,
|
|
275
|
+
) -> Optional["Window"]:
|
|
276
|
+
from .launcher import Launcher
|
|
277
|
+
|
|
278
|
+
if not name and not path:
|
|
279
|
+
raise ValueError("name or path is required")
|
|
280
|
+
if isinstance(timeout, bool) or not isinstance(timeout, (int, float)):
|
|
281
|
+
raise TypeError("timeout must be a number")
|
|
282
|
+
timeout = float(timeout)
|
|
283
|
+
if timeout < 0 or not math.isfinite(timeout):
|
|
284
|
+
raise ValueError("timeout must be finite and non-negative")
|
|
285
|
+
|
|
286
|
+
resolved_path, process_name = Launcher.get_info(name=name, path=path)
|
|
287
|
+
launch_target = resolved_path or path
|
|
288
|
+
if not process_name and launch_target:
|
|
289
|
+
process_name = os.path.basename(launch_target)
|
|
290
|
+
title_filter = name if name and not process_name else None
|
|
291
|
+
existing = cls.find_all(
|
|
292
|
+
title_re=title_filter, process_name=process_name, visible_only=False
|
|
293
|
+
)
|
|
294
|
+
if existing and not allow_multiple:
|
|
295
|
+
existing[0].activate()
|
|
296
|
+
return existing[0]
|
|
297
|
+
|
|
298
|
+
if not Launcher.run(name=name, path=launch_target):
|
|
299
|
+
return None
|
|
300
|
+
|
|
301
|
+
old_handles = {window.hwnd for window in existing}
|
|
302
|
+
deadline = time.monotonic() + timeout
|
|
303
|
+
while True:
|
|
304
|
+
current = cls.find_all(
|
|
305
|
+
title_re=title_filter, process_name=process_name, visible_only=False
|
|
306
|
+
)
|
|
307
|
+
candidates = (
|
|
308
|
+
[window for window in current if window.hwnd not in old_handles]
|
|
309
|
+
if allow_multiple else current
|
|
310
|
+
)
|
|
311
|
+
if candidates:
|
|
312
|
+
candidates[0].activate()
|
|
313
|
+
return candidates[0]
|
|
314
|
+
remaining = deadline - time.monotonic()
|
|
315
|
+
if remaining <= 0:
|
|
316
|
+
return existing[0] if existing else None
|
|
317
|
+
time.sleep(min(0.2, remaining))
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def is_valid(self) -> bool:
|
|
321
|
+
return bool(win32gui.IsWindow(self.hwnd))
|
|
322
|
+
|
|
323
|
+
@property
|
|
324
|
+
def title(self) -> str:
|
|
325
|
+
return win32gui.GetWindowText(self.hwnd)
|
|
326
|
+
|
|
327
|
+
@property
|
|
328
|
+
def class_name(self) -> str:
|
|
329
|
+
return win32gui.GetClassName(self.hwnd)
|
|
330
|
+
|
|
331
|
+
@property
|
|
332
|
+
def pid(self) -> int:
|
|
333
|
+
if self._pid is None:
|
|
334
|
+
_, self._pid = win32process.GetWindowThreadProcessId(self.hwnd)
|
|
335
|
+
return self._pid
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def process_path(self) -> str:
|
|
339
|
+
try:
|
|
340
|
+
return psutil.Process(self.pid).exe()
|
|
341
|
+
except (psutil.Error, OSError):
|
|
342
|
+
return "Unknown"
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def process_name(self) -> str:
|
|
346
|
+
try:
|
|
347
|
+
return psutil.Process(self.pid).name()
|
|
348
|
+
except (psutil.Error, OSError):
|
|
349
|
+
return "Unknown"
|
|
350
|
+
|
|
351
|
+
@property
|
|
352
|
+
def is_visible(self) -> bool:
|
|
353
|
+
if not self.is_valid or not win32gui.IsWindowVisible(self.hwnd):
|
|
354
|
+
return False
|
|
355
|
+
left, top, right, bottom = win32gui.GetWindowRect(self.hwnd)
|
|
356
|
+
return right > left and bottom > top and left > -30000 and top > -30000
|
|
357
|
+
|
|
358
|
+
@property
|
|
359
|
+
def is_minimized(self) -> bool:
|
|
360
|
+
return bool(win32gui.IsIconic(self.hwnd))
|
|
361
|
+
|
|
362
|
+
@property
|
|
363
|
+
def is_maximized(self) -> bool:
|
|
364
|
+
return bool(win32gui.IsZoomed(self.hwnd))
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def is_responding(self) -> bool:
|
|
368
|
+
if not self.is_valid:
|
|
369
|
+
return False
|
|
370
|
+
if _IS_HUNG_APP_WINDOW is not None:
|
|
371
|
+
return not bool(_IS_HUNG_APP_WINDOW(self.hwnd))
|
|
372
|
+
return self._check_responding_via_timeout()
|
|
373
|
+
|
|
374
|
+
def _check_responding_via_timeout(self) -> bool:
|
|
375
|
+
try:
|
|
376
|
+
win32gui.SendMessageTimeout(
|
|
377
|
+
self.hwnd, win32con.WM_NULL, 0, 0,
|
|
378
|
+
win32con.SMTO_ABORTIFHUNG | win32con.SMTO_BLOCK, 100,
|
|
379
|
+
)
|
|
380
|
+
return True
|
|
381
|
+
except win32gui.error:
|
|
382
|
+
return False
|
|
383
|
+
|
|
384
|
+
@property
|
|
385
|
+
def is_always_on_top(self) -> bool:
|
|
386
|
+
style = win32gui.GetWindowLong(self.hwnd, win32con.GWL_EXSTYLE)
|
|
387
|
+
return bool(style & win32con.WS_EX_TOPMOST)
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def rect(self) -> WindowRect:
|
|
391
|
+
native = win32gui.GetWindowRect(self.hwnd)
|
|
392
|
+
if _DWM_GET_WINDOW_ATTRIBUTE is not None:
|
|
393
|
+
visual = wintypes.RECT()
|
|
394
|
+
result = _DWM_GET_WINDOW_ATTRIBUTE(
|
|
395
|
+
self.hwnd, _DWMWA_EXTENDED_FRAME_BOUNDS,
|
|
396
|
+
ctypes.byref(visual), ctypes.sizeof(visual),
|
|
397
|
+
)
|
|
398
|
+
if result == 0 and visual.right > visual.left and visual.bottom > visual.top:
|
|
399
|
+
return WindowRect.from_ltrb(
|
|
400
|
+
visual.left, visual.top, visual.right, visual.bottom
|
|
401
|
+
)
|
|
402
|
+
return WindowRect.from_ltrb(*native)
|
|
403
|
+
|
|
404
|
+
@property
|
|
405
|
+
def client_rect(self) -> WindowRect:
|
|
406
|
+
left, top, right, bottom = win32gui.GetClientRect(self.hwnd)
|
|
407
|
+
screen_left, screen_top = win32gui.ClientToScreen(self.hwnd, (left, top))
|
|
408
|
+
return WindowRect.from_ltrb(
|
|
409
|
+
screen_left, screen_top,
|
|
410
|
+
screen_left + right - left, screen_top + bottom - top,
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
def set_rect(
|
|
414
|
+
self, x: Optional[int] = None, y: Optional[int] = None,
|
|
415
|
+
width: Optional[int] = None, height: Optional[int] = None,
|
|
416
|
+
):
|
|
417
|
+
for value, label in ((x, "x"), (y, "y"), (width, "width"), (height, "height")):
|
|
418
|
+
if value is not None and (not isinstance(value, int) or isinstance(value, bool)):
|
|
419
|
+
raise TypeError(f"{label} must be an integer or None")
|
|
420
|
+
if (width is not None and width <= 0) or (height is not None and height <= 0):
|
|
421
|
+
raise ValueError("width and height must be greater than zero")
|
|
422
|
+
|
|
423
|
+
visual = self.rect
|
|
424
|
+
native_left, native_top, native_right, native_bottom = win32gui.GetWindowRect(self.hwnd)
|
|
425
|
+
left_inset = visual.left - native_left
|
|
426
|
+
top_inset = visual.top - native_top
|
|
427
|
+
horizontal_insets = (visual.left - native_left) + (native_right - visual.right)
|
|
428
|
+
vertical_insets = (visual.top - native_top) + (native_bottom - visual.bottom)
|
|
429
|
+
desired_x = visual.left if x is None else x
|
|
430
|
+
desired_y = visual.top if y is None else y
|
|
431
|
+
desired_width = visual.width if width is None else width
|
|
432
|
+
desired_height = visual.height if height is None else height
|
|
433
|
+
win32gui.SetWindowPos(
|
|
434
|
+
self.hwnd, 0, desired_x - left_inset, desired_y - top_inset,
|
|
435
|
+
desired_width + horizontal_insets, desired_height + vertical_insets,
|
|
436
|
+
win32con.SWP_NOZORDER | win32con.SWP_NOACTIVATE,
|
|
437
|
+
)
|
|
438
|
+
return self
|
|
439
|
+
|
|
440
|
+
def move(self, x: int, y: int):
|
|
441
|
+
return self.set_rect(x=x, y=y)
|
|
442
|
+
|
|
443
|
+
def resize(self, width: int, height: int):
|
|
444
|
+
return self.set_rect(width=width, height=height)
|
|
445
|
+
|
|
446
|
+
def activate(self) -> bool:
|
|
447
|
+
if not self.is_valid:
|
|
448
|
+
return False
|
|
449
|
+
win32gui.ShowWindow(self.hwnd, win32con.SW_RESTORE if self.is_minimized else win32con.SW_SHOW)
|
|
450
|
+
try:
|
|
451
|
+
win32gui.SetForegroundWindow(self.hwnd)
|
|
452
|
+
except win32gui.error:
|
|
453
|
+
foreground = win32gui.GetForegroundWindow()
|
|
454
|
+
current_thread = _USER32.GetCurrentThreadId()
|
|
455
|
+
target_thread, _ = win32process.GetWindowThreadProcessId(self.hwnd)
|
|
456
|
+
foreground_thread = win32process.GetWindowThreadProcessId(foreground)[0] if foreground else 0
|
|
457
|
+
attached = []
|
|
458
|
+
try:
|
|
459
|
+
for thread_id in {target_thread, foreground_thread}:
|
|
460
|
+
if thread_id and thread_id != current_thread and _USER32.AttachThreadInput(current_thread, thread_id, True):
|
|
461
|
+
attached.append(thread_id)
|
|
462
|
+
win32gui.BringWindowToTop(self.hwnd)
|
|
463
|
+
win32gui.SetForegroundWindow(self.hwnd)
|
|
464
|
+
except win32gui.error:
|
|
465
|
+
return False
|
|
466
|
+
finally:
|
|
467
|
+
for thread_id in reversed(attached):
|
|
468
|
+
_USER32.AttachThreadInput(current_thread, thread_id, False)
|
|
469
|
+
return win32gui.GetForegroundWindow() == self.hwnd
|
|
470
|
+
|
|
471
|
+
def set_always_on_top(self, toggle: bool = True):
|
|
472
|
+
z_order = win32con.HWND_TOPMOST if bool(toggle) else win32con.HWND_NOTOPMOST
|
|
473
|
+
win32gui.SetWindowPos(
|
|
474
|
+
self.hwnd, z_order, 0, 0, 0, 0,
|
|
475
|
+
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE | win32con.SWP_NOACTIVATE,
|
|
476
|
+
)
|
|
477
|
+
return self
|
|
478
|
+
|
|
479
|
+
def minimize(self):
|
|
480
|
+
win32gui.ShowWindow(self.hwnd, win32con.SW_MINIMIZE)
|
|
481
|
+
return self
|
|
482
|
+
|
|
483
|
+
def is_active(self) -> bool:
|
|
484
|
+
return win32gui.GetForegroundWindow() == self.hwnd
|
|
485
|
+
|
|
486
|
+
def placement(self):
|
|
487
|
+
return win32gui.GetWindowPlacement(self.hwnd)
|
|
488
|
+
|
|
489
|
+
def frame(self, x=None, y=None, w=None, h=None):
|
|
490
|
+
return self.set_rect(x=x, y=y, width=w, height=h)
|
|
491
|
+
|
|
492
|
+
def mize_mini(self):
|
|
493
|
+
return self.minimize()
|
|
494
|
+
|
|
495
|
+
def mize_max(self):
|
|
496
|
+
win32gui.ShowWindow(self.hwnd, win32con.SW_MAXIMIZE)
|
|
497
|
+
return self
|
|
498
|
+
|
|
499
|
+
def mize_normal(self):
|
|
500
|
+
win32gui.ShowWindow(self.hwnd, win32con.SW_NORMAL)
|
|
501
|
+
return self
|
|
502
|
+
|
|
503
|
+
def to_dict(self):
|
|
504
|
+
return {
|
|
505
|
+
"hwnd": self.hwnd,
|
|
506
|
+
"name": self.class_name,
|
|
507
|
+
"class_name": self.class_name,
|
|
508
|
+
"title": self.title,
|
|
509
|
+
"rect": (self.rect.left, self.rect.top, self.rect.right, self.rect.bottom),
|
|
510
|
+
"placement": self.placement(),
|
|
511
|
+
"is_active": self.is_active(),
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
def close(self) -> bool:
|
|
515
|
+
return bool(win32gui.PostMessage(self.hwnd, win32con.WM_CLOSE, 0, 0))
|
|
516
|
+
|
|
517
|
+
def kill(self) -> bool:
|
|
518
|
+
try:
|
|
519
|
+
psutil.Process(self.pid).kill()
|
|
520
|
+
return True
|
|
521
|
+
except (psutil.Error, OSError):
|
|
522
|
+
return False
|
|
523
|
+
|
|
524
|
+
@staticmethod
|
|
525
|
+
def _normalize_capture_rect(rect, client: WindowRect) -> WindowRect:
|
|
526
|
+
if rect is None:
|
|
527
|
+
return client
|
|
528
|
+
if isinstance(rect, WindowRect):
|
|
529
|
+
target = rect
|
|
530
|
+
elif isinstance(rect, Sequence) and not isinstance(rect, (str, bytes)) and len(rect) == 4:
|
|
531
|
+
left, top, right, bottom = map(int, rect)
|
|
532
|
+
target = WindowRect.from_ltrb(left, top, right, bottom)
|
|
533
|
+
else:
|
|
534
|
+
raise TypeError("rect must be WindowRect, (left, top, right, bottom), or None")
|
|
535
|
+
left = max(client.left, target.left)
|
|
536
|
+
top = max(client.top, target.top)
|
|
537
|
+
right = min(client.right, target.right)
|
|
538
|
+
bottom = min(client.bottom, target.bottom)
|
|
539
|
+
if right <= left or bottom <= top:
|
|
540
|
+
raise ValueError("capture rect does not overlap the window client area")
|
|
541
|
+
return WindowRect.from_ltrb(left, top, right, bottom)
|
|
542
|
+
|
|
543
|
+
def _capture_client_printwindow(self, width: int, height: int) -> Optional[Image.Image]:
|
|
544
|
+
hwnd_dc = mfc_dc = save_dc = bitmap = None
|
|
545
|
+
try:
|
|
546
|
+
hwnd_dc = win32gui.GetWindowDC(self.hwnd)
|
|
547
|
+
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
548
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
549
|
+
bitmap = win32ui.CreateBitmap()
|
|
550
|
+
bitmap.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
551
|
+
save_dc.SelectObject(bitmap)
|
|
552
|
+
rendered = _USER32.PrintWindow(
|
|
553
|
+
self.hwnd, save_dc.GetSafeHdc(), 0x1 | 0x2,
|
|
554
|
+
)
|
|
555
|
+
if not rendered:
|
|
556
|
+
return None
|
|
557
|
+
info = bitmap.GetInfo()
|
|
558
|
+
bits = bitmap.GetBitmapBits(True)
|
|
559
|
+
return Image.frombuffer(
|
|
560
|
+
"RGB", (info["bmWidth"], info["bmHeight"]),
|
|
561
|
+
bits, "raw", "BGRX", 0, 1,
|
|
562
|
+
).copy()
|
|
563
|
+
finally:
|
|
564
|
+
if bitmap is not None:
|
|
565
|
+
try:
|
|
566
|
+
win32gui.DeleteObject(bitmap.GetHandle())
|
|
567
|
+
except win32gui.error:
|
|
568
|
+
pass
|
|
569
|
+
if save_dc is not None:
|
|
570
|
+
save_dc.DeleteDC()
|
|
571
|
+
if mfc_dc is not None:
|
|
572
|
+
mfc_dc.DeleteDC()
|
|
573
|
+
if hwnd_dc is not None:
|
|
574
|
+
win32gui.ReleaseDC(self.hwnd, hwnd_dc)
|
|
575
|
+
|
|
576
|
+
def capture(
|
|
577
|
+
self, save_path: Optional[str] = None,
|
|
578
|
+
format: WindowCaptureFormat = WindowCaptureFormat.PIL,
|
|
579
|
+
rect: Optional[Union[WindowRect, Sequence[int]]] = None,
|
|
580
|
+
force_activate: bool = True,
|
|
581
|
+
):
|
|
582
|
+
"""Capture the client area; *rect* uses physical screen coordinates."""
|
|
583
|
+
if not isinstance(format, WindowCaptureFormat):
|
|
584
|
+
raise TypeError("format must be a WindowCaptureFormat value")
|
|
585
|
+
if not self.is_valid:
|
|
586
|
+
return None
|
|
587
|
+
previous = win32gui.GetForegroundWindow() if force_activate else 0
|
|
588
|
+
try:
|
|
589
|
+
if force_activate:
|
|
590
|
+
self.activate()
|
|
591
|
+
client = self.client_rect
|
|
592
|
+
if client.width <= 0 or client.height <= 0:
|
|
593
|
+
return None
|
|
594
|
+
target = self._normalize_capture_rect(rect, client)
|
|
595
|
+
image = self._capture_client_printwindow(client.width, client.height)
|
|
596
|
+
if image is None:
|
|
597
|
+
if not self.is_visible:
|
|
598
|
+
return None
|
|
599
|
+
image = ImageGrab.grab(
|
|
600
|
+
bbox=(client.left, client.top, client.right, client.bottom),
|
|
601
|
+
all_screens=True,
|
|
602
|
+
).convert("RGB")
|
|
603
|
+
crop = (
|
|
604
|
+
target.left - client.left, target.top - client.top,
|
|
605
|
+
target.right - client.left, target.bottom - client.top,
|
|
606
|
+
)
|
|
607
|
+
image = image.crop(crop)
|
|
608
|
+
if save_path:
|
|
609
|
+
image.save(os.fspath(save_path))
|
|
610
|
+
return format.convert(image)
|
|
611
|
+
except (OSError, ValueError, win32gui.error):
|
|
612
|
+
return None
|
|
613
|
+
finally:
|
|
614
|
+
if force_activate and previous and previous != self.hwnd and win32gui.IsWindow(previous):
|
|
615
|
+
try:
|
|
616
|
+
win32gui.SetForegroundWindow(previous)
|
|
617
|
+
except win32gui.error:
|
|
618
|
+
pass
|
|
619
|
+
|
|
620
|
+
def _client_point(self, x: int, y: int, is_relative: bool) -> tuple[int, int]:
|
|
621
|
+
if not isinstance(x, int) or isinstance(x, bool) or not isinstance(y, int) or isinstance(y, bool):
|
|
622
|
+
raise TypeError("x and y must be integers")
|
|
623
|
+
return (x, y) if is_relative else win32gui.ScreenToClient(self.hwnd, (x, y))
|
|
624
|
+
|
|
625
|
+
def _get_lparam(self, x: int, y: int, is_relative: bool = True) -> int:
|
|
626
|
+
client_x, client_y = self._client_point(x, y, is_relative)
|
|
627
|
+
return ((client_y & 0xFFFF) << 16) | (client_x & 0xFFFF)
|
|
628
|
+
|
|
629
|
+
@staticmethod
|
|
630
|
+
def _button_messages(button: str):
|
|
631
|
+
if not isinstance(button, str):
|
|
632
|
+
raise TypeError("button must be a string")
|
|
633
|
+
token = button.casefold()
|
|
634
|
+
swapped = bool(_USER32.GetSystemMetrics(23))
|
|
635
|
+
if token == "primary":
|
|
636
|
+
token = "right" if swapped else "left"
|
|
637
|
+
elif token == "secondary":
|
|
638
|
+
token = "left" if swapped else "right"
|
|
639
|
+
mapping = {
|
|
640
|
+
"left": (win32con.WM_LBUTTONDOWN, win32con.WM_LBUTTONUP, win32con.MK_LBUTTON),
|
|
641
|
+
"right": (win32con.WM_RBUTTONDOWN, win32con.WM_RBUTTONUP, win32con.MK_RBUTTON),
|
|
642
|
+
"middle": (win32con.WM_MBUTTONDOWN, win32con.WM_MBUTTONUP, win32con.MK_MBUTTON),
|
|
643
|
+
}
|
|
644
|
+
if token not in mapping:
|
|
645
|
+
raise ValueError("button must be left, middle, right, primary or secondary")
|
|
646
|
+
return mapping[token]
|
|
647
|
+
|
|
648
|
+
def click(
|
|
649
|
+
self, x: int, y: int, clicks: int = 1, interval: float = 0.1,
|
|
650
|
+
button: str = "primary", move_duration: float = 0.0,
|
|
651
|
+
duration: float = 0.0, offset: int = 0, *, is_relative: bool = True,
|
|
652
|
+
):
|
|
653
|
+
"""Post background mouse messages without moving the physical pointer."""
|
|
654
|
+
# Keep the historical positional API while accepting the documented
|
|
655
|
+
# ``is_relative=`` keyword. This also handles ``click(x, y, False, 'right')``.
|
|
656
|
+
if isinstance(clicks, bool):
|
|
657
|
+
legacy_is_relative = clicks
|
|
658
|
+
legacy_button = interval if isinstance(interval, str) else button
|
|
659
|
+
clicks, interval, button, is_relative = 1, 0.1, legacy_button, legacy_is_relative
|
|
660
|
+
if not isinstance(clicks, int) or isinstance(clicks, bool) or clicks < 1:
|
|
661
|
+
raise ValueError("clicks must be a positive integer")
|
|
662
|
+
for value, label in ((interval, "interval"), (duration, "duration"), (move_duration, "move_duration")):
|
|
663
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)) or value < 0:
|
|
664
|
+
raise ValueError(f"{label} must be a non-negative number")
|
|
665
|
+
if not isinstance(offset, int) or isinstance(offset, bool) or offset < 0:
|
|
666
|
+
raise ValueError("offset must be a non-negative integer")
|
|
667
|
+
if move_duration or offset:
|
|
668
|
+
if is_relative:
|
|
669
|
+
x, y = win32gui.ClientToScreen(self.hwnd, (x, y))
|
|
670
|
+
Mouse.click(
|
|
671
|
+
x, y, clicks=clicks, interval=interval, button=button,
|
|
672
|
+
move_duration=move_duration, duration=duration, offset=offset,
|
|
673
|
+
)
|
|
674
|
+
return self
|
|
675
|
+
lparam = self._get_lparam(x, y, is_relative)
|
|
676
|
+
down, up, state = self._button_messages(button)
|
|
677
|
+
for index in range(clicks):
|
|
678
|
+
win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, 0, lparam)
|
|
679
|
+
win32gui.PostMessage(self.hwnd, down, state, lparam)
|
|
680
|
+
if duration:
|
|
681
|
+
time.sleep(duration)
|
|
682
|
+
win32gui.PostMessage(self.hwnd, up, 0, lparam)
|
|
683
|
+
if interval and index + 1 < clicks:
|
|
684
|
+
time.sleep(interval)
|
|
685
|
+
return self
|
|
686
|
+
|
|
687
|
+
def long_click(
|
|
688
|
+
self, x: int, y: int, duration: float = 1.0,
|
|
689
|
+
is_relative: bool = True, button: str = "left",
|
|
690
|
+
):
|
|
691
|
+
return self.click(x, y, is_relative=is_relative, button=button, duration=duration)
|
|
692
|
+
|
|
693
|
+
def drag(
|
|
694
|
+
self, x1: int, y1: int, x2: int, y2: int, duration: float = 0.5,
|
|
695
|
+
is_relative: bool = True, button: str = "left",
|
|
696
|
+
):
|
|
697
|
+
if isinstance(duration, bool) or not isinstance(duration, (int, float)) or duration < 0:
|
|
698
|
+
raise ValueError("duration must be a non-negative number")
|
|
699
|
+
start_x, start_y = self._client_point(x1, y1, is_relative)
|
|
700
|
+
end_x, end_y = self._client_point(x2, y2, is_relative)
|
|
701
|
+
down, up, state = self._button_messages(button)
|
|
702
|
+
start = self._get_lparam(start_x, start_y, True)
|
|
703
|
+
win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, 0, start)
|
|
704
|
+
win32gui.PostMessage(self.hwnd, down, state, start)
|
|
705
|
+
steps = max(1, min(240, round(max(duration, 0.01) * 60)))
|
|
706
|
+
started = time.perf_counter()
|
|
707
|
+
try:
|
|
708
|
+
for step in range(1, steps + 1):
|
|
709
|
+
progress = step / steps
|
|
710
|
+
current_x = round(start_x + (end_x - start_x) * progress)
|
|
711
|
+
current_y = round(start_y + (end_y - start_y) * progress)
|
|
712
|
+
current = self._get_lparam(current_x, current_y, True)
|
|
713
|
+
win32gui.PostMessage(self.hwnd, win32con.WM_MOUSEMOVE, state, current)
|
|
714
|
+
delay = started + duration * progress - time.perf_counter()
|
|
715
|
+
if delay > 0:
|
|
716
|
+
time.sleep(delay)
|
|
717
|
+
finally:
|
|
718
|
+
end = self._get_lparam(end_x, end_y, True)
|
|
719
|
+
win32gui.PostMessage(self.hwnd, up, 0, end)
|
|
720
|
+
return self
|
|
721
|
+
|
|
722
|
+
def _text_target(self) -> int:
|
|
723
|
+
"""Return the focused child control for this window's GUI thread."""
|
|
724
|
+
thread_id, _ = win32process.GetWindowThreadProcessId(self.hwnd)
|
|
725
|
+
info = _GUITHREADINFO(cbSize=ctypes.sizeof(_GUITHREADINFO))
|
|
726
|
+
try:
|
|
727
|
+
if _USER32.GetGUIThreadInfo(thread_id, ctypes.byref(info)) and info.hwndFocus:
|
|
728
|
+
return int(info.hwndFocus)
|
|
729
|
+
except (AttributeError, OSError):
|
|
730
|
+
pass
|
|
731
|
+
return self.hwnd
|
|
732
|
+
|
|
733
|
+
def type_text(self, text: str, interval: float = 0.01):
|
|
734
|
+
"""Post UTF-16 WM_CHAR messages to the focused child control."""
|
|
735
|
+
if not isinstance(text, str):
|
|
736
|
+
raise TypeError("text must be a string")
|
|
737
|
+
if isinstance(interval, bool) or not isinstance(interval, (int, float)) or interval < 0:
|
|
738
|
+
raise ValueError("interval must be a non-negative number")
|
|
739
|
+
encoded = text.encode("utf-16-le", errors="surrogatepass")
|
|
740
|
+
units = [int.from_bytes(encoded[index:index + 2], "little")
|
|
741
|
+
for index in range(0, len(encoded), 2)]
|
|
742
|
+
target = self._text_target()
|
|
743
|
+
for index, unit in enumerate(units):
|
|
744
|
+
win32gui.PostMessage(target, win32con.WM_CHAR, unit, 1)
|
|
745
|
+
if interval and index + 1 < len(units):
|
|
746
|
+
time.sleep(interval)
|
|
747
|
+
return self
|