Skip to content

Commit 5a3454b

Browse files
committed
Fix PostMessage hwnd targeting
1 parent 07409c1 commit 5a3454b

5 files changed

Lines changed: 134 additions & 67 deletions

File tree

ok/device/capture_methods/hwnd_window.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,15 @@ def stop(self):
8484
def _front_hwnd_candidates(self):
8585
return list(dict.fromkeys(hwnd for hwnd in (self.top_hwnd, self.hwnd) if hwnd))
8686

87+
def _top_hwnd_info(self, hwnds):
88+
if not hwnds:
89+
return None
90+
91+
for hwnd_info in hwnds:
92+
if hwnd_info[0] != self.hwnd:
93+
return hwnd_info
94+
return hwnds[0]
95+
8796
def bring_to_front(self):
8897
errors = []
8998
for refreshed in (False, True):
@@ -215,14 +224,15 @@ def do_update_window_size(self):
215224
self.real_y_offset = real_y_offset
216225
self.real_width = real_width
217226
self.real_height = real_height
218-
self.top_hwnd = hwnds[0][0] if hwnds else self.hwnd
227+
top_hwnd_info = self._top_hwnd_info(hwnds)
228+
self.top_hwnd = top_hwnd_info[0] if top_hwnd_info else self.hwnd
219229
self.top_offset_x = 0
220230
self.top_offset_y = 0
221-
if hwnds and len(hwnds) > 0:
231+
if top_hwnd_info:
222232
bg_hwnd_info = next((w for w in hwnds if w[0] == self.hwnd), None)
223233
if bg_hwnd_info:
224-
self.top_offset_x = hwnds[0][4] - bg_hwnd_info[4]
225-
self.top_offset_y = hwnds[0][5] - bg_hwnd_info[5]
234+
self.top_offset_x = top_hwnd_info[4] - bg_hwnd_info[4]
235+
self.top_offset_y = top_hwnd_info[5] - bg_hwnd_info[5]
226236

227237
exists = self.hwnd > 0
228238
if self.hwnd > 0:

ok/device/interaction_methods/post_message.py

Lines changed: 37 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
logger = Logger.get_logger(__name__)
1313

14+
1415
class PostMessageInteraction(BaseInteraction):
1516

1617
def __init__(self, capture: BaseCaptureMethod, hwnd_window):
@@ -72,6 +73,7 @@ def input_text(self, text, activate=True):
7273
def move(self, x, y, down_btn=0):
7374
long_pos = self.update_mouse_pos(x, y, True)
7475
self.post(win32con.WM_MOUSEMOVE, down_btn, long_pos)
76+
return long_pos
7577

7678
def scroll(self, x, y, scroll_amount):
7779
self.try_activate()
@@ -137,9 +139,10 @@ def try_activate(self):
137139
def click(self, x=-1, y=-1, move_back=False, name=None, down_time=0.01, move=True, key="left"):
138140
super().click(x, y, name=name)
139141
if move:
140-
self.move(x, y)
142+
long_position = self.move(x, y)
141143
time.sleep(down_time)
142-
long_position = self.update_mouse_pos(x, y, activate=not move)
144+
else:
145+
long_position = self.update_mouse_pos(x, y, activate=True)
143146

144147
if key == "left":
145148
btn_down = win32con.WM_LBUTTONDOWN
@@ -181,75 +184,52 @@ def mouse_down(self, x=-1, y=-1, name=None, key="left"):
181184

