Skip to content

Commit 5477177

Browse files
committed
Expose custom overlay painting to tasks
1 parent d44cf6b commit 5477177

5 files changed

Lines changed: 165 additions & 5 deletions

File tree

docs/api_doc/README.md

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -777,9 +777,45 @@ def clear_box(self)
777777
def get_overlay_view(self)
778778
```
779779

780-
返回覆盖在捕获窗口上的 Qt overlay widget。`BaseTask``CustomTab` 可直接调用此方法;配置的
781-
`my_app` 实例也会获得同名方法。无界面运行时返回 `None`。自定义内容绘制完成后可调用
782-
`overlay_view.request_show()` 令 overlay 在窗口上显示。
780+
返回覆盖在捕获窗口上的原始 Qt overlay widget。`BaseTask``CustomTab` 可直接调用此方法;
781+
配置的 `my_app` 实例也会获得同名方法。无界面运行时返回 `None`
782+
783+
任务线程需要通过 `overlay_view.draw(key, callback, duration=None)` 注册自定义绘制。回调会在
784+
Qt 绘制线程中执行,参数为 `(painter, overlay_view)`,可使用 `QPainter` 绘制任意内容。存在
785+
自定义绘制内容时 overlay 会自动显示,不受 `Enable Boxes` 开关影响。`duration` 为秒数;不传
786+
时持续显示,直到调用 `overlay_view.clear_draw(key)``overlay_view.clear_draw()`
787+
788+
```python
789+
from PySide6.QtGui import QColor, QFont, QPen
790+
from ok import TriggerTask
791+
792+
793+
class StatusOverlayTask(TriggerTask):
794+
def __init__(self, *args, **kwargs):
795+
super().__init__(*args, **kwargs)
796+
self.default_config = {'_enabled': True}
797+
self.trigger_interval = 0.5
798+
799+
def run(self):
800+
overlay = self.get_overlay_view()
801+
if overlay is None:
802+
return
803+
804+
status = "Tracking"
805+
806+
def paint(painter, view):
807+
painter.setPen(QPen(QColor(0, 255, 120), 2))
808+
painter.drawRect(30, 30, 240, 64)
809+
painter.setFont(QFont("Arial", 16))
810+
painter.drawText(48, 70, status)
811+
812+
overlay.draw("status", paint, duration=1)
813+
814+
def on_destroy(self):
815+
overlay = self.get_overlay_view()
816+
if overlay is not None:
817+
overlay.clear_draw("status")
818+
```
783819

784820
应用配置可提供 `blur_area(width, height)` 回调,返回一个 `Box``list[Box]`,用于遮挡
785821
游戏 UID 等静态区域:

ok/gui/debug/OverlayWidget.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ def paintEvent(self, event):
170170
self.paint_alt_overlay(painter)
171171
if og.config.get('debug_cover_uid'):
172172
self.paint_uid_cover(painter)
173+
self.paint_custom(painter)
173174

174175
def set_blur_patches(self, patches):
175176
images = []
@@ -197,6 +198,16 @@ def paint_blur(self, painter):
197198
painter.drawImage(QRectF(x * frame_ratio, y * frame_ratio,
198199
width * frame_ratio, height * frame_ratio), image)
199200

201+
def paint_custom(self, painter):
202+
for key, callback in list(getattr(self, 'custom_painters', {}).items()):
203+
painter.save()
204+
try:
205+
callback(painter, self)
206+
except Exception as e:
207+
logger.warning(f'custom overlay painter {key} failed: {e}')
208+
finally:
209+
painter.restore()
210+
200211
def paint_alt_overlay(self, painter):
201212
if not getattr(self, '_is_alt_down', False):
202213
return

ok/gui/overlay/OverlayWindow.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ class Communicate(QObject):
1616

1717

1818
class OverlayWindow(OverlayWidget):
19+
custom_draw_requested = Signal(str, object, object)
20+
custom_clear_requested = Signal(object)
21+
1922
def __init__(self, hwnd_window: HwndWindow):
2023
super().__init__()
2124
self._source_visible = False
@@ -27,6 +30,8 @@ def __init__(self, hwnd_window: HwndWindow):
2730
self._boxes_until = 0
2831
self._custom_drawing_active = False
2932
self._custom_drawing_until = 0
33+
self.custom_painters = {}
34+
self._custom_painter_until = {}
3035
# Set translucent background
3136
self.setAttribute(Qt.WA_TranslucentBackground)
3237

@@ -39,6 +44,8 @@ def __init__(self, hwnd_window: HwndWindow):
3944
communicate.clear_box.connect(self.clear_drawing)
4045
communicate.blur_overlay.connect(self.update_blur_patches)
4146
communicate.clear_blur_overlay.connect(self.clear_blur_overlay)
47+
self.custom_draw_requested.connect(self._set_custom_draw, Qt.QueuedConnection)
48+
self.custom_clear_requested.connect(self._clear_custom_draw, Qt.QueuedConnection)
4249

4350
# self.update_overlay(hwnd_window.visible, hwnd_window.x, hwnd_window.y, hwnd_window.window_width,
4451
# hwnd_window.window_height, hwnd_window.width, hwnd_window.height, hwnd_window.scaling)
@@ -65,6 +72,46 @@ def request_show(self, duration=4.0):
6572
self.update()
6673
QTimer.singleShot(int(duration * 1000) + 10, self.expire_custom_drawing)
6774

75+
def draw(self, key, callback, duration=None):
76+
"""Schedule arbitrary painting with callback(QPainter, OverlayWindow)."""
77+
if not callable(callback):
78+
raise TypeError('callback must be callable')
79+
self.custom_draw_requested.emit(str(key), callback, duration)
80+
81+
def clear_draw(self, key=None):
82+
"""Remove one custom painter, or all painters when key is None."""
83+
self.custom_clear_requested.emit(key)
84+
85+
def _set_custom_draw(self, key, callback, duration):
86+
self.custom_painters[key] = callback
87+
if duration is None:
88+
self._custom_painter_until.pop(key, None)
89+
else:
90+
duration = max(0.0, float(duration))
91+
self._custom_painter_until[key] = time.monotonic() + duration
92+
QTimer.singleShot(int(duration * 1000) + 10, lambda: self._expire_custom_draw(key))
93+
self.refresh_visibility()
94+
self.update()
95+
96+
def _clear_custom_draw(self, key):
97+
if key is None:
98+
self.custom_painters.clear()
99+
self._custom_painter_until.clear()
100+
else:
101+
key = str(key)
102+
self.custom_painters.pop(key, None)
103+
self._custom_painter_until.pop(key, None)
104+
self.refresh_visibility()
105+
self.update()
106+
107+
def _expire_custom_draw(self, key):
108+
until = self._custom_painter_until.get(key)
109+
if until is not None and time.monotonic() >= until:
110+
self.custom_painters.pop(key, None)
111+
self._custom_painter_until.pop(key, None)
112+
self.refresh_visibility()
113+
self.update()
114+
68115
def on_draw_box(self, key, boxes, color, frame, debug):
69116
if boxes and self._boxes_enabled:
70117
self._boxes_active = True
@@ -100,7 +147,8 @@ def clear_blur_overlay(self):
100147
self.refresh_visibility()
101148

102149
def refresh_visibility(self):
103-
required = self._boxes_active or self._custom_drawing_active or bool(self.blur_images)
150+
required = (self._boxes_active or self._custom_drawing_active
151+
or bool(self.blur_images) or bool(self.custom_painters))
104152
if self._source_visible and required and not self.isVisible():
105153
self.show()
106154
return

ok/task/task.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def start_device(self):
118118
self._app.start_controller.start_device()
119119

120120
def get_overlay_view(self):
121-
"""Return the shared overlay widget when running with the GUI."""
121+
"""Return the raw shared overlay widget when running with the GUI."""
122122
if hasattr(self._app, 'get_overlay_view'):
123123
return self._app.get_overlay_view()
124124
return None

tests/test_overlay_custom_draw.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import os
2+
import threading
3+
import unittest
4+
from types import SimpleNamespace
5+
6+
os.environ.setdefault('QT_QPA_PLATFORM', 'offscreen')
7+
8+
from PySide6.QtGui import QColor, QPen
9+
from PySide6.QtWidgets import QApplication
10+
11+
from ok import og
12+
from ok.gui.overlay.OverlayWindow import OverlayWindow
13+
14+
15+
class TestOverlayCustomDraw(unittest.TestCase):
16+
@classmethod
17+
def setUpClass(cls):
18+
cls.qt_app = QApplication.instance() or QApplication([])
19+
20+
def setUp(self):
21+
self.original_app = getattr(og, 'app', None)
22+
self.original_device_manager = getattr(og, 'device_manager', None)
23+
self.original_ok = getattr(og, 'ok', None)
24+
self.original_config = getattr(og, 'config', None)
25+
og.app = SimpleNamespace(ok_config={'use_overlay': False})
26+
og.device_manager = SimpleNamespace(width=100, height=100)
27+
og.ok = SimpleNamespace(screenshot=SimpleNamespace(ui_dict={}))
28+
og.config = {}
29+
self.view = OverlayWindow(None)
30+
self.view.update_overlay(True, 0, 0, 100, 100, 100, 100, 1)
31+
32+
def tearDown(self):
33+
self.view.close()
34+
self.view.deleteLater()
35+
QApplication.processEvents()
36+
og.app = self.original_app
37+
og.device_manager = self.original_device_manager
38+
og.ok = self.original_ok
39+
og.config = self.original_config
40+
41+
def test_custom_painter_controls_visibility_without_boxes_enabled(self):
42+
painted = []
43+
44+
def paint(painter, view):
45+
painted.append(view)
46+
painter.setPen(QPen(QColor('green'), 2))
47+
painter.drawRect(2, 2, 20, 20)
48+
49+
self.assertFalse(self.view.isVisible())
50+
51+
worker = threading.Thread(target=lambda: self.view.draw('status', paint))
52+
worker.start()
53+
worker.join()
54+
QApplication.processEvents()
55+
self.view.repaint()
56+
57+
self.assertTrue(self.view.isVisible())
58+
self.assertIn(self.view, painted)
59+
60+
worker = threading.Thread(target=lambda: self.view.clear_draw('status'))
61+
worker.start()
62+
worker.join()
63+
QApplication.processEvents()
64+
65+
self.assertFalse(self.view.isVisible())

0 commit comments

Comments
 (0)