diff --git a/monitor.py b/monitor.py index 22ad7ac..64660b8 100644 --- a/monitor.py +++ b/monitor.py @@ -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 卡死