PyClickerRPA 0.1.0__tar.gz
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.
- pyclickerrpa-0.1.0/PKG-INFO +90 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/PKG-INFO +90 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/SOURCES.txt +35 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/dependency_links.txt +1 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/entry_points.txt +5 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/requires.txt +11 -0
- pyclickerrpa-0.1.0/PyClickerRPA.egg-info/top_level.txt +1 -0
- pyclickerrpa-0.1.0/README.md +66 -0
- pyclickerrpa-0.1.0/autoclicker/__init__.py +14 -0
- pyclickerrpa-0.1.0/autoclicker/app.py +21 -0
- pyclickerrpa-0.1.0/autoclicker/capture.py +94 -0
- pyclickerrpa-0.1.0/autoclicker/clicker.py +212 -0
- pyclickerrpa-0.1.0/autoclicker/dpi.py +30 -0
- pyclickerrpa-0.1.0/autoclicker/gui/__init__.py +1 -0
- pyclickerrpa-0.1.0/autoclicker/gui/flow_connection.py +91 -0
- pyclickerrpa-0.1.0/autoclicker/gui/flow_editor.py +340 -0
- pyclickerrpa-0.1.0/autoclicker/gui/flow_node.py +107 -0
- pyclickerrpa-0.1.0/autoclicker/gui/hotkeys.py +40 -0
- pyclickerrpa-0.1.0/autoclicker/gui/main_window.py +651 -0
- pyclickerrpa-0.1.0/autoclicker/gui/monitor.py +84 -0
- pyclickerrpa-0.1.0/autoclicker/gui/preview_label.py +176 -0
- pyclickerrpa-0.1.0/autoclicker/gui/qt_runner.py +71 -0
- pyclickerrpa-0.1.0/autoclicker/gui/recorder.py +163 -0
- pyclickerrpa-0.1.0/autoclicker/gui/scheduler_widget.py +160 -0
- pyclickerrpa-0.1.0/autoclicker/gui/snippet_library.py +117 -0
- pyclickerrpa-0.1.0/autoclicker/gui/task_widget.py +547 -0
- pyclickerrpa-0.1.0/autoclicker/matcher.py +152 -0
- pyclickerrpa-0.1.0/autoclicker/models.py +468 -0
- pyclickerrpa-0.1.0/autoclicker/ocr.py +63 -0
- pyclickerrpa-0.1.0/autoclicker/run.py +64 -0
- pyclickerrpa-0.1.0/autoclicker/runner.py +666 -0
- pyclickerrpa-0.1.0/autoclicker/windows.py +53 -0
- pyclickerrpa-0.1.0/pyproject.toml +42 -0
- pyclickerrpa-0.1.0/setup.cfg +4 -0
- pyclickerrpa-0.1.0/tests/test_matcher.py +64 -0
- pyclickerrpa-0.1.0/tests/test_models.py +88 -0
- pyclickerrpa-0.1.0/tests/test_runner.py +196 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyClickerRPA
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visual auto-click / RPA tool: window capture, OpenCV image matching, OCR, a node flow editor and step-list editor, recorder, scheduler and triggers (Windows).
|
|
5
|
+
Author: Equinox
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
8
|
+
Keywords: autoclicker,rpa,automation,opencv,template-matching,ocr,gui,windows
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Desktop Environment
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: numpy>=1.23
|
|
17
|
+
Requires-Dist: opencv-python>=4.8
|
|
18
|
+
Requires-Dist: pywin32>=305; sys_platform == "win32"
|
|
19
|
+
Requires-Dist: pynput>=1.7
|
|
20
|
+
Requires-Dist: PySide6>=6.5
|
|
21
|
+
Provides-Extra: ocr
|
|
22
|
+
Requires-Dist: paddleocr>=2.7; extra == "ocr"
|
|
23
|
+
Requires-Dist: paddlepaddle>=2.5; extra == "ocr"
|
|
24
|
+
|
|
25
|
+
# PyClickerRPA
|
|
26
|
+
|
|
27
|
+
A visual auto-click / RPA tool for Windows — window/screen capture, OpenCV
|
|
28
|
+
template matching, optional OCR, a node-based flow editor **and** a linear
|
|
29
|
+
step-list editor, an action recorder, a scheduler, and image/color triggers.
|
|
30
|
+
|
|
31
|
+
It is a Python/PySide6 port of a Qt/C++ AutoClicker, and reads/writes the same
|
|
32
|
+
`.json` task format.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install PyClickerRPA
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Optional OCR (find-text / read-text steps) needs PaddleOCR:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install "PyClickerRPA[ocr]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Run
|
|
47
|
+
|
|
48
|
+
GUI:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
autoclicker
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Headless (run a saved task against a window by title, or the whole screen):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
autoclicker-run task.json --window "Notepad"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Targeting**: pick any visible window (or the whole screen); background-window
|
|
63
|
+
capture via `PrintWindow`.
|
|
64
|
+
- **Matching**: OpenCV `TM_CCOEFF_NORMED` template matching (multi-scale, ROI,
|
|
65
|
+
multi-target), pixel-color checks, and optional PaddleOCR text find / read.
|
|
66
|
+
- **Editors**: drag-and-drop **node flow graph** (back-edges become loops) and a
|
|
67
|
+
**step list** with a full per-step parameter dialog; both interoperate.
|
|
68
|
+
- **~30+ step types**: click image/text/position, click-all, relative & offset
|
|
69
|
+
clicks, wait / random wait, drag / long-press / pinch / scroll, key / type /
|
|
70
|
+
hotkey, loop / goto / while / do-until, if image/text/color/count/var, set-var
|
|
71
|
+
(`++`/`--`, `last_x/last_y/last_text`), read-text→variable, call sub-task,
|
|
72
|
+
groups.
|
|
73
|
+
- **Authoring aids**: action recorder (global hooks), snippet library, task
|
|
74
|
+
packages (`.zip` with templates), edit-on-preview coordinate picking,
|
|
75
|
+
template crop / color pick, single-step test run.
|
|
76
|
+
- **Runtime**: live variables panel, hit markers on the preview, screenshot on
|
|
77
|
+
failure, retry / repeat / until policies.
|
|
78
|
+
- **Automation**: global hotkeys (F6 run/stop, ESC stop), scheduler, and a
|
|
79
|
+
monitor/trigger mode (run a task when an image/color appears) with tray icon.
|
|
80
|
+
- **Correctness**: multi-monitor + high-DPI aware coordinates.
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- **Windows only** (uses Win32 for capture and `SendInput`).
|
|
85
|
+
- OCR's first run downloads PaddleOCR models (~100 MB).
|
|
86
|
+
- Some target windows may require running as administrator.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: PyClickerRPA
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Visual auto-click / RPA tool: window capture, OpenCV image matching, OCR, a node flow editor and step-list editor, recorder, scheduler and triggers (Windows).
|
|
5
|
+
Author: Equinox
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
8
|
+
Keywords: autoclicker,rpa,automation,opencv,template-matching,ocr,gui,windows
|
|
9
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
10
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Desktop Environment
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Requires-Python: >=3.10
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: numpy>=1.23
|
|
17
|
+
Requires-Dist: opencv-python>=4.8
|
|
18
|
+
Requires-Dist: pywin32>=305; sys_platform == "win32"
|
|
19
|
+
Requires-Dist: pynput>=1.7
|
|
20
|
+
Requires-Dist: PySide6>=6.5
|
|
21
|
+
Provides-Extra: ocr
|
|
22
|
+
Requires-Dist: paddleocr>=2.7; extra == "ocr"
|
|
23
|
+
Requires-Dist: paddlepaddle>=2.5; extra == "ocr"
|
|
24
|
+
|
|
25
|
+
# PyClickerRPA
|
|
26
|
+
|
|
27
|
+
A visual auto-click / RPA tool for Windows — window/screen capture, OpenCV
|
|
28
|
+
template matching, optional OCR, a node-based flow editor **and** a linear
|
|
29
|
+
step-list editor, an action recorder, a scheduler, and image/color triggers.
|
|
30
|
+
|
|
31
|
+
It is a Python/PySide6 port of a Qt/C++ AutoClicker, and reads/writes the same
|
|
32
|
+
`.json` task format.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
pip install PyClickerRPA
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Optional OCR (find-text / read-text steps) needs PaddleOCR:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
pip install "PyClickerRPA[ocr]"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Run
|
|
47
|
+
|
|
48
|
+
GUI:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
autoclicker
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Headless (run a saved task against a window by title, or the whole screen):
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
autoclicker-run task.json --window "Notepad"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Features
|
|
61
|
+
|
|
62
|
+
- **Targeting**: pick any visible window (or the whole screen); background-window
|
|
63
|
+
capture via `PrintWindow`.
|
|
64
|
+
- **Matching**: OpenCV `TM_CCOEFF_NORMED` template matching (multi-scale, ROI,
|
|
65
|
+
multi-target), pixel-color checks, and optional PaddleOCR text find / read.
|
|
66
|
+
- **Editors**: drag-and-drop **node flow graph** (back-edges become loops) and a
|
|
67
|
+
**step list** with a full per-step parameter dialog; both interoperate.
|
|
68
|
+
- **~30+ step types**: click image/text/position, click-all, relative & offset
|
|
69
|
+
clicks, wait / random wait, drag / long-press / pinch / scroll, key / type /
|
|
70
|
+
hotkey, loop / goto / while / do-until, if image/text/color/count/var, set-var
|
|
71
|
+
(`++`/`--`, `last_x/last_y/last_text`), read-text→variable, call sub-task,
|
|
72
|
+
groups.
|
|
73
|
+
- **Authoring aids**: action recorder (global hooks), snippet library, task
|
|
74
|
+
packages (`.zip` with templates), edit-on-preview coordinate picking,
|
|
75
|
+
template crop / color pick, single-step test run.
|
|
76
|
+
- **Runtime**: live variables panel, hit markers on the preview, screenshot on
|
|
77
|
+
failure, retry / repeat / until policies.
|
|
78
|
+
- **Automation**: global hotkeys (F6 run/stop, ESC stop), scheduler, and a
|
|
79
|
+
monitor/trigger mode (run a task when an image/color appears) with tray icon.
|
|
80
|
+
- **Correctness**: multi-monitor + high-DPI aware coordinates.
|
|
81
|
+
|
|
82
|
+
## Notes
|
|
83
|
+
|
|
84
|
+
- **Windows only** (uses Win32 for capture and `SendInput`).
|
|
85
|
+
- OCR's first run downloads PaddleOCR models (~100 MB).
|
|
86
|
+
- Some target windows may require running as administrator.
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
PyClickerRPA.egg-info/PKG-INFO
|
|
4
|
+
PyClickerRPA.egg-info/SOURCES.txt
|
|
5
|
+
PyClickerRPA.egg-info/dependency_links.txt
|
|
6
|
+
PyClickerRPA.egg-info/entry_points.txt
|
|
7
|
+
PyClickerRPA.egg-info/requires.txt
|
|
8
|
+
PyClickerRPA.egg-info/top_level.txt
|
|
9
|
+
autoclicker/__init__.py
|
|
10
|
+
autoclicker/app.py
|
|
11
|
+
autoclicker/capture.py
|
|
12
|
+
autoclicker/clicker.py
|
|
13
|
+
autoclicker/dpi.py
|
|
14
|
+
autoclicker/matcher.py
|
|
15
|
+
autoclicker/models.py
|
|
16
|
+
autoclicker/ocr.py
|
|
17
|
+
autoclicker/run.py
|
|
18
|
+
autoclicker/runner.py
|
|
19
|
+
autoclicker/windows.py
|
|
20
|
+
autoclicker/gui/__init__.py
|
|
21
|
+
autoclicker/gui/flow_connection.py
|
|
22
|
+
autoclicker/gui/flow_editor.py
|
|
23
|
+
autoclicker/gui/flow_node.py
|
|
24
|
+
autoclicker/gui/hotkeys.py
|
|
25
|
+
autoclicker/gui/main_window.py
|
|
26
|
+
autoclicker/gui/monitor.py
|
|
27
|
+
autoclicker/gui/preview_label.py
|
|
28
|
+
autoclicker/gui/qt_runner.py
|
|
29
|
+
autoclicker/gui/recorder.py
|
|
30
|
+
autoclicker/gui/scheduler_widget.py
|
|
31
|
+
autoclicker/gui/snippet_library.py
|
|
32
|
+
autoclicker/gui/task_widget.py
|
|
33
|
+
tests/test_matcher.py
|
|
34
|
+
tests/test_models.py
|
|
35
|
+
tests/test_runner.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
autoclicker
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# PyClickerRPA
|
|
2
|
+
|
|
3
|
+
A visual auto-click / RPA tool for Windows — window/screen capture, OpenCV
|
|
4
|
+
template matching, optional OCR, a node-based flow editor **and** a linear
|
|
5
|
+
step-list editor, an action recorder, a scheduler, and image/color triggers.
|
|
6
|
+
|
|
7
|
+
It is a Python/PySide6 port of a Qt/C++ AutoClicker, and reads/writes the same
|
|
8
|
+
`.json` task format.
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
pip install PyClickerRPA
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Optional OCR (find-text / read-text steps) needs PaddleOCR:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
pip install "PyClickerRPA[ocr]"
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Run
|
|
23
|
+
|
|
24
|
+
GUI:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
autoclicker
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Headless (run a saved task against a window by title, or the whole screen):
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
autoclicker-run task.json --window "Notepad"
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- **Targeting**: pick any visible window (or the whole screen); background-window
|
|
39
|
+
capture via `PrintWindow`.
|
|
40
|
+
- **Matching**: OpenCV `TM_CCOEFF_NORMED` template matching (multi-scale, ROI,
|
|
41
|
+
multi-target), pixel-color checks, and optional PaddleOCR text find / read.
|
|
42
|
+
- **Editors**: drag-and-drop **node flow graph** (back-edges become loops) and a
|
|
43
|
+
**step list** with a full per-step parameter dialog; both interoperate.
|
|
44
|
+
- **~30+ step types**: click image/text/position, click-all, relative & offset
|
|
45
|
+
clicks, wait / random wait, drag / long-press / pinch / scroll, key / type /
|
|
46
|
+
hotkey, loop / goto / while / do-until, if image/text/color/count/var, set-var
|
|
47
|
+
(`++`/`--`, `last_x/last_y/last_text`), read-text→variable, call sub-task,
|
|
48
|
+
groups.
|
|
49
|
+
- **Authoring aids**: action recorder (global hooks), snippet library, task
|
|
50
|
+
packages (`.zip` with templates), edit-on-preview coordinate picking,
|
|
51
|
+
template crop / color pick, single-step test run.
|
|
52
|
+
- **Runtime**: live variables panel, hit markers on the preview, screenshot on
|
|
53
|
+
failure, retry / repeat / until policies.
|
|
54
|
+
- **Automation**: global hotkeys (F6 run/stop, ESC stop), scheduler, and a
|
|
55
|
+
monitor/trigger mode (run a task when an image/color appears) with tray icon.
|
|
56
|
+
- **Correctness**: multi-monitor + high-DPI aware coordinates.
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- **Windows only** (uses Win32 for capture and `SendInput`).
|
|
61
|
+
- OCR's first run downloads PaddleOCR models (~100 MB).
|
|
62
|
+
- Some target windows may require running as administrator.
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""AutoClicker — Python port of the Qt/C++ visual auto-click / RPA tool.
|
|
2
|
+
|
|
3
|
+
Phase 1 exposes the headless engine (models + capture + clicker + matcher +
|
|
4
|
+
windows + runner). The GUI (PySide6) lands in later phases.
|
|
5
|
+
"""
|
|
6
|
+
from autoclicker.models import (
|
|
7
|
+
Step, Task, StepType, UntilCondition, RetryPolicy,
|
|
8
|
+
RepeatConfig, UntilConfig, RetryConfig,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"Step", "Task", "StepType", "UntilCondition", "RetryPolicy",
|
|
13
|
+
"RepeatConfig", "UntilConfig", "RetryConfig",
|
|
14
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""GUI entry point: python -m autoclicker.app"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def main() -> int:
|
|
8
|
+
from autoclicker.dpi import set_dpi_aware
|
|
9
|
+
set_dpi_aware()
|
|
10
|
+
|
|
11
|
+
from PySide6.QtWidgets import QApplication
|
|
12
|
+
from autoclicker.gui.main_window import MainWindow
|
|
13
|
+
|
|
14
|
+
app = QApplication(sys.argv)
|
|
15
|
+
win = MainWindow()
|
|
16
|
+
win.show()
|
|
17
|
+
return app.exec()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""Screen / window capture -> BGR numpy arrays (OpenCV order).
|
|
2
|
+
|
|
3
|
+
Port of capture.cpp: ``capture_window`` grabs a specific HWND via PrintWindow
|
|
4
|
+
(works for background windows that aren't fully occluded); ``capture_screen``
|
|
5
|
+
grabs the whole virtual desktop via GDI BitBlt. No mss dependency — win32 GDI
|
|
6
|
+
covers both.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import win32gui
|
|
14
|
+
import win32ui
|
|
15
|
+
import win32con
|
|
16
|
+
_HAS_WIN32 = True
|
|
17
|
+
except ImportError: # allow import on non-Windows for tests that mock capture
|
|
18
|
+
_HAS_WIN32 = False
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# PrintWindow flag that renders the full content (incl. DWM-composited/partly hidden)
|
|
22
|
+
PW_RENDERFULLCONTENT = 0x00000002
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _bitmap_to_bgr(bmp, width: int, height: int) -> np.ndarray:
|
|
26
|
+
"""Convert a win32ui bitmap's BGRA bits into an (H, W, 3) BGR ndarray."""
|
|
27
|
+
bits = bmp.GetBitmapBits(True)
|
|
28
|
+
arr = np.frombuffer(bits, dtype=np.uint8).reshape((height, width, 4))
|
|
29
|
+
return arr[:, :, :3].copy() # drop alpha; BGRA -> BGR
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def capture_window(hwnd) -> np.ndarray:
|
|
33
|
+
"""Capture the client area of *hwnd* as a BGR ndarray."""
|
|
34
|
+
if not _HAS_WIN32:
|
|
35
|
+
raise RuntimeError("win32 modules unavailable")
|
|
36
|
+
|
|
37
|
+
left, top, right, bottom = win32gui.GetClientRect(hwnd)
|
|
38
|
+
width, height = right - left, bottom - top
|
|
39
|
+
if width <= 0 or height <= 0:
|
|
40
|
+
return np.empty((0, 0, 3), dtype=np.uint8)
|
|
41
|
+
|
|
42
|
+
hwnd_dc = win32gui.GetWindowDC(hwnd)
|
|
43
|
+
mfc_dc = win32ui.CreateDCFromHandle(hwnd_dc)
|
|
44
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
45
|
+
bmp = win32ui.CreateBitmap()
|
|
46
|
+
bmp.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
47
|
+
save_dc.SelectObject(bmp)
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
import ctypes
|
|
51
|
+
ok = ctypes.windll.user32.PrintWindow(hwnd, save_dc.GetSafeHdc(), PW_RENDERFULLCONTENT)
|
|
52
|
+
if not ok:
|
|
53
|
+
# Fallback: BitBlt the window DC
|
|
54
|
+
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (0, 0), win32con.SRCCOPY)
|
|
55
|
+
img = _bitmap_to_bgr(bmp, width, height)
|
|
56
|
+
finally:
|
|
57
|
+
win32gui.DeleteObject(bmp.GetHandle())
|
|
58
|
+
save_dc.DeleteDC()
|
|
59
|
+
mfc_dc.DeleteDC()
|
|
60
|
+
win32gui.ReleaseDC(hwnd, hwnd_dc)
|
|
61
|
+
return img
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def capture_screen() -> np.ndarray:
|
|
65
|
+
"""Capture the entire virtual desktop as a BGR ndarray."""
|
|
66
|
+
if not _HAS_WIN32:
|
|
67
|
+
raise RuntimeError("win32 modules unavailable")
|
|
68
|
+
|
|
69
|
+
x = win32api_GetSystemMetrics(76) # SM_XVIRTUALSCREEN
|
|
70
|
+
y = win32api_GetSystemMetrics(77) # SM_YVIRTUALSCREEN
|
|
71
|
+
width = win32api_GetSystemMetrics(78) # SM_CXVIRTUALSCREEN
|
|
72
|
+
height = win32api_GetSystemMetrics(79) # SM_CYVIRTUALSCREEN
|
|
73
|
+
|
|
74
|
+
hdesktop = win32gui.GetDesktopWindow()
|
|
75
|
+
desktop_dc = win32gui.GetWindowDC(hdesktop)
|
|
76
|
+
mfc_dc = win32ui.CreateDCFromHandle(desktop_dc)
|
|
77
|
+
save_dc = mfc_dc.CreateCompatibleDC()
|
|
78
|
+
bmp = win32ui.CreateBitmap()
|
|
79
|
+
bmp.CreateCompatibleBitmap(mfc_dc, width, height)
|
|
80
|
+
save_dc.SelectObject(bmp)
|
|
81
|
+
try:
|
|
82
|
+
save_dc.BitBlt((0, 0), (width, height), mfc_dc, (x, y), win32con.SRCCOPY)
|
|
83
|
+
img = _bitmap_to_bgr(bmp, width, height)
|
|
84
|
+
finally:
|
|
85
|
+
win32gui.DeleteObject(bmp.GetHandle())
|
|
86
|
+
save_dc.DeleteDC()
|
|
87
|
+
mfc_dc.DeleteDC()
|
|
88
|
+
win32gui.ReleaseDC(hdesktop, desktop_dc)
|
|
89
|
+
return img
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def win32api_GetSystemMetrics(index: int) -> int:
|
|
93
|
+
import win32api
|
|
94
|
+
return win32api.GetSystemMetrics(index)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Mouse / keyboard synthesis via Win32 SendInput — port of clicker.cpp.
|
|
2
|
+
|
|
3
|
+
Window-local coordinates are converted to screen coords (ClientToScreen) then
|
|
4
|
+
normalised to the 0..65535 absolute range over the primary screen, matching the
|
|
5
|
+
C++ implementation exactly (incl. its single-monitor assumption).
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import ctypes
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import win32gui
|
|
14
|
+
import win32con
|
|
15
|
+
_HAS_WIN32 = True
|
|
16
|
+
except ImportError:
|
|
17
|
+
_HAS_WIN32 = False
|
|
18
|
+
|
|
19
|
+
WHEEL_DELTA = 120
|
|
20
|
+
|
|
21
|
+
# ---- ctypes SendInput plumbing ------------------------------------------------
|
|
22
|
+
if _HAS_WIN32:
|
|
23
|
+
from ctypes import wintypes
|
|
24
|
+
|
|
25
|
+
ULONG_PTR = ctypes.wintypes.WPARAM
|
|
26
|
+
|
|
27
|
+
class _MOUSEINPUT(ctypes.Structure):
|
|
28
|
+
_fields_ = [("dx", wintypes.LONG), ("dy", wintypes.LONG),
|
|
29
|
+
("mouseData", wintypes.DWORD), ("dwFlags", wintypes.DWORD),
|
|
30
|
+
("time", wintypes.DWORD), ("dwExtraInfo", ULONG_PTR)]
|
|
31
|
+
|
|
32
|
+
class _KEYBDINPUT(ctypes.Structure):
|
|
33
|
+
_fields_ = [("wVk", wintypes.WORD), ("wScan", wintypes.WORD),
|
|
34
|
+
("dwFlags", wintypes.DWORD), ("time", wintypes.DWORD),
|
|
35
|
+
("dwExtraInfo", ULONG_PTR)]
|
|
36
|
+
|
|
37
|
+
class _INPUTunion(ctypes.Union):
|
|
38
|
+
_fields_ = [("mi", _MOUSEINPUT), ("ki", _KEYBDINPUT)]
|
|
39
|
+
|
|
40
|
+
class _INPUT(ctypes.Structure):
|
|
41
|
+
_fields_ = [("type", wintypes.DWORD), ("u", _INPUTunion)]
|
|
42
|
+
|
|
43
|
+
INPUT_MOUSE, INPUT_KEYBOARD = 0, 1
|
|
44
|
+
MOUSEEVENTF_MOVE, MOUSEEVENTF_ABSOLUTE = 0x0001, 0x8000
|
|
45
|
+
MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP = 0x0002, 0x0004
|
|
46
|
+
MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP = 0x0008, 0x0010
|
|
47
|
+
MOUSEEVENTF_WHEEL = 0x0800
|
|
48
|
+
MOUSEEVENTF_VIRTUALDESK = 0x4000
|
|
49
|
+
KEYEVENTF_KEYUP, KEYEVENTF_UNICODE = 0x0002, 0x0004
|
|
50
|
+
SM_CXSCREEN, SM_CYSCREEN = 0, 1
|
|
51
|
+
SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN = 76, 77
|
|
52
|
+
SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN = 78, 79
|
|
53
|
+
# Absolute move across the whole virtual desktop (multi-monitor correct)
|
|
54
|
+
_ABS_MOVE = MOUSEEVENTF_MOVE | MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK
|
|
55
|
+
|
|
56
|
+
def _send(*inputs: _INPUT) -> None:
|
|
57
|
+
n = len(inputs)
|
|
58
|
+
arr = (_INPUT * n)(*inputs)
|
|
59
|
+
ctypes.windll.user32.SendInput(n, arr, ctypes.sizeof(_INPUT))
|
|
60
|
+
|
|
61
|
+
def _mouse(dx=0, dy=0, data=0, flags=0) -> _INPUT:
|
|
62
|
+
return _INPUT(type=INPUT_MOUSE, u=_INPUTunion(
|
|
63
|
+
mi=_MOUSEINPUT(dx, dy, data & 0xFFFFFFFF, flags, 0, 0)))
|
|
64
|
+
|
|
65
|
+
def _kbd(vk=0, scan=0, flags=0) -> _INPUT:
|
|
66
|
+
return _INPUT(type=INPUT_KEYBOARD, u=_INPUTunion(
|
|
67
|
+
ki=_KEYBDINPUT(vk, scan, flags, 0, 0)))
|
|
68
|
+
|
|
69
|
+
def _metric(i: int) -> int:
|
|
70
|
+
return ctypes.windll.user32.GetSystemMetrics(i)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _to_screen(hwnd, x: int, y: int) -> tuple[int, int]:
|
|
74
|
+
if hwnd:
|
|
75
|
+
return win32gui.ClientToScreen(hwnd, (x, y))
|
|
76
|
+
return x, y
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _norm(sx: int, sy: int) -> tuple[int, int]:
|
|
80
|
+
# Normalize to 0..65535 over the whole virtual desktop (all monitors),
|
|
81
|
+
# to be paired with MOUSEEVENTF_VIRTUALDESK. Fixes the primary-screen-only
|
|
82
|
+
# assumption that misplaced clicks on multi-monitor / scaled setups.
|
|
83
|
+
x0, y0 = _metric(SM_XVIRTUALSCREEN), _metric(SM_YVIRTUALSCREEN)
|
|
84
|
+
cx = _metric(SM_CXVIRTUALSCREEN) or 1
|
|
85
|
+
cy = _metric(SM_CYVIRTUALSCREEN) or 1
|
|
86
|
+
nx = int((sx - x0) * 65535.0 / cx)
|
|
87
|
+
ny = int((sy - y0) * 65535.0 / cy)
|
|
88
|
+
return nx, ny
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class Clicker:
|
|
92
|
+
"""Thin, injectable wrapper over Win32 input synthesis."""
|
|
93
|
+
|
|
94
|
+
# ---- mouse ----
|
|
95
|
+
def click(self, hwnd, x: int, y: int) -> None:
|
|
96
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
97
|
+
nx, ny = _norm(sx, sy)
|
|
98
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE),
|
|
99
|
+
_mouse(flags=MOUSEEVENTF_LEFTDOWN),
|
|
100
|
+
_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
101
|
+
|
|
102
|
+
def double_click(self, hwnd, x: int, y: int) -> None:
|
|
103
|
+
self.click(hwnd, x, y)
|
|
104
|
+
time.sleep(0.05)
|
|
105
|
+
self.click(hwnd, x, y)
|
|
106
|
+
|
|
107
|
+
def right_click(self, hwnd, x: int, y: int) -> None:
|
|
108
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
109
|
+
nx, ny = _norm(sx, sy)
|
|
110
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE),
|
|
111
|
+
_mouse(flags=MOUSEEVENTF_RIGHTDOWN),
|
|
112
|
+
_mouse(flags=MOUSEEVENTF_RIGHTUP))
|
|
113
|
+
|
|
114
|
+
def long_press(self, hwnd, x: int, y: int, hold_ms: int) -> None:
|
|
115
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
116
|
+
nx, ny = _norm(sx, sy)
|
|
117
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
118
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTDOWN))
|
|
119
|
+
time.sleep(hold_ms / 1000.0)
|
|
120
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
121
|
+
|
|
122
|
+
def swipe(self, hwnd, x1, y1, x2, y2, duration_ms: int = 500) -> None:
|
|
123
|
+
steps = 20
|
|
124
|
+
step_delay = duration_ms / steps / 1000.0
|
|
125
|
+
sx1, sy1 = _to_screen(hwnd, x1, y1)
|
|
126
|
+
sx2, sy2 = _to_screen(hwnd, x2, y2)
|
|
127
|
+
nx, ny = _norm(sx1, sy1)
|
|
128
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
129
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTDOWN))
|
|
130
|
+
for i in range(1, steps + 1):
|
|
131
|
+
cx = sx1 + (sx2 - sx1) * i // steps
|
|
132
|
+
cy = sy1 + (sy2 - sy1) * i // steps
|
|
133
|
+
mx, my = _norm(cx, cy)
|
|
134
|
+
_send(_mouse(mx, my, flags=_ABS_MOVE))
|
|
135
|
+
time.sleep(step_delay)
|
|
136
|
+
_send(_mouse(flags=MOUSEEVENTF_LEFTUP))
|
|
137
|
+
|
|
138
|
+
def drag(self, hwnd, x1, y1, x2, y2, duration_ms: int = 300) -> None:
|
|
139
|
+
self.swipe(hwnd, x1, y1, x2, y2, duration_ms)
|
|
140
|
+
|
|
141
|
+
def pinch(self, hwnd, cx, cy, start_dist, end_dist, duration_ms: int = 400) -> None:
|
|
142
|
+
sx, sy = _to_screen(hwnd, cx, cy)
|
|
143
|
+
nx, ny = _norm(sx, sy)
|
|
144
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
145
|
+
steps = 10
|
|
146
|
+
step_delay = duration_ms / steps / 1000.0
|
|
147
|
+
scroll_per_step = ((end_dist - start_dist) * WHEEL_DELTA) // (steps * 50)
|
|
148
|
+
for _ in range(steps):
|
|
149
|
+
_send(_mouse(data=scroll_per_step, flags=MOUSEEVENTF_WHEEL))
|
|
150
|
+
time.sleep(step_delay)
|
|
151
|
+
|
|
152
|
+
def scroll(self, hwnd, x, y, amount: int) -> None:
|
|
153
|
+
sx, sy = _to_screen(hwnd, x, y)
|
|
154
|
+
nx, ny = _norm(sx, sy)
|
|
155
|
+
_send(_mouse(nx, ny, flags=_ABS_MOVE))
|
|
156
|
+
_send(_mouse(data=amount * WHEEL_DELTA, flags=MOUSEEVENTF_WHEEL))
|
|
157
|
+
|
|
158
|
+
# ---- keyboard ----
|
|
159
|
+
def key_down(self, vk: int) -> None:
|
|
160
|
+
_send(_kbd(vk=vk))
|
|
161
|
+
|
|
162
|
+
def key_up(self, vk: int) -> None:
|
|
163
|
+
_send(_kbd(vk=vk, flags=KEYEVENTF_KEYUP))
|
|
164
|
+
|
|
165
|
+
def key_press(self, vk: int) -> None:
|
|
166
|
+
self.key_down(vk)
|
|
167
|
+
time.sleep(0.03)
|
|
168
|
+
self.key_up(vk)
|
|
169
|
+
|
|
170
|
+
def type_text(self, text: str) -> None:
|
|
171
|
+
for ch in text:
|
|
172
|
+
code = ord(ch)
|
|
173
|
+
_send(_kbd(scan=code, flags=KEYEVENTF_UNICODE),
|
|
174
|
+
_kbd(scan=code, flags=KEYEVENTF_UNICODE | KEYEVENTF_KEYUP))
|
|
175
|
+
time.sleep(0.02)
|
|
176
|
+
|
|
177
|
+
def hotkey(self, modifier: int, vk: int) -> None:
|
|
178
|
+
if modifier:
|
|
179
|
+
self.key_down(modifier)
|
|
180
|
+
self.key_press(vk)
|
|
181
|
+
if modifier:
|
|
182
|
+
self.key_up(modifier)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# ---- vk parsing (port of Clicker::parseVkCode) --------------------------------
|
|
186
|
+
_NAMED_VK = {
|
|
187
|
+
"ENTER": 0x0D, "RETURN": 0x0D, "ESC": 0x1B, "ESCAPE": 0x1B, "TAB": 0x09,
|
|
188
|
+
"SPACE": 0x20, "BACKSPACE": 0x08, "BS": 0x08, "DELETE": 0x2E, "DEL": 0x2E,
|
|
189
|
+
"UP": 0x26, "DOWN": 0x28, "LEFT": 0x25, "RIGHT": 0x27, "HOME": 0x24, "END": 0x23,
|
|
190
|
+
"PAGEUP": 0x21, "PGUP": 0x21, "PAGEDOWN": 0x22, "PGDN": 0x22,
|
|
191
|
+
"CTRL": 0x11, "ALT": 0x12, "SHIFT": 0x10, "WIN": 0x5B,
|
|
192
|
+
}
|
|
193
|
+
_VK_F1 = 0x70
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def parse_vk(key_name: str) -> int:
|
|
197
|
+
"""Map a key name to a virtual-key code (0 if unknown)."""
|
|
198
|
+
k = (key_name or "").upper().strip()
|
|
199
|
+
if k in _NAMED_VK:
|
|
200
|
+
return _NAMED_VK[k]
|
|
201
|
+
if k.startswith("F") and len(k) <= 3:
|
|
202
|
+
try:
|
|
203
|
+
n = int(k[1:])
|
|
204
|
+
except ValueError:
|
|
205
|
+
n = 0
|
|
206
|
+
if 1 <= n <= 24:
|
|
207
|
+
return _VK_F1 + n - 1
|
|
208
|
+
if len(k) == 1:
|
|
209
|
+
c = k[0]
|
|
210
|
+
if "A" <= c <= "Z" or "0" <= c <= "9":
|
|
211
|
+
return ord(c)
|
|
212
|
+
return 0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Process DPI awareness.
|
|
2
|
+
|
|
3
|
+
Making the process per-monitor DPI-aware keeps window coordinates, screenshots
|
|
4
|
+
and synthesized clicks in the same physical-pixel space on scaled displays
|
|
5
|
+
(>100% scaling), avoiding offset clicks. Call once at startup.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def set_dpi_aware() -> None:
|
|
13
|
+
if os.name != "nt":
|
|
14
|
+
return
|
|
15
|
+
import ctypes
|
|
16
|
+
# PER_MONITOR_AWARE_V2 (Win10 1703+) -> PER_MONITOR (Win8.1) -> system-DPI (legacy)
|
|
17
|
+
try:
|
|
18
|
+
ctypes.windll.user32.SetProcessDpiAwarenessContext(ctypes.c_void_p(-4))
|
|
19
|
+
return
|
|
20
|
+
except Exception:
|
|
21
|
+
pass
|
|
22
|
+
try:
|
|
23
|
+
ctypes.windll.shcore.SetProcessDpiAwareness(2)
|
|
24
|
+
return
|
|
25
|
+
except Exception:
|
|
26
|
+
pass
|
|
27
|
+
try:
|
|
28
|
+
ctypes.windll.user32.SetProcessDPIAware()
|
|
29
|
+
except Exception:
|
|
30
|
+
pass
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""PySide6 GUI for AutoClicker (Phase 2+)."""
|