Feature: Wrapper, Logging und Doku überarbeitet

- Debug-Logging optional gemacht, den Legacy-Wrapper portabler aufgebaut (relative Pfade, Debug-Weitergabe, Python-Fallback) und die README für Endanwender und Entwickler klarer neu strukturiert.
This commit is contained in:
Andre
2026-04-18 17:10:23 +02:00
parent a00bd1ed30
commit b9c1bd5f78
5 changed files with 242 additions and 94 deletions
+49 -13
View File
@@ -8,42 +8,78 @@ FocusFix is a Windows utility that forces a game window into the foreground afte
## Architecture
- **`focusfix.py`** — Python script that supports two modes: `--process Name` for monitoring an already-known process, or direct Steam wrapper mode where `%command%` is passed as positional arguments. In wrapper mode it derives the process name from the target executable, launches the game itself, then polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Runtime logging goes to `focusfix.log`.
- **`launch.vbs`** — Legacy Steam wrapper. No longer required for normal usage, but still present in the repository.
- **`focusfix.py`** — Python script that supports two modes: `--process Name` for monitoring an already-known process, or direct Steam wrapper mode where `%command%` is passed as positional arguments. In wrapper mode it derives the process name from the target executable, launches the game itself, then polls for the game process, waits for its main window (filtered by size to skip splash screens, preferring windows with titles over titleless overlays), waits for the window size to stabilize, then brings the game window to foreground using Win32 API workarounds (`AttachThreadInput` + `Alt` key simulation to bypass `SetForegroundWindow` restrictions). Monitors for window changes (splash → main window) and re-focuses automatically. Retries up to 15 times. Debug logging is only enabled explicitly with `--debug`/`-debug`, writes to `focusfix.log`, and overwrites the file on each new script start.
- **`launch.vbs`** — Legacy Steam wrapper. Still supported as a fallback, but not the primary user path anymore. It resolves `focusfix.py` relative to its own location, prefers a sibling `.venv\Scripts\pythonw.exe`, falls back to `pythonw.exe` from `PATH`, and forwards `-d`/`-debug`/`--debug` to `focusfix.py` without passing those flags to the game.
- **`requirements.txt`** — Source-install dependencies for `focusfix.py` (`psutil`, `pywin32`).
## Setup
```
py -m pip install -r requirements.txt
```
Optional for development:
```
py -m venv .venv
.venv\Scripts\activate
pip install psutil pywin32
pip install -r requirements.txt
```
## Steam Launch Options
Preferred for end users:
```
"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\focusfix.py" %command%
"C:\Path\To\FocusFix\FocusFix.exe" %command%
```
Same launch option works for all Steam games with similar foreground/focus issues — the process name is extracted automatically.
Direct Python/script usage:
```
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command%
```
Legacy wrapper fallback:
```
wscript "C:\Path\To\FocusFix\launch.vbs" %command%
wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command%
```
All Steam variants above work generically across games with similar foreground/focus issues because the process name is derived from the target executable.
## Sunshine / Moonlight
Use as command preparation in Sunshine (full venv path required):
Preferred for end users:
```
"C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe" "C:\Projekte\Steam_backgroud_fix\focusfix.py" --process "ProcessName"
"C:\Path\To\FocusFix\FocusFix.exe" --process "ProcessName"
```
Direct Python/script usage:
```
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "ProcessName"
```
## Running Manually (for testing)
```
.venv\Scripts\python focusfix.py --process CrimsonDesert
.venv\Scripts\python focusfix.py --process CrimsonDesert --timeout 180
.venv\Scripts\python focusfix.py "C:\Games\CrimsonDesert.exe"
python focusfix.py --process CrimsonDesert
python focusfix.py --process CrimsonDesert --timeout 180
python focusfix.py "C:\Games\CrimsonDesert.exe"
python focusfix.py -debug "C:\Games\CrimsonDesert.exe"
```
Use either `--process` or a game command. Runtime logging goes to `focusfix.log`. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console.
Use either `--process` or a game command. By default no log file is created. When started with `--debug` or `-debug`, runtime logging goes to `focusfix.log` and overwrites any existing file from a previous run. When started with `pythonw.exe`, no console window is shown; manual runs with `python.exe` can still print argument or interpreter errors to the console.
## Working Assumptions
- End-user docs and defaults should prefer `FocusFix.exe` over Python or `.venv`-based usage.
- Source-based usage is still supported for developers and power users through `requirements.txt`.
- `launch.vbs` is legacy/fallback behavior, not the primary integration path.
- Do not assume `.venv` exists; wrapper behavior explicitly supports a fallback to global `pythonw.exe`.
## Key Constants in focusfix.py
@@ -54,6 +90,6 @@ Use either `--process` or a game command. Runtime logging goes to `focusfix.log`
- `SetForegroundWindow` is restricted by Windows unless the calling process owns the foreground. Workaround: simulate an `Alt` keypress + `AttachThreadInput` to the foreground thread.
- Alt key release is in a `finally` block to prevent it from staying pressed on errors.
- `pythonw.exe` (not `python.exe`) is required for invisible background execution.
- `launch.vbs` is legacy-only; normal Steam usage now calls `focusfix.py` directly.
- `pythonw.exe` (not `python.exe`) is required for invisible background execution when using the Python/script path.
- Normal user-facing guidance should point to `FocusFix.exe`; direct `focusfix.py` usage is the source/developer path.
- Window selection prefers titled windows over titleless ones (prevents overlay issues like in Wuthering Waves).
+114 -47
View File
@@ -6,11 +6,15 @@ Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bring
## Inhaltsverzeichnis
- [Schnellstart](#schnellstart)
- [Für Nutzer](#fur-nutzer)
- [Für Entwickler](#fur-entwickler)
- [Problembeschreibung](#problembeschreibung)
- [Lösungsansatz](#lösungsansatz)
- [Architektur](#architektur)
- [Voraussetzungen](#voraussetzungen)
- [Installation](#installation)
- [Direkte Verwendung](#direkte-verwendung)
- [Aus Quellcode ausführen](#aus-quellcode-ausfuhren)
- [Konfiguration](#konfiguration)
- [Steam](#steam)
- [Sunshine / Moonlight](#sunshine--moonlight)
@@ -29,6 +33,47 @@ Windows-Hilfsprogramm, das ein Spielfenster automatisch in den Vordergrund bring
---
## Schnellstart
### Für Nutzer
Wenn du FocusFix direkt verwenden willst, ist eine fertige `FocusFix.exe` der empfohlene Weg. Du brauchst dann weder ein Virtual Environment noch ein manuelles `pip install`.
Steam-Beispiel:
```text
"C:\Path\To\FocusFix\FocusFix.exe" %command%
```
Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" -debug %command%
```
### Für Entwickler
Wenn du FocusFix aus dem Repository starten oder weiterentwickeln willst:
```bash
cd C:\Path\To\FocusFix
py -m pip install -r requirements.txt
```
Danach kannst du `focusfix.py` direkt mit deinem Python-Interpreter oder über `pythonw.exe` verwenden.
Steam-Beispiel für die Quellcode-Nutzung mit lokalem `.venv`:
```text
"C:\Projekte\FocusFix\.venv\Scripts\pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command%
```
Ohne lokales `.venv` und mit globalem Python im `PATH`:
```text
"pythonw.exe" "C:\Projekte\FocusFix\focusfix.py" %command%
```
## Problembeschreibung
Beim Start eines Spiels über Steam oder Sunshine/Moonlight öffnet sich das Spielfenster im **Hintergrund**. Die Steam-Oberfläche, die Windows-Taskleiste oder andere Fenster verdecken es, und der Spieler muss manuell per Alt+Tab wechseln.
@@ -86,33 +131,31 @@ Steam Launch Options / Sunshine Befehlsvorbereitung
| Komponente | Version | Hinweis |
|---|---|---|
| Windows | 10 / 11 | Benötigt Win32 API (user32.dll) |
| Python | 3.10+ | `py` Launcher muss verfügbar sein |
| pip-Pakete | `psutil`, `pywin32` | Werden im `.venv` installiert |
| Steam | Beliebig | Startoptionen müssen konfigurierbar sein |
| Python | 3.10+ | Nur für Quellcode-Nutzung erforderlich |
| pip-Pakete | `psutil`, `pywin32` | Nur für Quellcode-Nutzung erforderlich |
## Installation
## Direkte Verwendung
Für normale Nutzer ist eine fertige `.exe` der einfachste Weg.
## Aus Quellcode ausführen
```bash
cd C:\Path\To\FocusFix
# Virtual Environment erstellen
py -m venv .venv
# Aktivieren
.venv\Scripts\activate
# Abhängigkeiten installieren
pip install psutil pywin32
py -m pip install -r requirements.txt
```
### Dateistruktur nach Installation
Optional kannst du dafür auch ein Virtual Environment verwenden. Für Entwickler ist das sinnvoll, für Endanwender aber nicht der Standardweg.
### Dateistruktur im Repository
```
C:\Path\To\FocusFix\
├── .venv\ # Python Virtual Environment
├── focusfix.py # Hauptscript
├── focusfix.log # Logdatei (wird automatisch erstellt)
├── focusfix.log # Logdatei (nur bei --debug / -debug)
├── launch.vbs # Legacy-Wrapper, optional
├── requirements.txt # Python-Abhängigkeiten für Quellcode-Nutzung
├── CLAUDE.md # Kontext für Claude Code
└── README.md # Diese Dokumentation
```
@@ -125,10 +168,25 @@ C:\Path\To\FocusFix\
2. Rechtsklick auf das Spiel → **Eigenschaften**
3. Unter **Allgemein → Startoptionen** eine der folgenden Varianten eintragen:
#### Variante A — Empfohlen: direkt mit `focusfix.py`
#### Variante A — Empfohlen für Nutzer: `FocusFix.exe`
```text
"C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command%
"C:\Path\To\FocusFix\FocusFix.exe" %command%
```
Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" -debug %command%
```
Die `.exe` übernimmt denselben Wrapper-Modus wie `focusfix.py`: Sie erhält `%command%`, startet das Spiel und überwacht danach den Fokus. Diese Variante ist für Endanwender gedacht.
Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden.
#### Variante B — Quellcode direkt mit `focusfix.py`
```text
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" %command%
```
`focusfix.py` extrahiert den Prozessnamen automatisch aus `%command%`, startet das eigentliche Spiel und überwacht danach den Fensterfokus. Die gleiche Startoption kann für beliebige Steam-Spiele mit vergleichbarem Fokus-Problem verwendet werden.
@@ -139,41 +197,49 @@ Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen I
- `"...\focusfix.py"` — Python-Script im Steam-Wrapper-Modus
- `%command%` — Steam-Platzhalter, wird zur Laufzeit durch den vollständigen Pfad zur Spiel-Executable ersetzt, z.B. `"C:\SteamLibrary\...\Game.exe"`
#### Variante B — Legacy: über `launch.vbs`
#### Variante C — Legacy: über `launch.vbs`
Wenn du den alten VBScript-Wrapper weiterhin verwenden willst, kannst du in Steam Folgendes eintragen:
```text
wscript "C:\Path\To\FocusFix\launch.vbs" %command%
Beispiel: "C:\Projekte\FocusFix\launch.vbs" %command%
```
`launch.vbs` extrahiert den Prozessnamen aus `%command%`, startet `focusfix.py` unsichtbar mit `--process` und startet danach das eigentliche Spiel. Diese Variante funktioniert weiterhin, ist aber nur noch als Legacy-/Fallback-Weg gedacht, weil `focusfix.py` den Wrapper-Job inzwischen selbst übernehmen kann.
Die Pfade im Beispiel sind Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden.
Mit Debug-Logging:
```text
wscript "C:\Path\To\FocusFix\launch.vbs" -debug %command%
wscript "C:\Projekte\FocusFix\launch.vbs" -debug %command%
```
`launch.vbs` extrahiert den Prozessnamen aus `%command%`, startet `focusfix.py` unsichtbar mit `--process` und startet danach das eigentliche Spiel. `-d`, `-debug` und `--debug` werden an `focusfix.py` weitergereicht und nicht an das Spiel übergeben. Diese Variante funktioniert weiterhin, ist aber nur noch als Legacy-/Fallback-Weg gedacht, weil `focusfix.py` den Wrapper-Job inzwischen selbst übernehmen kann.
Der Wrapper erwartet `focusfix.py` relativ zu seinem eigenen Speicherort. Für Python nutzt er zuerst `.venv\Scripts\pythonw.exe` im selben Ordner und fällt, falls nicht vorhanden, auf `pythonw.exe` aus der globalen Installation beziehungsweise dem `PATH` zurück.
**Erklärung:**
- `wscript` — startet VBScript ohne sichtbares Konsolenfenster
- `"...\launch.vbs"` — Legacy-Wrapper, der `focusfix.py` indirekt startet
- `-debug` — optional, aktiviert `focusfix.log` für `focusfix.py`
- `%command%` — Steam-Platzhalter mit dem ursprünglichen Spielstartbefehl
#### Variante C — Release-Exe
Wenn du eine gebaute Release-Datei verwenden willst, kannst du statt Python direkt die `.exe` in Steam eintragen. In diesem Repository liegt aktuell eine Build-Datei unter `dist\nuinstall\FocusFix.exe`:
```text
"C:\Path\To\FocusFix\dist\nuinstall\FocusFix.exe" %command%
```
Die `.exe` übernimmt denselben Wrapper-Modus wie `focusfix.py`: Sie erhält `%command%`, startet das Spiel und überwacht danach den Fokus. Diese Variante ist praktisch, wenn du keine sichtbare Python-Installation oder kein venv in den Steam-Startoptionen referenzieren willst.
**Wichtig:** Der Dateiname der Release-Exe hängt vom Build ab. Wenn du später eine neu benannte Exe wie `FocusFix.exe` baust, muss genau dieser Pfad in Steam eingetragen werden.
Auch hier sind die Pfade im Beispiel Platzhalter und müssen durch deinen tatsächlichen Installationspfad ersetzt werden.
### Sunshine / Moonlight
In Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel eintragen:
Für Nutzer mit `.exe`:
```text
"C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "Prozessname"
"C:\Path\To\FocusFix\FocusFix.exe" --process "Prozessname"
```
Mit Debug-Logging:
```text
"C:\Path\To\FocusFix\FocusFix.exe" --debug --process "Prozessname"
```
Für Quellcode-Nutzung in Sunshine als **Befehlsvorbereitung** (Command Preparation) pro Spiel:
```text
"C:\Path\To\Python\pythonw.exe" "C:\Path\To\FocusFix\focusfix.py" --process "Prozessname"
```
Den Prozessnamen im Task-Manager ablesen, ohne `.exe`. Beispiele:
@@ -183,7 +249,7 @@ Den Prozessnamen im Task-Manager ablesen, ohne `.exe`. Beispiele:
| Crimson Desert | `CrimsonDesert` |
| Wuthering Waves | `Client-Win64-Shipping` |
**Wichtig:** Der vollständige Pfad zu `pythonw.exe` im `.venv` ist nötig, damit die Abhängigkeiten (`psutil`, `pywin32`) gefunden werden. `py` oder `python` nutzt sonst möglicherweise eine andere Python-Installation.
**Wichtig:** Wenn du den Quellcode-Weg nutzt, muss der verwendete Python-Interpreter Zugriff auf die installierten Abhängigkeiten aus `requirements.txt` haben. `pythonw.exe` ist für unsichtbare Hintergrundausführung sinnvoller als `python.exe`.
## Ablauf zur Laufzeit
@@ -245,7 +311,7 @@ t=~15s Erfolg → focusfix.py beendet sich
### launch.vbs
`launch.vbs` ist nur noch ein Legacy-Wrapper. Die aktuelle empfohlene Steam-Konfiguration nutzt `focusfix.py` direkt. Die Datei kann als Fallback im Repository bleiben, wird für den normalen Ablauf aber nicht mehr benötigt.
`launch.vbs` ist nur noch ein Legacy-Wrapper. Die aktuelle empfohlene Nutzung für Endanwender erfolgt über `FocusFix.exe`. Die Datei kann als Fallback im Repository bleiben, wird für den normalen Ablauf aber nicht mehr benötigt. Der Wrapper löst `focusfix.py` relativ zu seinem eigenen Ordner auf, verwendet bevorzugt `.venv\Scripts\pythonw.exe` daneben und fällt sonst auf `pythonw.exe` aus der globalen Python-Installation beziehungsweise dem `PATH` zurück. `-d`/`-debug`/`--debug` kann er an `focusfix.py` weitergeben.
## Konfigurationsparameter
@@ -271,7 +337,8 @@ Es muss entweder `--process` oder ein Spielbefehl übergeben werden.
| Pfad | Zweck |
|---|---|
| `C:\Path\To\FocusFix\.venv\Scripts\pythonw.exe` | Beispielpfad zum Python-Interpreter ohne Konsolenfenster, für Steam empfohlen |
| `C:\Path\To\FocusFix\FocusFix.exe` | Empfohlener Pfad für Endanwender |
| `C:\Path\To\Python\pythonw.exe` | Beispielpfad zum Python-Interpreter ohne Konsolenfenster, für Quellcode-Nutzung |
| `C:\Path\To\FocusFix\focusfix.py` | Beispielpfad zum Hauptscript |
Bei einer Änderung des Installationsverzeichnisses müssen diese Pfade in den Steam-Startoptionen oder in Sunshine angepasst werden.
@@ -312,27 +379,27 @@ Das wichtigste Unterscheidungsmerkmal ist hier die Fenstergröße. Kleine Startf
```bash
cd C:\Path\To\FocusFix
.venv\Scripts\python focusfix.py --process "CrimsonDesert" --timeout 5
python focusfix.py --process "CrimsonDesert" --timeout 5
```
### Steam-Wrapper-Modus testen
```bash
.venv\Scripts\python focusfix.py "C:\Path\To\Game\TestGame.exe"
python focusfix.py "C:\Path\To\Game\TestGame.exe"
```
Die Laufzeit-Logs landen in `focusfix.log`. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe. Bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen.
Standardmäßig erzeugt FocusFix keine Logdatei. Mit `--debug` oder `-debug` landen die Laufzeit-Logs in `focusfix.log`; bei jedem neuen Scriptstart wird die Datei dabei überschrieben. Bei Ausführung über `pythonw.exe` gibt es keine sichtbare Ausgabe. Bei manuellen Starts mit `python.exe` können Argument- oder Interpreter-Fehler weiterhin in der Konsole erscheinen.
### Gesamtablauf testen
1. Steam-Startoptionen wie oben konfigurieren.
2. Spiel über Steam starten.
3. Erwartung: Spielfenster erscheint nach dem Ladescreen im Vordergrund, ohne zusätzliches Konsolenfenster.
4. `focusfix.log` prüfen.
4. Falls mit `--debug` gestartet wurde, `focusfix.log` prüfen.
## Debugging
Alle Informationen werden in `focusfix.log` protokolliert:
Für die Fehlersuche FocusFix mit `--debug` oder `-debug` starten. Dann werden alle Informationen in `focusfix.log` protokolliert; ein vorhandenes Logfile aus einem früheren Lauf wird dabei ersetzt:
- Gefundene Kandidaten-Fenster mit Handle, Titel, Größe und PID
- Fensterwechsel und ignorierte titellose Fenster
@@ -343,12 +410,12 @@ Alle Informationen werden in `focusfix.log` protokolliert:
| Symptom | Ursache | Lösung |
|---|---|---|
| Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht und `pythonw.exe`/`focusfix.py` korrekt referenziert werden |
| `ModuleNotFoundError: psutil` | Falsche Python-Installation | Vollständigen Pfad zu `.venv\Scripts\pythonw.exe` verwenden |
| Spiel startet nicht | Steam-Startoptionen fehlerhaft | Prüfen ob `%command%` am Ende steht und `.exe` oder `pythonw.exe`/`focusfix.py` korrekt referenziert werden |
| `ModuleNotFoundError: psutil` | Falsche Python-Installation | `py -m pip install -r requirements.txt` ausführen oder den richtigen Python-Interpreter verwenden |
| Script findet Prozess nicht | Prozessname stimmt nicht oder Launcher-Kette ist ungewöhnlich | Mit `--process` testen oder Log prüfen |
| Fenster wird nicht fokussiert | Windows-Restriktion zu streng | Retry-Anzahl/Delay in Konstanten erhöhen |
| Alt-Taste bleibt hängen | Spiel mit Admin-Rechten, Script ohne | Script ebenfalls als Admin starten |
| CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Steam-/Sunshine-Konfiguration prüfen |
| CMD-Fenster sichtbar | `python.exe` statt `pythonw.exe` | Steam-/Sunshine-Konfiguration prüfen oder `.exe` verwenden |
| Verdunkeltes/blockiertes Spiel | Titelloses Overlay-Fenster | Log prüfen; sollte meist durch Fenster-mit-Titel-Bevorzugung abgefangen werden |
### Fenster-Inspektion per PowerShell
+34 -20
View File
@@ -26,11 +26,9 @@ import win32process
from pathlib import Path
_LOG_PATH = Path(__file__).resolve().parent / "focusfix.log"
logging.basicConfig(
filename=str(_LOG_PATH),
level=logging.DEBUG,
format="%(asctime)s %(levelname)s %(message)s",
)
_LOGGER = logging.getLogger("focusfix")
_LOGGER.addHandler(logging.NullHandler())
_LOGGER.propagate = False
# Minimale Fensterbreite in Pixeln, um den Splashscreen auszuschließen.
MIN_WINDOW_WIDTH = 800
@@ -46,8 +44,21 @@ def derive_process_name(command: list[str]) -> str:
return Path(command[0]).stem
def configure_logging(enabled: bool, log_path: Path = _LOG_PATH) -> None:
_LOGGER.handlers.clear()
if not enabled:
_LOGGER.addHandler(logging.NullHandler())
_LOGGER.setLevel(logging.CRITICAL + 1)
return
file_handler = logging.FileHandler(log_path, mode="w", encoding="utf-8")
file_handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
_LOGGER.addHandler(file_handler)
_LOGGER.setLevel(logging.DEBUG)
def launch_game(command: list[str]) -> subprocess.Popen:
logging.info("Launching game command: %r", command)
_LOGGER.info("Launching game command: %r", command)
return subprocess.Popen(command)
@@ -82,7 +93,7 @@ def find_game_window(pids: set[int]) -> int | None:
height = rect[3] - rect[1]
if width >= MIN_WINDOW_WIDTH:
title = win32gui.GetWindowText(hwnd)
logging.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid)
_LOGGER.debug("Candidate window: hwnd=%s title=%r size=%dx%d pid=%d", hwnd, title, width, height, pid)
found.append((hwnd, title, width, height))
return True
@@ -115,7 +126,7 @@ def wait_for_stable_size(hwnd: int, polls: int = 3, interval: float = 0.5,
stable_count = 0
last_size = size
time.sleep(interval)
logging.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)",
_LOGGER.debug("Stable size for hwnd=%s: %dx%d (after %d stable polls)",
hwnd, last_size[0], last_size[1], stable_count)
return last_size
@@ -154,11 +165,11 @@ def bring_to_front(hwnd: int) -> bool:
ctypes.windll.user32.AttachThreadInput(tgt_tid, fg_tid, False)
result = is_foreground(hwnd)
logging.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)",
_LOGGER.debug("bring_to_front result: %s (foreground hwnd=%s, target hwnd=%s)",
result, win32gui.GetForegroundWindow(), hwnd)
return result
except Exception as e:
logging.exception("bring_to_front failed: %s", e)
_LOGGER.exception("bring_to_front failed: %s", e)
return False
finally:
win32api.keybd_event(win32con.VK_MENU, 0, win32con.KEYEVENTF_KEYUP, 0)
@@ -167,12 +178,15 @@ def bring_to_front(hwnd: int) -> bool:
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("-d", "--debug", "-debug", action="store_true",
help="Debug-Logging nach focusfix.log aktivieren")
parser.add_argument("--timeout", type=int, default=120)
parser.add_argument("--process", type=str,
help="Prozessname des Spiels (ohne .exe)")
parser.add_argument("command", nargs="*",
help="Steam-Spielbefehl, z.B. %%command%%")
args, extra_command_args = parser.parse_known_args()
configure_logging(args.debug)
process_name = args.process
command = args.command + extra_command_args
@@ -187,13 +201,13 @@ def main() -> None:
try:
launch_game(command)
except OSError as exc:
logging.exception("Failed to launch game command: %s", exc)
_LOGGER.exception("Failed to launch game command: %s", exc)
sys.exit(1)
elif not process_name:
parser.error("Either --process or a game command is required.")
deadline = time.time() + args.timeout
logging.info("Monitoring process name: %s", process_name)
_LOGGER.info("Monitoring process name: %s", process_name)
# --- Phase 1: Warten auf den Prozess ---
pids: set[int] = set()
@@ -211,13 +225,13 @@ def main() -> None:
pids = find_process_pids(process_name)
hwnd = find_game_window(pids)
if hwnd:
logging.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd))
_LOGGER.info("Initial window found: hwnd=%s title=%r", hwnd, win32gui.GetWindowText(hwnd))
# Warten bis das Spiel sein Fenster vollständig aufgebaut hat
time.sleep(3)
break
time.sleep(0.5)
else:
logging.error("Timeout waiting for game window")
_LOGGER.error("Timeout waiting for game window")
sys.exit(1)
# --- Phase 3: Spielfenster fokussieren und auf Fensterwechsel reagieren ---
@@ -227,7 +241,7 @@ def main() -> None:
for attempt in range(FOCUS_RETRIES):
pids = find_process_pids(process_name)
if not pids:
logging.warning("Game process gone, stopping")
_LOGGER.warning("Game process gone, stopping")
break
current_hwnd = find_game_window(pids)
@@ -237,10 +251,10 @@ def main() -> None:
old_title = win32gui.GetWindowText(hwnd) if hwnd else ""
# Nicht von einem Fenster mit Titel auf ein titelloses wechseln
if not new_title and old_title:
logging.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)",
_LOGGER.debug("Ignoring titleless window hwnd=%s (keeping %s title=%r)",
current_hwnd, hwnd, old_title)
else:
logging.info("Window changed: old=%s new=%s title=%r",
_LOGGER.info("Window changed: old=%s new=%s title=%r",
hwnd, current_hwnd, new_title)
hwnd = current_hwnd
focused_hwnd = None # neues Fenster muss neu fokussiert werden
@@ -258,15 +272,15 @@ def main() -> None:
# Warten bis Fenstergröße stabil ist (Resize abgeschlossen)
wait_for_stable_size(hwnd)
logging.info("Focus attempt %d/%d for hwnd=%s title=%r",
_LOGGER.info("Focus attempt %d/%d for hwnd=%s title=%r",
attempt + 1, FOCUS_RETRIES, hwnd, win32gui.GetWindowText(hwnd))
success = bring_to_front(hwnd)
if success:
logging.info("Focus succeeded on attempt %d", attempt + 1)
_LOGGER.info("Focus succeeded on attempt %d", attempt + 1)
focused_hwnd = hwnd
time.sleep(FOCUS_RETRY_DELAY)
logging.info("Done. Final focused hwnd=%s", focused_hwnd)
_LOGGER.info("Done. Final focused hwnd=%s", focused_hwnd)
if __name__ == "__main__":
+42 -13
View File
@@ -1,21 +1,50 @@
Set WshShell = CreateObject("WScript.Shell")
' Prozessname aus dem ersten Argument (Steam %command%) extrahieren
Dim exePath, fso, exeName
Set fso = CreateObject("Scripting.FileSystemObject")
exePath = WScript.Arguments(0)
Dim baseDir, pythonwPath, focusfixPath
baseDir = fso.GetParentFolderName(WScript.ScriptFullName)
pythonwPath = fso.BuildPath(baseDir, ".venv\Scripts\pythonw.exe")
focusfixPath = fso.BuildPath(baseDir, "focusfix.py")
If Not fso.FileExists(pythonwPath) Then
pythonwPath = "pythonw.exe"
End If
Dim debugEnabled, exePath, exeName, gameCmd, arg, i
debugEnabled = False
exePath = ""
gameCmd = ""
For i = 0 To WScript.Arguments.Count - 1
arg = WScript.Arguments(i)
If arg = "-d" Or arg = "-debug" Or arg = "--debug" Then
debugEnabled = True
Else
If exePath = "" Then
exePath = arg
End If
gameCmd = gameCmd & """" & arg & """ "
End If
Next
If exePath = "" Then
WScript.Quit 1
End If
' Prozessname aus dem ersten nicht-Wrapper-Argument (Steam %command%) extrahieren
exeName = fso.GetBaseName(exePath)
' FocusFix Script unsichtbar starten (pythonw = kein Konsolenfenster)
WshShell.Run """C:\Projekte\Steam_backgroud_fix\.venv\Scripts\pythonw.exe"" ""C:\Projekte\Steam_backgroud_fix\focusfix.py"" --process """ & exeName & """", 0, False
Dim focusfixCmd
focusfixCmd = """" & pythonwPath & """ """ & focusfixPath & """"
If debugEnabled Then
focusfixCmd = focusfixCmd & " -debug"
End If
focusfixCmd = focusfixCmd & " --process """ & exeName & """"
WshShell.Run focusfixCmd, 0, False
' Spiel starten: alle Argumente (= Steam %command%) zusammensetzen und ausführen
Dim gameCmd, i
gameCmd = ""
For i = 0 To WScript.Arguments.Count - 1
gameCmd = gameCmd & """" & WScript.Arguments(i) & """ "
Next
If gameCmd <> "" Then
' Spiel starten: alle übrigen Argumente (= Steam %command%) zusammensetzen und ausführen
If Trim(gameCmd) <> "" Then
WshShell.Run gameCmd, 1, False
End If
+2
View File
@@ -0,0 +1,2 @@
psutil
pywin32