fix: 用户操作不再打断bot唤醒截图 — 窗口可见即可截图,不要求前台
根因:_ensure_visible 强制要求窗口在前台(_check_foreground), 用户操作鼠标/键盘会瞬间抢走前台焦点 → 3次重试全部失败 → "唤醒失败"跳过本轮。但 ImageGrab.grab 只需要窗口在屏幕上可见, 不要求前台,用户操作≠窗口被遮挡。 修复: - _ensure_visible:前台校验失败后,若窗口仍然可见(!IsIconic && IsWindowVisible), 仍返回True继续截图。仅窗口真的不可见时返回False - QQ/微信轮询:窗口不可见时退避5秒(每0.1秒检查急停), 给用户接管时间,同时不阻塞急停操作
This commit is contained in:
+29
-10
@@ -672,13 +672,16 @@ class MessageMonitor:
|
||||
logger.debug(f"[{platform}] 滚动到底失败: {e}")
|
||||
|
||||
def _ensure_visible(self, hwnd) -> bool:
|
||||
"""确保窗口可见且真正处于前台(Z-order 最前),用于 OCR 截图路径。
|
||||
"""确保窗口可见,尝试置前但不强制要求前台(用户操作不打断截屏)。
|
||||
|
||||
截屏用 ImageGrab.grab(bbox) 截的是整屏坐标,若目标窗口被遮挡,截到的就是
|
||||
遮挡物(如用户停留的终端)。光靠 SwitchToThisWindow 无法保证置顶,必须
|
||||
SetForegroundWindow。本方法:恢复最小化/隐藏 → AllowSetForegroundWindow
|
||||
解除系统前台限制 → SetForegroundWindow + SwitchToThisWindow 兜底,最多重试 3 次
|
||||
并校验前台成功。不做 TOPMOST(避免 DWM 重排冻结)。
|
||||
截屏用 ImageGrab.grab(bbox) 截的是整屏坐标,只要窗口在屏幕上可见
|
||||
(未被其他窗口完全遮挡),截屏就能拿到正确内容。用户操作鼠标/键盘
|
||||
会抢走前台焦点,但不等于窗口被遮挡——不应因此放弃截屏。
|
||||
|
||||
流程:恢复最小化/隐藏 → AllowSetForegroundWindow 解除限制 →
|
||||
SetForegroundWindow + SwitchToThisWindow 尽力置前(重试3次)→
|
||||
若置前成功直接返回;若置前失败但窗口仍然可见(IsWindowVisible + !IsIconic),
|
||||
也返回 True 继续截图。仅当窗口真的不可见时返回 False。
|
||||
"""
|
||||
root_hwnd = win32gui.GetAncestor(hwnd, win32con.GA_ROOT)
|
||||
if not root_hwnd or not win32gui.IsWindow(root_hwnd):
|
||||
@@ -695,7 +698,7 @@ class MessageMonitor:
|
||||
ctypes.windll.user32.AllowSetForegroundWindow(-1)
|
||||
except Exception:
|
||||
pass
|
||||
# 3) 真正置前并校验:SetForegroundWindow + SwitchToThisWindow 兜底,重试 3 次
|
||||
# 3) 尽力置前:SetForegroundWindow + SwitchToThisWindow 兜底,重试 3 次
|
||||
for _ in range(3):
|
||||
try:
|
||||
win32gui.SetForegroundWindow(root_hwnd)
|
||||
@@ -709,7 +712,13 @@ class MessageMonitor:
|
||||
time.sleep(0.1)
|
||||
if self._check_foreground(root_hwnd):
|
||||
return True
|
||||
logger.warning(f"[{self.__class__.__name__}] 窗口 {root_hwnd} 唤醒失败,可能仍被遮挡")
|
||||
# 4) 前台校验失败 —— 但窗口可能仍然可见(用户操作抢走了前台焦点)
|
||||
# ImageGrab.grab 只需要窗口在屏幕上可见即可,不要求前台。
|
||||
# 仅当窗口真的不可见(被完全遮挡/最小化)时才放弃。
|
||||
if win32gui.IsWindowVisible(root_hwnd) and not win32gui.IsIconic(root_hwnd):
|
||||
logger.debug(f"[{self.__class__.__name__}] 窗口 {root_hwnd} 未获得前台但仍然可见,继续截图")
|
||||
return True
|
||||
logger.warning(f"[{self.__class__.__name__}] 窗口 {root_hwnd} 不可见,跳过本轮")
|
||||
return False
|
||||
|
||||
def _ensure_foreground(self, hwnd, max_retries=5) -> bool:
|
||||
@@ -1296,7 +1305,12 @@ class MessageMonitor:
|
||||
# ⭐ 先唤醒再取窗口 rect(最小化恢复后位置会变化),再截屏
|
||||
# ImageGrab.grab 截整屏坐标,若 QQ 被遮挡(用户停在终端)截到的就是遮挡物
|
||||
if not self._ensure_visible(hwnd):
|
||||
logger.debug("[qq] 唤醒失败,跳过本轮")
|
||||
logger.debug("[qq] 窗口不可见,退避5秒(用户可能在接管)")
|
||||
# 退避期间也要检查急停,避免阻塞5秒
|
||||
for _ in range(50):
|
||||
if not self._running:
|
||||
return []
|
||||
time.sleep(0.1)
|
||||
return []
|
||||
time.sleep(0.4)
|
||||
rect = win32gui.GetWindowRect(hwnd)
|
||||
@@ -1438,7 +1452,12 @@ class MessageMonitor:
|
||||
# ⭐ 先唤醒再截屏:ImageGrab.grab 截整屏坐标,若微信被遮挡(用户停在终端)
|
||||
# 截到的就是遮挡物。唤醒后才取窗口 rect(最小化恢复后位置会变化)。
|
||||
if not self._ensure_visible(hwnd):
|
||||
logger.debug("[wechat] 唤醒失败,跳过本轮")
|
||||
logger.debug("[wechat] 窗口不可见,退避5秒(用户可能在接管)")
|
||||
# 退避期间也要检查急停,避免阻塞5秒
|
||||
for _ in range(50):
|
||||
if not self._running:
|
||||
return []
|
||||
time.sleep(0.1)
|
||||
return []
|
||||
time.sleep(0.4) # 让 DWM 完成窗口激活的合成,防紧接 BitBlt 卡死
|
||||
|
||||
|
||||
Reference in New Issue
Block a user