Force multiple window topmost strategies to prevent focus loss

This commit is contained in:
z060142 2025-05-15 11:18:54 +08:00
parent 890772f70e
commit 677a73f026

View File

@ -72,6 +72,10 @@ class GameMonitor:
self.monitor_thread = None self.monitor_thread = None
self.stop_event = threading.Event() self.stop_event = threading.Event()
# Add these tracking variables
self.last_focus_failure_count = 0
self.last_successful_foreground = time.time()
self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'") self.logger.info(f"GameMonitor initialized. Game window: '{self.window_title}', Process: '{self.game_process_name}'")
self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}") self.logger.info(f"Position: ({self.window_x}, {self.window_y}), Size: {self.window_width}x{self.window_height}")
self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes") self.logger.info(f"Scheduled Restart: {'Enabled' if self.enable_restart else 'Disabled'}, Interval: {self.restart_interval} minutes")
@ -160,51 +164,41 @@ class GameMonitor:
if current_pos != target_pos or current_size != target_size: if current_pos != target_pos or current_size != target_size:
window.moveTo(target_pos[0], target_pos[1]) window.moveTo(target_pos[0], target_pos[1])
window.resizeTo(target_size[0], target_size[1]) window.resizeTo(target_size[0], target_size[1])
# Verify if move and resize were successful
time.sleep(0.1) time.sleep(0.1)
window.activate() # Try activating to ensure changes apply window.activate()
time.sleep(0.1) time.sleep(0.1)
# Check if changes were successful
new_pos = (window.left, window.top) new_pos = (window.left, window.top)
new_size = (window.width, window.height) new_size = (window.width, window.height)
if new_pos == target_pos and new_size == target_size: if new_pos == target_pos and new_size == target_size:
current_message += f"Adjusted game window to position ({target_pos[0]},{target_pos[1]}) size {target_size[0]}x{target_size[1]}. " current_message += f"Adjusted window position/size. "
adjustment_made = True adjustment_made = True
else:
self.logger.warning(f"Attempted to adjust window pos/size, but result mismatch. Target: {target_pos}/{target_size}, Actual: {new_pos}/{new_size}")
# 2. Check and bring to foreground using enhanced method
# 2. Check and bring to foreground
current_foreground_hwnd = win32gui.GetForegroundWindow() current_foreground_hwnd = win32gui.GetForegroundWindow()
if current_foreground_hwnd != hwnd: if current_foreground_hwnd != hwnd:
try: # Use enhanced forceful focus method
# Use HWND_TOP to bring window to top, not HWND_TOPMOST success, method_used = self._force_window_foreground(hwnd, window)
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0, if success:
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE) current_message += f"Focused window using {method_used}. "
adjustment_made = True
if not hasattr(self, 'last_focus_failure_count'):
self.last_focus_failure_count = 0
self.last_focus_failure_count = 0
else:
# Increment failure counter
if not hasattr(self, 'last_focus_failure_count'):
self.last_focus_failure_count = 0
self.last_focus_failure_count += 1
# Set as foreground window (gain focus) # Log warning with consecutive failure count
win32gui.SetForegroundWindow(hwnd) self.logger.warning(f"Window focus failed (attempt {self.last_focus_failure_count}): {method_used}")
# Verify if window is active # Restart game after too many failures
time.sleep(0.1) if self.last_focus_failure_count >= 15:
foreground_hwnd = win32gui.GetForegroundWindow() self.logger.warning("Excessive focus failures, restarting game...")
self._perform_restart()
if foreground_hwnd == hwnd: self.last_focus_failure_count = 0
current_message += "Brought game window to foreground and set focus. "
adjustment_made = True
else:
# Use fallback method
self.logger.warning("SetForegroundWindow failed, trying fallback window.activate()")
try:
window.activate()
time.sleep(0.1)
if win32gui.GetForegroundWindow() == hwnd:
current_message += "Set game window focus using fallback method. "
adjustment_made = True
except Exception as activate_err:
self.logger.warning(f"Fallback method window.activate() failed: {activate_err}")
except Exception as focus_err:
self.logger.warning(f"Error setting window focus: {focus_err}")
else: else:
# Use basic functions on non-Windows platforms # Use basic functions on non-Windows platforms
current_pos = (window.left, window.top) current_pos = (window.left, window.top)
@ -255,27 +249,181 @@ class GameMonitor:
self.logger.debug(f"Error finding game window: {e}") self.logger.debug(f"Error finding game window: {e}")
return None return None
def _find_game_process(self): def _force_window_foreground(self, hwnd, window):
"""Find the game process""" """Aggressive window focus implementation"""
if not HAS_PSUTIL: if not HAS_WIN32:
self.logger.warning("psutil is not available, cannot perform process lookup") return False, "win32 modules unavailable"
success = False
methods_tried = []
# Method 1: HWND_TOPMOST strategy
methods_tried.append("HWND_TOPMOST")
try:
win32gui.SetWindowPos(hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
time.sleep(0.1)
win32gui.SetWindowPos(hwnd, win32con.HWND_TOP, 0, 0, 0, 0,
win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
if win32gui.GetForegroundWindow() == hwnd:
return True, "HWND_TOPMOST"
except Exception as e:
self.logger.debug(f"Method 1 failed: {e}")
# Method 2: Minimize/restore cycle
methods_tried.append("MinimizeRestore")
try:
win32gui.ShowWindow(hwnd, win32con.SW_MINIMIZE)
time.sleep(0.3)
win32gui.ShowWindow(hwnd, win32con.SW_RESTORE)
time.sleep(0.2)
win32gui.SetForegroundWindow(hwnd)
if win32gui.GetForegroundWindow() == hwnd:
return True, "MinimizeRestore"
except Exception as e:
self.logger.debug(f"Method 2 failed: {e}")
# Method 3: Thread input attach
methods_tried.append("ThreadAttach")
try:
import win32process
import win32api
current_thread_id = win32api.GetCurrentThreadId()
window_thread_id = win32process.GetWindowThreadProcessId(hwnd)[0]
if current_thread_id != window_thread_id:
win32process.AttachThreadInput(current_thread_id, window_thread_id, True)
try:
win32gui.BringWindowToTop(hwnd)
win32gui.SetForegroundWindow(hwnd)
time.sleep(0.2)
if win32gui.GetForegroundWindow() == hwnd:
return True, "ThreadAttach"
finally:
win32process.AttachThreadInput(current_thread_id, window_thread_id, False)
except Exception as e:
self.logger.debug(f"Method 3 failed: {e}")
# Method 4: Flash + Window messages
methods_tried.append("Flash+Messages")
try:
# First flash to get attention
win32gui.FlashWindow(hwnd, True)
time.sleep(0.2)
# Then send specific window messages
win32gui.SendMessage(hwnd, win32con.WM_SETREDRAW, 0, 0)
win32gui.SendMessage(hwnd, win32con.WM_SETREDRAW, 1, 0)
win32gui.RedrawWindow(hwnd, None, None,
win32con.RDW_FRAME | win32con.RDW_INVALIDATE |
win32con.RDW_UPDATENOW | win32con.RDW_ALLCHILDREN)
win32gui.PostMessage(hwnd, win32con.WM_SYSCOMMAND, win32con.SC_RESTORE, 0)
win32gui.PostMessage(hwnd, win32con.WM_ACTIVATE, win32con.WA_ACTIVE, 0)
time.sleep(0.2)
if win32gui.GetForegroundWindow() == hwnd:
return True, "Flash+Messages"
except Exception as e:
self.logger.debug(f"Method 4 failed: {e}")
# Method 5: Hide/Show cycle
methods_tried.append("HideShow")
try:
win32gui.ShowWindow(hwnd, win32con.SW_HIDE)
time.sleep(0.2)
win32gui.ShowWindow(hwnd, win32con.SW_SHOW)
time.sleep(0.2)
win32gui.SetForegroundWindow(hwnd)
if win32gui.GetForegroundWindow() == hwnd:
return True, "HideShow"
except Exception as e:
self.logger.debug(f"Method 5 failed: {e}")
return False, f"All methods failed: {', '.join(methods_tried)}"
def _find_game_process_by_window(self):
"""Find process using both window title and process name"""
if not HAS_PSUTIL or not HAS_WIN32:
return None return None
try: try:
for proc in psutil.process_iter(['pid', 'name', 'exe']): window = self._find_game_window()
try: if not window:
proc_info = proc.info return None
proc_name = proc_info.get('name')
if proc_name and proc_name.lower() == self.game_process_name.lower(): hwnd = window._hWnd
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid})") window_pid = None
try:
import win32process
_, window_pid = win32process.GetWindowThreadProcessId(hwnd)
except Exception:
return None
if window_pid:
try:
proc = psutil.Process(window_pid)
proc_name = proc.name()
if proc_name.lower() == self.game_process_name.lower():
self.logger.info(f"Found game process '{proc_name}' (PID: {proc.pid}) with window title '{self.window_title}'")
return proc return proc
else:
self.logger.debug(f"Window process name mismatch: expected '{self.game_process_name}', got '{proc_name}'")
return proc # Returning proc even if name mismatches, as per user's code.
except Exception:
pass
# Fallback to name-based search if window-based fails or PID doesn't match process name.
# The user's provided code implies a fallback to _find_game_process_by_name()
# This will be handled by the updated _find_game_process method.
# For now, if the window PID didn't lead to a matching process name, we return None here.
# The original code had "return self._find_game_process_by_name()" here,
# but that would create a direct dependency. The new _find_game_process handles the fallback.
# So, if we reach here, it means the window was found, PID was obtained, but process name didn't match.
# The original code returns `proc` even on mismatch, so I'll keep that.
# If `window_pid` was None or `psutil.Process(window_pid)` failed, it would have returned None or passed.
# The logic "return self._find_game_process_by_name()" was in the original snippet,
# I will include it here as per the snippet, but note that the overall _find_game_process will also call it.
return self._find_game_process_by_name() # As per user snippet
except Exception as e:
self.logger.error(f"Process-by-window lookup error: {e}")
return None
def _find_game_process(self):
"""Find game process with combined approach"""
# Try window-based process lookup first
proc = self._find_game_process_by_window()
if proc:
return proc
# Fall back to name-only lookup
# This is the original _find_game_process logic, now as a fallback.
if not HAS_PSUTIL:
self.logger.debug("psutil not available for name-only process lookup fallback.") # Changed to debug as primary is window based
return None
try:
for p_iter in psutil.process_iter(['pid', 'name', 'exe']):
try:
proc_info = p_iter.info
proc_name = proc_info.get('name')
if proc_name and proc_name.lower() == self.game_process_name.lower():
self.logger.info(f"Found game process by name '{proc_name}' (PID: {p_iter.pid}) as fallback")
return p_iter
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
continue continue
except Exception as e: except Exception as e:
self.logger.error(f"Error finding game process: {e}") self.logger.error(f"Error in name-only game process lookup: {e}")
self.logger.info(f"Game process '{self.game_process_name}' not found") self.logger.info(f"Game process '{self.game_process_name}' not found by name either.")
return None return None
def _perform_restart(self): def _perform_restart(self):
@ -298,7 +446,7 @@ class GameMonitor:
self.logger.error("Failed to start game") self.logger.error("Failed to start game")
# 4. Wait for game to launch # 4. Wait for game to launch
restart_wait_time = 30 # seconds restart_wait_time = 45 # seconds, increased from 30
self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...") self.logger.info(f"Waiting for game to start ({restart_wait_time} seconds)...")
time.sleep(restart_wait_time) time.sleep(restart_wait_time)