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.
Files changed (37) hide show
  1. pyclickerrpa-0.1.0/PKG-INFO +90 -0
  2. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/PKG-INFO +90 -0
  3. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/SOURCES.txt +35 -0
  4. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/dependency_links.txt +1 -0
  5. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/entry_points.txt +5 -0
  6. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/requires.txt +11 -0
  7. pyclickerrpa-0.1.0/PyClickerRPA.egg-info/top_level.txt +1 -0
  8. pyclickerrpa-0.1.0/README.md +66 -0
  9. pyclickerrpa-0.1.0/autoclicker/__init__.py +14 -0
  10. pyclickerrpa-0.1.0/autoclicker/app.py +21 -0
  11. pyclickerrpa-0.1.0/autoclicker/capture.py +94 -0
  12. pyclickerrpa-0.1.0/autoclicker/clicker.py +212 -0
  13. pyclickerrpa-0.1.0/autoclicker/dpi.py +30 -0
  14. pyclickerrpa-0.1.0/autoclicker/gui/__init__.py +1 -0
  15. pyclickerrpa-0.1.0/autoclicker/gui/flow_connection.py +91 -0
  16. pyclickerrpa-0.1.0/autoclicker/gui/flow_editor.py +340 -0
  17. pyclickerrpa-0.1.0/autoclicker/gui/flow_node.py +107 -0
  18. pyclickerrpa-0.1.0/autoclicker/gui/hotkeys.py +40 -0
  19. pyclickerrpa-0.1.0/autoclicker/gui/main_window.py +651 -0
  20. pyclickerrpa-0.1.0/autoclicker/gui/monitor.py +84 -0
  21. pyclickerrpa-0.1.0/autoclicker/gui/preview_label.py +176 -0
  22. pyclickerrpa-0.1.0/autoclicker/gui/qt_runner.py +71 -0
  23. pyclickerrpa-0.1.0/autoclicker/gui/recorder.py +163 -0
  24. pyclickerrpa-0.1.0/autoclicker/gui/scheduler_widget.py +160 -0
  25. pyclickerrpa-0.1.0/autoclicker/gui/snippet_library.py +117 -0
  26. pyclickerrpa-0.1.0/autoclicker/gui/task_widget.py +547 -0
  27. pyclickerrpa-0.1.0/autoclicker/matcher.py +152 -0
  28. pyclickerrpa-0.1.0/autoclicker/models.py +468 -0
  29. pyclickerrpa-0.1.0/autoclicker/ocr.py +63 -0
  30. pyclickerrpa-0.1.0/autoclicker/run.py +64 -0
  31. pyclickerrpa-0.1.0/autoclicker/runner.py +666 -0
  32. pyclickerrpa-0.1.0/autoclicker/windows.py +53 -0
  33. pyclickerrpa-0.1.0/pyproject.toml +42 -0
  34. pyclickerrpa-0.1.0/setup.cfg +4 -0
  35. pyclickerrpa-0.1.0/tests/test_matcher.py +64 -0
  36. pyclickerrpa-0.1.0/tests/test_models.py +88 -0
  37. 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,5 @@
1
+ [console_scripts]
2
+ autoclicker-run = autoclicker.run:main
3
+
4
+ [gui_scripts]
5
+ autoclicker = autoclicker.app:main
@@ -0,0 +1,11 @@
1
+ numpy>=1.23
2
+ opencv-python>=4.8
3
+ pynput>=1.7
4
+ PySide6>=6.5
5
+
6
+ [:sys_platform == "win32"]
7
+ pywin32>=305
8
+
9
+ [ocr]
10
+ paddleocr>=2.7
11
+ paddlepaddle>=2.5
@@ -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+)."""