182185
def update_mouse_pos(self, x, y, activate=True):
183186
self.try_activate()
184-
187+
185188
base_hwnd = self.hwnd_window.top_hwnd if self.hwnd_window.top_hwnd else self.hwnd_window.hwnd
186-
189+
187190
if x == -1 or y == -1:
188191
x, y = getattr(self, 'bg_mouse_pos', (0, 0))
189192
else:
190193
x, y = self.hwnd_window.get_top_window_cords(x, y)
191194
self.bg_mouse_pos = (x, y)
192-
195+
193196
try:
194197
abs_x, abs_y = win32gui.ClientToScreen(base_hwnd, (int(x), int(y)))
195-
196-
# Validate that the click position falls within the boundary of base_hwnd.
197-
# If not, search through the available hwnds list for one that contains it.
198+
199+
target_hwnd = base_hwnd
198200
hwnds = getattr(self.hwnd_window, 'hwnds', [])
199-
if hwnds and len(hwnds) > 1:
201+
for hwnd_info in hwnds:
202+
candidate = hwnd_info[0]
203+
if not win32gui.IsWindow(candidate):
204+
continue
200205
try:
201-
base_rect = win32gui.GetWindowRect(base_hwnd)
202-
in_boundary = base_rect[0] <= abs_x < base_rect[2] and base_rect[1] <= abs_y < base_rect[3]
203-
if not in_boundary:
204-
for hwnd_info in hwnds:
205-
candidate = hwnd_info[0]
206-
if candidate == base_hwnd or not win32gui.IsWindow(candidate):
207-
continue
208-
try:
209-
rect = win32gui.GetWindowRect(candidate)
210-
if rect[0] <= abs_x < rect[2] and rect[1] <= abs_y < rect[3]:
211-
logger.debug(
212-
f'update_mouse_pos click ({abs_x},{abs_y}) outside base_hwnd {base_hwnd} rect {base_rect}, switching to hwnd {candidate} rect {rect}')
213-
base_hwnd = candidate
214-
# Recompute abs coords relative to the new base_hwnd
215-
local_candidate_x = hwnd_info[4] if len(hwnd_info) > 4 else 0
216-
local_candidate_y = hwnd_info[5] if len(hwnd_info) > 5 else 0
217-
# x,y are in original bg-window coords; translate to candidate client coords
218-
orig_base_info = next((w for w in hwnds if w[0] == (self.hwnd_window.top_hwnd or self.hwnd_window.hwnd)), None)
219-
if orig_base_info:
220-
dx = orig_base_info[4] - local_candidate_x
221-
dy = orig_base_info[5] - local_candidate_y
222-
abs_x, abs_y = win32gui.ClientToScreen(candidate, (int(x + dx), int(y + dy)))
223-
break
224-
except Exception:
225-
continue
206+
left = hwnd_info[4]
207+
top = hwnd_info[5]
208+
right = left + hwnd_info[2]
209+
bottom = top + hwnd_info[3]
210+
if left <= abs_x < right and top <= abs_y < bottom:
211+
target_hwnd = candidate
212+
break
226213
except Exception:
227-
pass
228-
229-
found_child = None
230-
if self.hwnd_window.top_hwnd and self.hwnd_window.top_hwnd != self.hwnd_window.hwnd:
231-
def find_child_at_point(child, _):
232-
nonlocal found_child
233-
if win32gui.IsWindowVisible(child):
234-
rect = win32gui.GetWindowRect(child)
235-
if rect[0] <= abs_x < rect[2] and rect[1] <= abs_y < rect[3]:
236-
found_child = child
237-
return False
238-
return True
239-
240-
try:
241-
win32gui.EnumChildWindows(base_hwnd, find_child_at_point, None)
242-
except Exception: pass
243-
logger.debug(f'found_child {found_child}')
244-
245-
target_hwnd = found_child if found_child else base_hwnd
214+
continue
246215
self._dynamic_target_hwnd = target_hwnd
247-
216+
248217
local_x, local_y = win32gui.ScreenToClient(target_hwnd, (abs_x, abs_y))
249-
250-
# logger.debug(f'mouse_pos dynamically aimed at {target_hwnd} ({win32gui.GetClassName(target_hwnd)}): {local_x}, {local_y}')
218+
219+
hwnd_descriptions = []
220+
for index, hwnd_info in enumerate(hwnds):
221+
candidate = hwnd_info[0]
222+
try:
223+
class_name = win32gui.GetClassName(candidate) if win32gui.IsWindow(candidate) else '<invalid>'
224+
except Exception as e:
225+
class_name = f'<class error: {e}>'
226+
hwnd_descriptions.append(f'{index}:{candidate}({class_name})')
227+
logger.debug(
228+
f'hwnd_window hwnds hwnd={self.hwnd_window.hwnd} top_hwnd={self.hwnd_window.top_hwnd}: {hwnd_descriptions}')
229+
logger.debug(
230+
f'mouse_pos dynamically aimed at {target_hwnd} ({win32gui.GetClassName(target_hwnd)}): {local_x}, {local_y}')
251231
return win32api.MAKELONG(local_x, local_y)
252-
232+
253233
except Exception as e:
254234
logger.error(f'update_mouse_pos conversion error targeting {base_hwnd}', e)
255235
self._dynamic_target_hwnd = base_hwnd

