PyClickerRPA 0.3.1__tar.gz → 0.4.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.3.1 → pyclickerrpa-0.4.0}/PKG-INFO +25 -4
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/PKG-INFO +25 -4
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/SOURCES.txt +3 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/README.md +23 -2
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/main_window.py +20 -4
- pyclickerrpa-0.4.0/autoclicker/gui/recorder.py +290 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/task_widget.py +13 -4
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/models.py +3 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/runner.py +38 -0
- pyclickerrpa-0.4.0/autoclicker/templatize.py +119 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/pyproject.toml +2 -2
- pyclickerrpa-0.4.0/tests/test_recorder_gui.py +321 -0
- pyclickerrpa-0.4.0/tests/test_templatize.py +133 -0
- pyclickerrpa-0.3.1/autoclicker/gui/recorder.py +0 -163
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/LICENSE +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/dependency_links.txt +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/entry_points.txt +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/requires.txt +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/PyClickerRPA.egg-info/top_level.txt +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/__init__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/adb.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/app.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/capture.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/clicker.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/dpi.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/expr.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/__init__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_connection.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_editor.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/flow_node.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/hotkeys.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/monitor.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/preview_label.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/qt_runner.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/scheduler_widget.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/snippet_library.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/gui/testkit_widget.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/matcher.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/ocr.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/run.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/__init__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/__main__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/assertions.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/case.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/loader.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/pytest_plugin.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/report.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/runner.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/testkit/visual.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/__init__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/__main__.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/run_manager.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/scheduler.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/serialize.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/server.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/static/app.js +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/static/index.html +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/web/static/style.css +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/autoclicker/windows.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/setup.cfg +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_adb.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_flow_branch.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_matcher.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_models.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_runner.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_testkit.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_testkit_gui.py +0 -0
- {pyclickerrpa-0.3.1 → pyclickerrpa-0.4.0}/tests/test_web.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
@@ -82,7 +82,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
82
82
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
83
83
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
85
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
87
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
88
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -93,12 +93,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
93
93
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
94
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
95
|
interoperable **step list** with a full
|
|
96
|
-
per-step dialog;
|
|
96
|
+
per-step dialog; snippet library, task
|
|
97
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
98
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
99
99
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
100
100
|
run visual test suites.
|
|
101
101
|
|
|
102
|
+
### Smart recorder
|
|
103
|
+
|
|
104
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
105
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
106
|
+
like* instead of where they *were*:
|
|
107
|
+
|
|
108
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
109
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
110
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
111
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
112
|
+
content hash.
|
|
113
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
114
|
+
the *displacement* is stored, so start and end move together.
|
|
115
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
116
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
117
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
118
|
+
would fail the run).
|
|
119
|
+
|
|
120
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
121
|
+
step and says why in the log.
|
|
122
|
+
|
|
102
123
|
## Visual test framework (`autoclicker.testkit`)
|
|
103
124
|
|
|
104
125
|
Turn automation into tests:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: PyClickerRPA
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, recorder, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Visual UI automation & testing tool: Windows + Android(ADB) automation, robust/self-healing image matching, OCR, node flow editor, a smart recorder that records images instead of coordinates, a visual test framework (assertions, visual regression, HTML/JUnit reports) and a web console.
|
|
5
5
|
Author: Equinox
|
|
6
6
|
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/Equinox/PyClickerRPA
|
|
@@ -82,7 +82,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
82
82
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
83
83
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
84
84
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
85
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
85
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
86
86
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
87
87
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
88
88
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -93,12 +93,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
93
93
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
94
94
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
95
95
|
interoperable **step list** with a full
|
|
96
|
-
per-step dialog;
|
|
96
|
+
per-step dialog; snippet library, task
|
|
97
97
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
98
98
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
99
99
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
100
100
|
run visual test suites.
|
|
101
101
|
|
|
102
|
+
### Smart recorder
|
|
103
|
+
|
|
104
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
105
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
106
|
+
like* instead of where they *were*:
|
|
107
|
+
|
|
108
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
109
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
110
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
111
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
112
|
+
content hash.
|
|
113
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
114
|
+
the *displacement* is stored, so start and end move together.
|
|
115
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
116
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
117
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
118
|
+
would fail the run).
|
|
119
|
+
|
|
120
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
121
|
+
step and says why in the log.
|
|
122
|
+
|
|
102
123
|
## Visual test framework (`autoclicker.testkit`)
|
|
103
124
|
|
|
104
125
|
Turn automation into tests:
|
|
@@ -19,6 +19,7 @@ autoclicker/models.py
|
|
|
19
19
|
autoclicker/ocr.py
|
|
20
20
|
autoclicker/run.py
|
|
21
21
|
autoclicker/runner.py
|
|
22
|
+
autoclicker/templatize.py
|
|
22
23
|
autoclicker/windows.py
|
|
23
24
|
autoclicker/gui/__init__.py
|
|
24
25
|
autoclicker/gui/flow_connection.py
|
|
@@ -56,7 +57,9 @@ tests/test_adb.py
|
|
|
56
57
|
tests/test_flow_branch.py
|
|
57
58
|
tests/test_matcher.py
|
|
58
59
|
tests/test_models.py
|
|
60
|
+
tests/test_recorder_gui.py
|
|
59
61
|
tests/test_runner.py
|
|
62
|
+
tests/test_templatize.py
|
|
60
63
|
tests/test_testkit.py
|
|
61
64
|
tests/test_testkit_gui.py
|
|
62
65
|
tests/test_web.py
|
|
@@ -44,7 +44,7 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
44
44
|
- **Smart wait**: `wait_stable` waits until the screen (or an ROI) stops
|
|
45
45
|
changing — replaces guessed `sleep`s (fast when ready, patient when slow).
|
|
46
46
|
- **~30+ step types**: click image/text/position, click-all, relative/offset
|
|
47
|
-
clicks, wait / random / wait-stable, drag / long-press / pinch / scroll,
|
|
47
|
+
clicks, wait / random / wait-stable, drag / **drag-from-image** / long-press / pinch / scroll,
|
|
48
48
|
key / type / hotkey, loop / goto / while / do-until, if image/text/color/
|
|
49
49
|
count/var with **true/false branching** (if-else), set-var, read-text→variable,
|
|
50
50
|
activate-window, HTTP request, run-Python-script, call sub-task, groups, and a
|
|
@@ -55,12 +55,33 @@ autoclicker-web --suites-dir examples # web console at http://127.0.0.1
|
|
|
55
55
|
Drag-and-drop **node flow graph** (condition nodes have **true/false branch
|
|
56
56
|
ports** for visual if-else; a terminal **stop** node ends the flow) and an
|
|
57
57
|
interoperable **step list** with a full
|
|
58
|
-
per-step dialog;
|
|
58
|
+
per-step dialog; snippet library, task
|
|
59
59
|
packages (`.zip` with templates), on-preview coordinate/color picking, template
|
|
60
60
|
crop, single-step test run; global hotkeys (F6 run/stop, ESC stop), scheduler,
|
|
61
61
|
and an image/color **monitor/trigger** with tray icon; a **Test** tab to load and
|
|
62
62
|
run visual test suites.
|
|
63
63
|
|
|
64
|
+
### Smart recorder
|
|
65
|
+
|
|
66
|
+
The action **recorder** (global hooks) doesn't just replay coordinates and
|
|
67
|
+
sleeps — with **智能录制 / smart mode** on (default) it records what things *look
|
|
68
|
+
like* instead of where they *were*:
|
|
69
|
+
|
|
70
|
+
- **Clicks become image steps.** A patch is cropped around the click and grown
|
|
71
|
+
(48 → 192 px) until it is both textured enough and **unique on screen**, then
|
|
72
|
+
emitted as a `click_image` step — so the flow still works after the window
|
|
73
|
+
moves or the resolution changes. Identical patches are de-duplicated by
|
|
74
|
+
content hash.
|
|
75
|
+
- **Drags become `drag_image`.** The drag's *start* is anchored to an image and
|
|
76
|
+
the *displacement* is stored, so start and end move together.
|
|
77
|
+
- **Pauses become `wait_stable`.** A long pause is only converted if the
|
|
78
|
+
recorder actually **observed the screen settle** during it; on a permanently
|
|
79
|
+
animating screen it keeps the fixed wait (a `wait_stable` that never settles
|
|
80
|
+
would fail the run).
|
|
81
|
+
|
|
82
|
+
Anything that can't be made robust falls back to the plain coordinate/fixed-wait
|
|
83
|
+
step and says why in the log.
|
|
84
|
+
|
|
64
85
|
## Visual test framework (`autoclicker.testkit`)
|
|
65
86
|
|
|
66
87
|
Turn automation into tests:
|
|
@@ -12,10 +12,10 @@ import tempfile
|
|
|
12
12
|
import uuid
|
|
13
13
|
import zipfile
|
|
14
14
|
|
|
15
|
-
from PySide6.QtCore import Qt, QThread, Signal
|
|
15
|
+
from PySide6.QtCore import Qt, QSettings, QThread, Signal
|
|
16
16
|
from PySide6.QtGui import QAction, QGuiApplication, QKeySequence
|
|
17
17
|
from PySide6.QtWidgets import (
|
|
18
|
-
QComboBox, QDockWidget, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
|
|
18
|
+
QCheckBox, QComboBox, QDockWidget, QFileDialog, QHBoxLayout, QInputDialog, QLabel,
|
|
19
19
|
QMainWindow, QMessageBox, QPushButton, QSplitter, QTableWidget,
|
|
20
20
|
QTableWidgetItem, QTabWidget, QTextEdit, QVBoxLayout, QWidget,
|
|
21
21
|
)
|
|
@@ -69,8 +69,11 @@ class MainWindow(QMainWindow):
|
|
|
69
69
|
self._runner.runFinished.connect(self._on_finished)
|
|
70
70
|
self._runner.pausedAt.connect(self._on_paused)
|
|
71
71
|
|
|
72
|
+
self._settings = QSettings("AutoClicker", "AutoClicker")
|
|
73
|
+
|
|
72
74
|
self._recorder = Recorder(self)
|
|
73
75
|
self._recorder.stepRecorded.connect(self._on_step_recorded)
|
|
76
|
+
self._recorder.note.connect(self._append_log)
|
|
74
77
|
|
|
75
78
|
self._build_ui()
|
|
76
79
|
self._build_menu()
|
|
@@ -229,6 +232,16 @@ class MainWindow(QMainWindow):
|
|
|
229
232
|
capture_btn.clicked.connect(self.capture)
|
|
230
233
|
self.record_btn = QPushButton("● 录制")
|
|
231
234
|
self.record_btn.clicked.connect(self.toggle_record)
|
|
235
|
+
self.template_mode_chk = QCheckBox("智能录制")
|
|
236
|
+
self.template_mode_chk.setToolTip(
|
|
237
|
+
"录出来的任务尽量不依赖绝对坐标和绝对时间:\n"
|
|
238
|
+
"· 点击处裁一小块图当模板 → 位置无关的「找图点击」\n"
|
|
239
|
+
"· 长间隔改成「等画面稳定」→ 换台机器也不会等不够\n"
|
|
240
|
+
"抽不出好模板、或录制时画面一直在动,则各自退回坐标点击/固定等待。")
|
|
241
|
+
self.template_mode_chk.setChecked(
|
|
242
|
+
self._settings.value("recorder/templateMode", True, type=bool))
|
|
243
|
+
self.template_mode_chk.toggled.connect(
|
|
244
|
+
lambda on: self._settings.setValue("recorder/templateMode", on))
|
|
232
245
|
self.run_btn = QPushButton("▶ 运行")
|
|
233
246
|
self.run_btn.clicked.connect(self.run_task)
|
|
234
247
|
self.pause_btn = QPushButton("⏸ 暂停")
|
|
@@ -250,6 +263,7 @@ class MainWindow(QMainWindow):
|
|
|
250
263
|
bar.addWidget(refresh_btn)
|
|
251
264
|
bar.addWidget(capture_btn)
|
|
252
265
|
bar.addStretch()
|
|
266
|
+
bar.addWidget(self.template_mode_chk)
|
|
253
267
|
bar.addWidget(self.record_btn)
|
|
254
268
|
bar.addWidget(self.run_btn)
|
|
255
269
|
bar.addWidget(self.pause_btn)
|
|
@@ -513,10 +527,12 @@ class MainWindow(QMainWindow):
|
|
|
513
527
|
self._append_log("录制结束")
|
|
514
528
|
return
|
|
515
529
|
hwnd = self._target_hwnd()
|
|
516
|
-
self.
|
|
530
|
+
tmpl = self.template_mode_chk.isChecked()
|
|
531
|
+
self._recorder.start(hwnd, template_mode=tmpl)
|
|
517
532
|
self.record_btn.setText("■ 停止录制")
|
|
518
533
|
self.tabs.setCurrentWidget(self.task_widget)
|
|
519
|
-
self._append_log("开始录制(在目标窗口内操作;再次点击结束)"
|
|
534
|
+
self._append_log("开始录制(在目标窗口内操作;再次点击结束)"
|
|
535
|
+
+ ("[智能录制]" if tmpl else ""))
|
|
520
536
|
|
|
521
537
|
def _on_step_recorded(self, step: Step):
|
|
522
538
|
self.task_widget.append_step(step)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Action recorder — port of recorder.cpp using pynput global hooks.
|
|
2
|
+
|
|
3
|
+
Records left clicks / drags (client-relative to the target window) and key
|
|
4
|
+
presses into Steps, inserting Wait steps for gaps >= 150ms. Emits stepRecorded
|
|
5
|
+
from the listener thread; the signal is delivered to the GUI thread by Qt.
|
|
6
|
+
|
|
7
|
+
In *template mode* a click additionally gets a small image patch cropped around
|
|
8
|
+
it and becomes a position-independent ClickImage step (see `templatize`); when no
|
|
9
|
+
usable template can be cut it falls back to the plain ClickPosition step.
|
|
10
|
+
"""
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import math
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import QObject, QTimer, Signal
|
|
17
|
+
|
|
18
|
+
from autoclicker.models import Step, StepType
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
import win32gui
|
|
22
|
+
_HAS_WIN32 = True
|
|
23
|
+
except ImportError:
|
|
24
|
+
_HAS_WIN32 = False
|
|
25
|
+
|
|
26
|
+
from pynput import mouse, keyboard
|
|
27
|
+
|
|
28
|
+
# pynput special key -> our key name (mirrors Recorder::vkToName specials)
|
|
29
|
+
_SPECIAL_KEYS = {
|
|
30
|
+
keyboard.Key.enter: "ENTER", keyboard.Key.esc: "ESC", keyboard.Key.tab: "TAB",
|
|
31
|
+
keyboard.Key.space: "SPACE", keyboard.Key.backspace: "BACKSPACE",
|
|
32
|
+
keyboard.Key.delete: "DELETE", keyboard.Key.up: "UP", keyboard.Key.down: "DOWN",
|
|
33
|
+
keyboard.Key.left: "LEFT", keyboard.Key.right: "RIGHT", keyboard.Key.home: "HOME",
|
|
34
|
+
keyboard.Key.end: "END", keyboard.Key.page_up: "PAGEUP", keyboard.Key.page_down: "PAGEDOWN",
|
|
35
|
+
}
|
|
36
|
+
_MODIFIERS = {
|
|
37
|
+
keyboard.Key.ctrl, keyboard.Key.ctrl_l, keyboard.Key.ctrl_r,
|
|
38
|
+
keyboard.Key.shift, keyboard.Key.shift_l, keyboard.Key.shift_r,
|
|
39
|
+
keyboard.Key.alt, keyboard.Key.alt_l, keyboard.Key.alt_r, keyboard.Key.alt_gr,
|
|
40
|
+
keyboard.Key.cmd, keyboard.Key.cmd_l, keyboard.Key.cmd_r,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class Recorder(QObject):
|
|
45
|
+
stepRecorded = Signal(object) # emits a Step
|
|
46
|
+
recordingStopped = Signal()
|
|
47
|
+
note = Signal(str) # 图像模式的取模板结果/回退原因,接到主窗口日志
|
|
48
|
+
|
|
49
|
+
# 抓帧间隔。只是为了在点击时手里有一张"刚刚"的画面,不需要很快
|
|
50
|
+
GRAB_INTERVAL_MS = 120
|
|
51
|
+
|
|
52
|
+
# 智能等待:只有间隔够长(人是真在等界面)、且录制时**观察到画面确实静下来过**,
|
|
53
|
+
# 才把固定 Wait 换成 WaitStable。有持续动画的画面永远稳不下来,
|
|
54
|
+
# 那种地方换成 WaitStable 会超时 Fail,比固定等待更糟。
|
|
55
|
+
STABLE_TOL = 0.005 # 和 runner._exec_wait_stable 的默认容差一致
|
|
56
|
+
STABLE_MIN_GAP_MS = 700 # 短于此的间隔只是手速,不是在等界面
|
|
57
|
+
STABLE_MIN_QUIET_MS = 250 # 至少观察到这么久的静止,才敢认为它能稳
|
|
58
|
+
STABLE_MAX_MS = 500 # 运行时要求的稳定时长上限(= Step.stableMs 默认值)
|
|
59
|
+
|
|
60
|
+
def __init__(self, parent=None):
|
|
61
|
+
super().__init__(parent)
|
|
62
|
+
self._hwnd = None
|
|
63
|
+
self._recording = False
|
|
64
|
+
self._mouse_listener = None
|
|
65
|
+
self._key_listener = None
|
|
66
|
+
self._t0 = 0.0
|
|
67
|
+
self._last_event = None
|
|
68
|
+
self._down = False
|
|
69
|
+
self._down_pos = (0, 0)
|
|
70
|
+
self._down_time = 0.0
|
|
71
|
+
self._template_mode = False
|
|
72
|
+
self._grab = None
|
|
73
|
+
self._frame = None # 最近抓到的一帧
|
|
74
|
+
self._down_frame = None # 按下那一刻的帧(点击前的画面)
|
|
75
|
+
self._quiet_since = None # 画面连续静止的起点(elapsed ms),动了就清空
|
|
76
|
+
self._timer = QTimer(self)
|
|
77
|
+
self._timer.timeout.connect(self._grab_frame)
|
|
78
|
+
|
|
79
|
+
def is_recording(self) -> bool:
|
|
80
|
+
return self._recording
|
|
81
|
+
|
|
82
|
+
def start(self, hwnd, *, template_mode: bool = False, grab=None):
|
|
83
|
+
"""grab 可注入(默认抓目标窗口客户区/全屏),为的是离屏测试能塞假帧。"""
|
|
84
|
+
if self._recording:
|
|
85
|
+
return
|
|
86
|
+
self._hwnd = hwnd
|
|
87
|
+
self._recording = True
|
|
88
|
+
self._template_mode = template_mode
|
|
89
|
+
self._grab = grab or self._default_grab
|
|
90
|
+
self._frame = None
|
|
91
|
+
self._down_frame = None
|
|
92
|
+
self._quiet_since = None
|
|
93
|
+
self._t0 = time.monotonic()
|
|
94
|
+
self._last_event = None
|
|
95
|
+
self._down = False
|
|
96
|
+
self._mouse_listener = mouse.Listener(on_click=self._on_click)
|
|
97
|
+
self._key_listener = keyboard.Listener(on_press=self._on_press)
|
|
98
|
+
self._mouse_listener.start()
|
|
99
|
+
self._key_listener.start()
|
|
100
|
+
if template_mode:
|
|
101
|
+
self._grab_frame() # 先垫一帧,别让第一次点击没图可用
|
|
102
|
+
self._timer.start(self.GRAB_INTERVAL_MS)
|
|
103
|
+
|
|
104
|
+
def stop(self):
|
|
105
|
+
if not self._recording:
|
|
106
|
+
return
|
|
107
|
+
self._recording = False
|
|
108
|
+
self._timer.stop()
|
|
109
|
+
if self._mouse_listener:
|
|
110
|
+
self._mouse_listener.stop()
|
|
111
|
+
self._mouse_listener = None
|
|
112
|
+
if self._key_listener:
|
|
113
|
+
self._key_listener.stop()
|
|
114
|
+
self._key_listener = None
|
|
115
|
+
self._frame = None
|
|
116
|
+
self._down_frame = None
|
|
117
|
+
self.recordingStopped.emit()
|
|
118
|
+
|
|
119
|
+
# ---- 抓帧(跑在 Qt 线程的 QTimer 上) ----
|
|
120
|
+
def _default_grab(self):
|
|
121
|
+
from autoclicker import capture
|
|
122
|
+
return capture.capture_window(self._hwnd) if self._hwnd else capture.capture_screen()
|
|
123
|
+
|
|
124
|
+
def _grab_frame(self):
|
|
125
|
+
"""故意不在 pynput 回调里抓图:回调跑在低级钩子线程上,PrintWindow 一次要几十毫秒,
|
|
126
|
+
阻塞太久 Windows 会直接把钩子摘掉,录制就莫名失灵了。"""
|
|
127
|
+
try:
|
|
128
|
+
frame = self._grab()
|
|
129
|
+
except Exception as e:
|
|
130
|
+
self._frame = None
|
|
131
|
+
self._quiet_since = None
|
|
132
|
+
self.note.emit(f"图像模式抓屏失败: {e}")
|
|
133
|
+
return
|
|
134
|
+
frame = frame if frame is not None and frame.size else None
|
|
135
|
+
prev, self._frame = self._frame, frame
|
|
136
|
+
if frame is None:
|
|
137
|
+
self._quiet_since = None
|
|
138
|
+
return
|
|
139
|
+
# 顺手记录画面有没有静下来 —— 这是后面决定能不能用 WaitStable 的依据
|
|
140
|
+
from autoclicker import matcher
|
|
141
|
+
if prev is None or matcher.diff_ratio(prev, frame) > self.STABLE_TOL:
|
|
142
|
+
self._quiet_since = None
|
|
143
|
+
elif self._quiet_since is None:
|
|
144
|
+
self._quiet_since = self._elapsed_ms()
|
|
145
|
+
|
|
146
|
+
def _click_step(self, cx: int, cy: int) -> Step:
|
|
147
|
+
"""图像模式下尽量产出位置无关的 ClickImage,抽不出好模板就退回坐标点击。"""
|
|
148
|
+
if not self._template_mode:
|
|
149
|
+
return Step(type=StepType.ClickPosition, x=cx, y=cy)
|
|
150
|
+
frame = self._down_frame
|
|
151
|
+
if frame is None:
|
|
152
|
+
self.note.emit(f"({cx},{cy}) 没抓到画面 → 退回坐标点击")
|
|
153
|
+
return Step(type=StepType.ClickPosition, x=cx, y=cy)
|
|
154
|
+
from autoclicker import templatize
|
|
155
|
+
try:
|
|
156
|
+
choice = templatize.pick_template(frame, cx, cy)
|
|
157
|
+
if choice is None:
|
|
158
|
+
self.note.emit(f"({cx},{cy}) 该处不够独特(太平淡或全屏有多处相同) → "
|
|
159
|
+
f"退回坐标点击;可事后用「裁剪模板」手工指定")
|
|
160
|
+
return Step(type=StepType.ClickPosition, x=cx, y=cy)
|
|
161
|
+
path = templatize.save_template(choice.patch)
|
|
162
|
+
step = templatize.build_click_image_step(path, choice.off_x, choice.off_y)
|
|
163
|
+
except Exception as e:
|
|
164
|
+
self.note.emit(f"({cx},{cy}) 取模板失败({e}) → 退回坐标点击")
|
|
165
|
+
return Step(type=StepType.ClickPosition, x=cx, y=cy)
|
|
166
|
+
self.note.emit(f"({cx},{cy}) 已取模板 {choice.size}px 纹理{choice.score:.0f} → {path}")
|
|
167
|
+
return step
|
|
168
|
+
|
|
169
|
+
def _drag_step(self, cx: int, cy: int, duration_ms: int) -> Step:
|
|
170
|
+
"""智能录制下把拖拽的**起点**图像化(DragImage 存的是位移,所以终点跟着起点走);
|
|
171
|
+
起点抽不出模板就退回绝对坐标的 Drag。"""
|
|
172
|
+
sx, sy = self._down_pos
|
|
173
|
+
plain = Step(type=StepType.Drag, x=sx, y=sy, x2=cx, y2=cy, durationMs=duration_ms)
|
|
174
|
+
if not self._template_mode or self._down_frame is None:
|
|
175
|
+
return plain
|
|
176
|
+
from autoclicker import templatize
|
|
177
|
+
try:
|
|
178
|
+
choice = templatize.pick_template(self._down_frame, sx, sy)
|
|
179
|
+
if choice is None:
|
|
180
|
+
self.note.emit(f"拖拽起点({sx},{sy})不够独特 → 退回坐标拖拽")
|
|
181
|
+
return plain
|
|
182
|
+
path = templatize.save_template(choice.patch)
|
|
183
|
+
except Exception as e:
|
|
184
|
+
self.note.emit(f"拖拽起点取模板失败({e}) → 退回坐标拖拽")
|
|
185
|
+
return plain
|
|
186
|
+
self.note.emit(f"拖拽起点({sx},{sy}) 已取模板 {choice.size}px → 位移({cx - sx},{cy - sy})")
|
|
187
|
+
return Step(type=StepType.DragImage, imagePath=path,
|
|
188
|
+
x=choice.off_x, y=choice.off_y,
|
|
189
|
+
x2=cx - sx, y2=cy - sy, durationMs=duration_ms)
|
|
190
|
+
|
|
191
|
+
# ---- helpers ----
|
|
192
|
+
def _elapsed_ms(self) -> float:
|
|
193
|
+
return (time.monotonic() - self._t0) * 1000.0
|
|
194
|
+
|
|
195
|
+
def _inside(self, x, y) -> bool:
|
|
196
|
+
if not _HAS_WIN32 or not self._hwnd:
|
|
197
|
+
return True
|
|
198
|
+
try:
|
|
199
|
+
l, t, r, b = win32gui.GetWindowRect(self._hwnd)
|
|
200
|
+
except Exception:
|
|
201
|
+
return False
|
|
202
|
+
return l <= x <= r and t <= y <= b
|
|
203
|
+
|
|
204
|
+
def _to_client(self, x, y):
|
|
205
|
+
if _HAS_WIN32 and self._hwnd:
|
|
206
|
+
try:
|
|
207
|
+
return win32gui.ScreenToClient(self._hwnd, (x, y))
|
|
208
|
+
except Exception:
|
|
209
|
+
return x, y
|
|
210
|
+
return x, y
|
|
211
|
+
|
|
212
|
+
def _emit_wait_if_needed(self):
|
|
213
|
+
now = self._elapsed_ms()
|
|
214
|
+
# 用 None 而不是 0 表示"还没有上一个事件":Windows 的 monotonic 精度约 15.6ms,
|
|
215
|
+
# 第一次操作和 start() 落在同一刻度时 elapsed 恰好是 0.0,用 >0 判断会把
|
|
216
|
+
# 第一个间隔静默丢掉。
|
|
217
|
+
if self._last_event is not None:
|
|
218
|
+
gap = int(now - self._last_event)
|
|
219
|
+
if gap >= 150:
|
|
220
|
+
self.stepRecorded.emit(self._wait_step(gap, now))
|
|
221
|
+
self._last_event = now
|
|
222
|
+
|
|
223
|
+
def _wait_step(self, gap: int, now: float) -> Step:
|
|
224
|
+
"""长间隔 + 录制时确实看到画面静下来过 → 等画面稳定(换台机器也不会等不够);
|
|
225
|
+
否则照旧记录固定间隔。"""
|
|
226
|
+
quiet = self._quiet_since
|
|
227
|
+
if not self._template_mode or gap < self.STABLE_MIN_GAP_MS or quiet is None:
|
|
228
|
+
return Step(type=StepType.Wait, waitMs=gap)
|
|
229
|
+
quiet_ms = int(now - quiet)
|
|
230
|
+
if quiet_ms < self.STABLE_MIN_QUIET_MS:
|
|
231
|
+
return Step(type=StepType.Wait, waitMs=gap)
|
|
232
|
+
# 运行时要求的稳定时长不超过我们实际观察到的,免得它永远等不到
|
|
233
|
+
stable_ms = max(200, min(self.STABLE_MAX_MS, quiet_ms))
|
|
234
|
+
timeout_ms = min(15000, max(3000, gap * 3))
|
|
235
|
+
self.note.emit(f"间隔 {gap}ms 且画面静止过 {quiet_ms}ms → 改为等画面稳定"
|
|
236
|
+
f"(稳定 {stable_ms}ms / 超时 {timeout_ms}ms)")
|
|
237
|
+
return Step(type=StepType.WaitStable, stableMs=stable_ms, timeoutMs=timeout_ms)
|
|
238
|
+
|
|
239
|
+
# ---- pynput callbacks (run on listener thread) ----
|
|
240
|
+
def _on_click(self, x, y, button, pressed):
|
|
241
|
+
if not self._recording or button != mouse.Button.left:
|
|
242
|
+
return
|
|
243
|
+
if not self._inside(x, y):
|
|
244
|
+
return
|
|
245
|
+
cx, cy = self._to_client(x, y)
|
|
246
|
+
if pressed:
|
|
247
|
+
self._down = True
|
|
248
|
+
self._down_pos = (cx, cy)
|
|
249
|
+
self._down_time = self._elapsed_ms()
|
|
250
|
+
# 按下这一刻的画面才是"点击前"的样子;用释放时的帧会抓到已经按下去
|
|
251
|
+
# 或者菜单已经弹出来的状态,裁出来的模板就没用了
|
|
252
|
+
self._down_frame = self._frame
|
|
253
|
+
elif self._down:
|
|
254
|
+
self._down = False
|
|
255
|
+
dx, dy = cx - self._down_pos[0], cy - self._down_pos[1]
|
|
256
|
+
dist = math.hypot(dx, dy)
|
|
257
|
+
self._emit_wait_if_needed()
|
|
258
|
+
if dist > 10:
|
|
259
|
+
dur = int(self._elapsed_ms() - self._down_time)
|
|
260
|
+
self.stepRecorded.emit(self._drag_step(cx, cy, dur if dur >= 50 else 200))
|
|
261
|
+
else:
|
|
262
|
+
self.stepRecorded.emit(self._click_step(cx, cy))
|
|
263
|
+
|
|
264
|
+
def _on_press(self, key):
|
|
265
|
+
if not self._recording or key in _MODIFIERS:
|
|
266
|
+
return
|
|
267
|
+
if _HAS_WIN32 and self._hwnd:
|
|
268
|
+
try:
|
|
269
|
+
if win32gui.GetForegroundWindow() != self._hwnd:
|
|
270
|
+
return
|
|
271
|
+
except Exception:
|
|
272
|
+
pass
|
|
273
|
+
name = self._key_name(key)
|
|
274
|
+
if name:
|
|
275
|
+
self._emit_wait_if_needed()
|
|
276
|
+
self.stepRecorded.emit(Step(type=StepType.KeyPress, keyName=name))
|
|
277
|
+
|
|
278
|
+
@staticmethod
|
|
279
|
+
def _key_name(key) -> str:
|
|
280
|
+
if key in _SPECIAL_KEYS:
|
|
281
|
+
return _SPECIAL_KEYS[key]
|
|
282
|
+
if isinstance(key, keyboard.Key):
|
|
283
|
+
k = str(key).replace("Key.", "")
|
|
284
|
+
if k.startswith("f") and k[1:].isdigit():
|
|
285
|
+
return k.upper()
|
|
286
|
+
return ""
|
|
287
|
+
ch = getattr(key, "char", None)
|
|
288
|
+
if ch and (ch.isalnum()):
|
|
289
|
+
return ch.upper()
|
|
290
|
+
return ""
|
|
@@ -50,6 +50,7 @@ TYPE_LABELS = [
|
|
|
50
50
|
(StepType.WaitImage, "等待图片出现"),
|
|
51
51
|
(StepType.WaitImageGone, "等待图片消失"),
|
|
52
52
|
(StepType.MergeImage, "合成拖拽"),
|
|
53
|
+
(StepType.DragImage, "找图拖拽"),
|
|
53
54
|
(StepType.ClickAll, "点击所有匹配"),
|
|
54
55
|
(StepType.ClickRelative, "相对点击(last+偏移)"),
|
|
55
56
|
(StepType.ReadText, "读取文字→变量"),
|
|
@@ -65,13 +66,13 @@ TYPE_LABELS = [
|
|
|
65
66
|
]
|
|
66
67
|
|
|
67
68
|
_IMAGE_TYPES = {StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone,
|
|
68
|
-
StepType.MergeImage, StepType.
|
|
69
|
-
StepType.IfImageCount}
|
|
69
|
+
StepType.MergeImage, StepType.DragImage, StepType.ClickAll,
|
|
70
|
+
StepType.IfImageFound, StepType.IfImageCount}
|
|
70
71
|
_IF_TYPES = {StepType.IfImageFound, StepType.IfTextFound, StepType.IfElse,
|
|
71
72
|
StepType.IfColorMatch, StepType.IfImageCount, StepType.IfVar}
|
|
72
73
|
_ACTION_TYPES = {StepType.ClickImage, StepType.ClickText, StepType.ClickPosition,
|
|
73
74
|
StepType.CallSub, StepType.WaitImage, StepType.WaitImageGone,
|
|
74
|
-
StepType.MergeImage, StepType.ClickAll}
|
|
75
|
+
StepType.MergeImage, StepType.DragImage, StepType.ClickAll}
|
|
75
76
|
_UNTIL_TYPES = {StepType.DoUntil, StepType.WhileCondition}
|
|
76
77
|
|
|
77
78
|
# 变量比较运算符 (显示标签, 存储 token)。前 6 个 label==token 兼容旧任务。
|
|
@@ -174,8 +175,16 @@ class StepEditorDialog(QDialog):
|
|
|
174
175
|
mm.setCurrentIndex({"robust": 0, "template": 1, "feature": 2}.get(s.matchMode, 0))
|
|
175
176
|
self.w["matchMode"] = mm
|
|
176
177
|
self.form.addRow("匹配方式", mm)
|
|
177
|
-
if t in (StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone,
|
|
178
|
+
if t in (StepType.ClickImage, StepType.WaitImage, StepType.WaitImageGone,
|
|
179
|
+
StepType.MergeImage, StepType.DragImage):
|
|
178
180
|
self.form.addRow("超时(ms)", self._spin("timeoutMs", s.timeoutMs, 0, 3600000))
|
|
181
|
+
if t == StepType.DragImage:
|
|
182
|
+
# 存的是相对命中点的位移,不是终点绝对坐标 —— 窗口移位时终点会跟着走
|
|
183
|
+
self.form.addRow("起点微调 X", self._spin("x", s.x))
|
|
184
|
+
self.form.addRow("起点微调 Y", self._spin("y", s.y))
|
|
185
|
+
self.form.addRow("拖拽位移 X2", self._spin("x2", s.x2))
|
|
186
|
+
self.form.addRow("拖拽位移 Y2", self._spin("y2", s.y2))
|
|
187
|
+
self.form.addRow("拖拽时长(ms)", self._spin("durationMs", s.durationMs, 0, 60000))
|
|
179
188
|
if t == StepType.IfImageCount:
|
|
180
189
|
self.form.addRow("最少匹配数", self._spin("minMatchCount", s.minMatchCount, 1, 100))
|
|
181
190
|
if t == StepType.ClickAll:
|
|
@@ -51,6 +51,7 @@ class StepType(str, Enum):
|
|
|
51
51
|
ClickRelative = "click_relative"
|
|
52
52
|
HttpRequest = "http_request"
|
|
53
53
|
RunScript = "run_script"
|
|
54
|
+
DragImage = "drag_image" # 找到起点模板后按 x2/y2 位移拖拽(位置无关的拖拽)
|
|
54
55
|
WaitStable = "wait_stable" # 等画面(或 ROI)稳定后继续
|
|
55
56
|
ActivateWindow = "activate_window" # 刷新/激活目标窗口(点击会切前台窗口时用)
|
|
56
57
|
Stop = "stop" # 结束/停止任务(终止循环)
|
|
@@ -433,6 +434,8 @@ class Step:
|
|
|
433
434
|
elif t == StepType.ReadText:
|
|
434
435
|
mode = "数字" if self.varValue == "number" else "文字"
|
|
435
436
|
base = f"读取{mode}→变量: {self.varName}"
|
|
437
|
+
elif t == StepType.DragImage:
|
|
438
|
+
base = f"找图拖拽: {fname(self.imagePath)} →偏移({self.x2},{self.y2})"
|
|
436
439
|
elif t == StepType.ClickRelative:
|
|
437
440
|
base = f"相对点击: last+({self.x},{self.y})"
|
|
438
441
|
elif t == StepType.HttpRequest:
|
|
@@ -504,6 +504,8 @@ class TaskRunner(threading.Thread):
|
|
|
504
504
|
self.cb.log(f" 相对点击 last+({step.x},{step.y}) = ({cx}, {cy})")
|
|
505
505
|
self.clicker.click(self.target_hwnd, cx, cy)
|
|
506
506
|
return StepResult.Success
|
|
507
|
+
if t == StepType.DragImage:
|
|
508
|
+
return self._exec_drag_image(step)
|
|
507
509
|
if t == StepType.WaitStable:
|
|
508
510
|
return self._exec_wait_stable(step)
|
|
509
511
|
if t == StepType.ActivateWindow:
|
|
@@ -729,6 +731,42 @@ class TaskRunner(threading.Thread):
|
|
|
729
731
|
self._sleep_ms(200) # 给前台切换一点时间
|
|
730
732
|
return StepResult.Success
|
|
731
733
|
|
|
734
|
+
def _exec_drag_image(self, step: Step) -> StepResult:
|
|
735
|
+
"""找到起点模板,再从命中点按 x2/y2 的**位移**拖拽。
|
|
736
|
+
|
|
737
|
+
存位移而不是终点绝对坐标,这样窗口整体移位时起点和终点一起跟着走。
|
|
738
|
+
"""
|
|
739
|
+
templates = self._template_list(step)
|
|
740
|
+
if not templates:
|
|
741
|
+
self.cb.log(" 错误: 未指定图片路径")
|
|
742
|
+
return StepResult.Fail
|
|
743
|
+
templs = [(img, mask) for (img, mask) in
|
|
744
|
+
(self._load_template(p, step.display_name()) for p in templates) if img is not None]
|
|
745
|
+
if not templs:
|
|
746
|
+
return StepResult.Fail
|
|
747
|
+
|
|
748
|
+
start = _now_ms()
|
|
749
|
+
while self._running and _now_ms() - start < step.timeoutMs:
|
|
750
|
+
screen = self._screenshot()
|
|
751
|
+
for img, mask in templs:
|
|
752
|
+
results = self.matcher.find_image_roi(screen, img, step.threshold, step.roiRect,
|
|
753
|
+
step.multiScale, mask=mask, mode=step.matchMode)
|
|
754
|
+
if not results:
|
|
755
|
+
continue
|
|
756
|
+
best = results[0]
|
|
757
|
+
sx = self._rand_offset(best.cx + step.x, step.randomOffset)
|
|
758
|
+
sy = self._rand_offset(best.cy + step.y, step.randomOffset)
|
|
759
|
+
ex, ey = sx + step.x2, sy + step.y2
|
|
760
|
+
self.cb.match_found(sx, sy, best.confidence)
|
|
761
|
+
self.cb.log(f" 找图拖拽 ({sx},{sy}) → ({ex},{ey}) 置信度: {best.confidence:.3f}")
|
|
762
|
+
self.clicker.drag(self.target_hwnd, sx, sy, ex, ey, step.durationMs)
|
|
763
|
+
self._vars["last_x"] = str(ex)
|
|
764
|
+
self._vars["last_y"] = str(ey)
|
|
765
|
+
return StepResult.Success
|
|
766
|
+
self._sleep_ms(500)
|
|
767
|
+
self.cb.log(" 超时: 未找到拖拽起点图片")
|
|
768
|
+
return StepResult.Fail
|
|
769
|
+
|
|
732
770
|
def _exec_wait_stable(self, step: Step, tol: float = 0.005, interval_ms: int = 150) -> StepResult:
|
|
733
771
|
"""轮询截图,直到监视区域连续稳定 step.stableMs(相邻帧变化比例 <= tol)。
|
|
734
772
|
超时(step.timeoutMs)仍未稳定则 Fail。"""
|