fix: 用户操作不再打断bot唤醒截图 — 窗口可见即可截图,不要求前台

根因:_ensure_visible 强制要求窗口在前台(_check_foreground),
用户操作鼠标/键盘会瞬间抢走前台焦点 → 3次重试全部失败 →
"唤醒失败"跳过本轮。但 ImageGrab.grab 只需要窗口在屏幕上可见,
不要求前台,用户操作≠窗口被遮挡。

修复:
- _ensure_visible:前台校验失败后,若窗口仍然可见(!IsIconic && IsWindowVisible),
  仍返回True继续截图。仅窗口真的不可见时返回False
- QQ/微信轮询:窗口不可见时退避5秒(每0.1秒检查急停),
  给用户接管时间,同时不阻塞急停操作
This commit is contained in:
youpaishidifu
2026-07-31 14:44:12 +08:00
parent 3a4fecbc03
commit ce74fc762a
+29 -10
View File
@@ -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 卡死