ok/task/TaskExecutor.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,17 +333,19 @@ def wait_condition(self, condition, time_out=0, pre_action=None, post_action=Non
333333
result = condition()
334334
result_str = list_or_obj_to_str(result)
335335
if result:
336-
logger.debug(
337-
f"found result {result_str} {(time.time() - start):.3f}")
338336
if settle_time == -1:
339337
settle_time = self.wait_until_settle_time
340338
if settle_time > 0:
341-
if settled > 0 and time.time() - settled > settle_time:
339+
now = time.time()
340+
if settled > 0 and now - settled > settle_time:
341+
logger.debug(f"found result {result_str} {(now - start):.3f}")
342342
return result
343343
if settled == 0:
344-
settled = time.time()
344+
logger.debug(f"found result {result_str} {(now - start):.3f}")
345+
settled = now
345346
continue
346347
else:
348+
logger.debug(f"found result {result_str} {(time.time() - start):.3f}")
347349
return result
348350
else:
349351
settled = 0

ok/util/print_hwnd.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import time
2+
13
import win32gui
24
import win32process
35
import psutil
@@ -105,10 +107,9 @@ def traverse_and_print(parent_hwnd=0, depth=0):
105107

106108
# Formatting variables for clean output
107109
vis_str = "VIS" if info['visible'] else "INV"
108-
hwnd_hex = f"{info['hwnd']:08X}"
109110
size_str = f"{info['width']}x{info['height']}"
110111

111-
print(f"{indent}[HWND: {hwnd_hex}] | {vis_str} | Size: {size_str:<9} (Area: {info['area']:<7}) | "
112+
print(f"{indent}[HWND: {info['hwnd']}] | {vis_str} | Size: {size_str:<9} (Area: {info['area']:<7}) | "
112113
f"EXE: {info['exe']:<20} | PID: {info['pid']:<6} | Class: {info['class']} | "
113114
f"CMD: {info['cmdline']}")
114115

ok/util/window.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,75 @@ def is_foreground_window(hwnd):
122122
return win32gui.IsWindowVisible(hwnd) and win32gui.GetForegroundWindow() == hwnd
123123

124124

125+
def sort_hwnds_top_to_bottom(hwnds):
126+
if not hwnds:
127+
return hwnds
128+
129+
hwnd_map = {hwnd_info[0]: hwnd_info for hwnd_info in hwnds}
130+
ordered = []
131+
seen = set()
132+
133+
try:
134+
hwnd = win32gui.GetTopWindow(0)
135+
while hwnd:
136+
if hwnd in hwnd_map and hwnd not in seen:
137+
ordered.append(hwnd_map[hwnd])
138+
seen.add(hwnd)
139+
if len(seen) == len(hwnd_map):
140+
break
141+
hwnd = win32gui.GetWindow(hwnd, win32con.GW_HWNDNEXT)
142+
except Exception as e:
143+
logger.debug(f'sort_hwnds_top_to_bottom failed: {e}')
144+
145+
ordered.extend(hwnd_info for hwnd_info in hwnds if hwnd_info[0] not in seen)
146+
return ordered
147+
148+
149+
def _hwnd_info(hwnd, full_path=None):
150+
x, y, _, _, width, height, m_scaling = get_window_bounds(hwnd)
151+
text = win32gui.GetWindowText(hwnd)
152+
cname = win32gui.GetClassName(hwnd)
153+
return hwnd, full_path, width, height, x, y, text, cname, m_scaling
154+
155+
156+
def _clickable_child_hwnds(parent_info, known_hwnds, top_hwnd_class=None):
157+
parent_hwnd = parent_info[0]
158+
full_path = parent_info[1]
159+
clickable = []
160+
161+
def append_children(parent):
162+
child = win32gui.GetTopWindow(parent)
163+
while child:
164+
if child not in known_hwnds and win32gui.IsWindow(child) and win32gui.IsWindowVisible(child) and win32gui.IsWindowEnabled(child):
165+
try:
166+
child_info = _hwnd_info(child, full_path)
167+
if child_info[2] > 0 and child_info[3] > 0:
168+
append_children(child)
169+
if top_hwnd_class is None or _match_class_name(child_info[7], top_hwnd_class) >= 0:
170+
clickable.append(child_info)
171+
known_hwnds.add(child)
172+
except Exception:
173+
pass
174+
child = win32gui.GetWindow(child, win32con.GW_HWNDNEXT)
175+
176+
try:
177+
append_children(parent_hwnd)
178+
except Exception as e:
179+
logger.debug(f'_clickable_child_hwnds failed for {parent_hwnd}: {e}')
180+
return clickable
181+
182+
183+
def expand_clickable_hwnds(hwnds, main_hwnd=0, top_hwnd_class=None):
184+
expanded = []
185+
known_hwnds = {hwnd_info[0] for hwnd_info in hwnds}
186+
187+
for hwnd_info in hwnds:
188+
if len(hwnds) == 1 or hwnd_info[0] != main_hwnd:
189+
expanded.extend(_clickable_child_hwnds(hwnd_info, known_hwnds, top_hwnd_class))
190+
expanded.append(hwnd_info)
191+
return expanded
192+
193+
125194
def show_title_bar(hwnd):
126195
try:
127196
current_style = win32gui.GetWindowLong(hwnd, win32con.GWL_STYLE)
@@ -351,6 +420,11 @@ def is_match(hwnd, results):
351420
for top_item in reversed(filtered_top):
352421
if top_item[0] != biggest[0] and not any(r[0] == top_item[0] for r in results):
353422
results.insert(0, top_item[:9] if len(top_item) > 9 else top_item)
423+
sorted_results = sort_hwnds_top_to_bottom(results)
424+
if [r[0] for r in sorted_results] != [r[0] for r in results]:
425+
logger.debug(f'find_hwnd sorted hwnds by z-order {[r[0] for r in sorted_results]}')
426+
results = sorted_results
427+
results = expand_clickable_hwnds(results, biggest[0], top_hwnd_class)
354428

355429
x_offset, y_offset, real_width, real_height = 0, 0, biggest[2], biggest[3]
356430
if class_name is None and frame_aspect_ratio != 0:

0 commit comments

Comments
 (0)