Seine Ansage "da gehen noch die popups auf dem video monitor auf" "einfach so programmieren das die popup auf dem fenster mittig vom main fenster auf gehen" Was falsch war An sechs Stellen stand `max(0, x)`. Das sollte verhindern, dass ein Dialog ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE Koordinate auf null. Bei ihm steht der zweite Monitor links (x = -1920): jedes Popup sprang damit zurueck auf den Hauptbildschirm, obwohl das Programm nebenan lief. Dazu kamen feste Versaetze wie "+120+140", die mit der Fenstergroesse nichts zu tun hatten. theme.mittig(dialog, ueber) Eine Stelle fuer alle Dialoge: mittig ueber dem Elternfenster, etwas oberhalb der Mitte (ein Drittel statt der Haelfte), damit der Blick dort ist und die Mitte des Formulars frei bleibt. Das ist zugleich die Antwort auf die Monitorfrage: steht das Programm auf dem zweiten Bildschirm, geht der Dialog dort auf - ohne dass irgendwo ein Monitor gesucht werden muss. Die Sonderbehandlung im Testlauf konnte deshalb entfallen; geblieben ist dort nur der Verzicht auf grab_set. theme.auf_bildschirm(x, y, breite, hoehe) Begrenzt eine Position auf den GESAMTEN Desktop ueber alle Monitore (SM_*VIRTUALSCREEN) statt auf den Hauptmonitor. Das war die eigentliche Absicht hinter dem alten max(0, ...). Gemessen statt geglaubt: Dialogmitte und Fenstermitte liegen 8 Pixel auseinander (Fensterrahmen), der Dialog liegt innerhalb des Fensters. Steht als Pruefung in pruef_darstellung.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
142 lines
5 KiB
Python
142 lines
5 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Pruefstand: kommt bei der GEBAUTEN EXE wirklich ein Fenster?
|
|
|
|
DER FALL, DER DAZU GEFUEHRT HAT (06.09.2026)
|
|
--------------------------------------------
|
|
Nach dem Installieren lief das Programm - im Taskmanager stand es da, es
|
|
verbrauchte Rechenzeit - und auf dem Bildschirm war NICHTS. Ursache: der
|
|
Erprobungshinweis ging auf, bevor das Hauptfenster gezeichnet war. Der modale
|
|
Dialog sperrte das Hauptfenster, und beide blieben unsichtbar.
|
|
|
|
WARUM DAS KEIN ANDERER PRUEFSTAND FINDET
|
|
----------------------------------------
|
|
Alle uebrigen Pruefstaende fahren das Programm aus der QUELLE. Dort ist das
|
|
Fenster sofort gezeichnet, der Fehler tritt nicht auf. Er gehoert zur gepackten
|
|
EXE - also muss genau die gestartet werden.
|
|
|
|
Und "der Prozess laeuft" beweist nichts: er lief ja. Geprueft wird deshalb, ob
|
|
Windows ein Fenster dieses Programms als SICHTBAR fuehrt.
|
|
|
|
ZWEI FALLEN, die hier schon zugeschnappt sind:
|
|
* Eine PyInstaller-Onefile-EXE ist zweistufig. Der gestartete Prozess packt
|
|
nur aus; das Fenster gehoert dem KIND. Wer nur die eigene PID betrachtet,
|
|
sieht nie ein Fenster.
|
|
* terminate() beendet nur den Auspacker. Das Kind laeuft weiter - und dessen
|
|
Einzelinstanz-Sperre haelt danach jeden weiteren Start ab. Es muss die
|
|
ganze Sippe beendet werden.
|
|
"""
|
|
import ctypes
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from ctypes import wintypes
|
|
|
|
HIER = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, HIER)
|
|
|
|
import version # noqa: E402
|
|
|
|
fehler = []
|
|
|
|
|
|
def pruefe(bedingung, text):
|
|
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
|
kod = sys.stdout.encoding or "utf-8"
|
|
print(zeile.encode(kod, "replace").decode(kod))
|
|
if not bedingung:
|
|
fehler.append(text)
|
|
|
|
|
|
stamm, endung = os.path.splitext(version.EXE_NAME)
|
|
EXE = os.path.join(HIER, "dist", f"{stamm}-{version.VERSION}{endung}")
|
|
|
|
if not os.path.exists(EXE):
|
|
print(f"UEBERSPRUNGEN | {os.path.basename(EXE)} ist nicht gebaut "
|
|
f"(vorher: python bauen.py)")
|
|
sys.exit(0)
|
|
|
|
u = ctypes.windll.user32
|
|
# Mit --test: dann geht das Fenster auf dem ZWEITEN Monitor auf und nicht
|
|
# mitten auf dem Hauptbildschirm, wo gerade jemand arbeitet. Seine Ansage am
|
|
# 07.09.2026 - und dieser Pruefstand war der eine, der sie noch nicht befolgte,
|
|
# weil er die gebaute EXE ganz normal startet. Sichtbar bleibt das Fenster,
|
|
# und genau das wird hier ja geprueft.
|
|
p = subprocess.Popen([EXE, "--test"])
|
|
print(f" (gestartet: {os.path.basename(EXE)}, PID {p.pid})")
|
|
|
|
RUECKRUF = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
|
gefunden = []
|
|
verwandte = {p.pid}
|
|
|
|
|
|
def sippe():
|
|
alle = {p.pid}
|
|
try:
|
|
e = subprocess.run(
|
|
["powershell", "-NoProfile", "-Command",
|
|
"Get-CimInstance Win32_Process | Where-Object "
|
|
f"{{ $_.ParentProcessId -eq {p.pid} }} | ForEach-Object {{ $_.ProcessId }}"],
|
|
capture_output=True, text=True, timeout=20)
|
|
for zeile in e.stdout.split():
|
|
if zeile.strip().isdigit():
|
|
alle.add(int(zeile.strip()))
|
|
except Exception: # noqa: BLE001 - dann bleibt es bei der eigenen PID
|
|
pass
|
|
return alle
|
|
|
|
|
|
def je_fenster(hwnd, _):
|
|
pid = wintypes.DWORD()
|
|
u.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
|
if pid.value not in verwandte:
|
|
return True
|
|
laenge = u.GetWindowTextLengthW(hwnd)
|
|
puffer = ctypes.create_unicode_buffer(laenge + 1)
|
|
u.GetWindowTextW(hwnd, puffer, laenge + 1)
|
|
r = wintypes.RECT()
|
|
u.GetWindowRect(hwnd, ctypes.byref(r))
|
|
gefunden.append((puffer.value, bool(u.IsWindowVisible(hwnd)),
|
|
r.right - r.left, r.bottom - r.top))
|
|
return True
|
|
|
|
|
|
ende = time.time() + 60
|
|
sichtbar = False
|
|
vorzeitig_weg = False
|
|
while time.time() < ende and not sichtbar:
|
|
if p.poll() is not None:
|
|
vorzeitig_weg = True
|
|
break
|
|
verwandte = sippe()
|
|
gefunden.clear()
|
|
u.EnumWindows(RUECKRUF(je_fenster), 0)
|
|
# Ein "Fenster" von wenigen Pixeln ist Verwaltung (IME, Notifier), kein
|
|
# Programmfenster. Deshalb die Groesse mitpruefen.
|
|
sichtbar = any(s and b > 300 and h > 300 for _, s, b, h in gefunden)
|
|
time.sleep(0.5)
|
|
|
|
for titel, s, b, h in gefunden:
|
|
print(f" {titel!r:42s} sichtbar={s} {b}x{h}")
|
|
|
|
pruefe(not vorzeitig_weg,
|
|
f"das Programm bleibt am Leben (Rueckgabe {p.returncode})"
|
|
if vorzeitig_weg else "das Programm bleibt am Leben")
|
|
if vorzeitig_weg and p.returncode == 0:
|
|
print(" (Rueckgabe 0 heisst meist: es laeuft schon eine Fassung. "
|
|
"Erst beenden, dann noch einmal.)")
|
|
|
|
pruefe(sichtbar, "ein sichtbares Programmfenster ist da")
|
|
pruefe(any(str(t).startswith(stamm) and s for t, s, _, _ in gefunden),
|
|
f"und es traegt den Programmnamen ({stamm})")
|
|
|
|
subprocess.run(["taskkill", "/PID", str(p.pid), "/T", "/F"], capture_output=True)
|
|
try:
|
|
p.wait(10)
|
|
except subprocess.TimeoutExpired:
|
|
p.kill()
|
|
|
|
if fehler:
|
|
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
|
|
sys.exit(1)
|
|
print("GRUEN | Die gebaute EXE zeigt ein Fenster")
|