Popups kommen nach Klick woanders / Alt+Tab wieder nach vorn; Büroklammer im Tipp des Tages

Fehler: War ein Popup offen und man klickte in ein anderes Programm, ließ sich das Popup über
Hauptfenster, Taskleiste oder Alt+Tab nicht mehr nach vorn holen.
Ursache: Rahmenlose Tk-Fenster mit transient() legt Tk unter Windows als Kindfenster des Desktops
(WS_CHILD) an – ohne Besitzer, daher kam beim Zurückholen nur das (gesperrte) Hauptfenster nach vorn.

- Popups (Dialoge, Rückfragen, Rundgang) ohne transient(), stattdessen Windows-Besitzer +
  Sperre des aufrufenden Fensters wie bei echten modalen Windows-Dialogen (gui/win32.py)
- Rundgang: neue Sprechblase erscheint vor dem Entfernen der alten (sonst springt Windows weg)
- Alt+F4 und Modalsperre robust gegen Timing (Sperre wird bei Bedarf nachgeholt)
- Tests (nur unter Windows/Wine): Besitzer, Sperre, Stapelreihenfolge nach „woanders klicken“
  und Zurückholen, verschachtelte Rückfragen, Rundgang
- Tipp des Tages mit freigestelltem Büroklammer-Bild (build/make_tipp_bild.py), in der .exe
  enthalten und vom Selbsttest geprüft

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-11-02 11:26:17 +01:00
1 parent aae28fa7f2
commit 26ac90bb0f
10 files changed
+417 -29

No files matched your search

+110
View File
@@ -448,3 +448,113 @@ def test_hauptfenster_ablaeufe(lauf_mit_daten):
assert not lauf.uebrig, f"nicht ausgeführt: {lauf.uebrig}"
assert any(p["prozent"] == 80 for p in app.positionen)
lauf.pruefen()
# =========================================================================== Windows: Popups bleiben erreichbar
import sys # noqa: E402
nur_windows = pytest.mark.skipif(not sys.platform.startswith("win"),
reason="Win32-Fensterbesitz gibt es nur unter Windows (im Build: Wine)")
@nur_windows
def test_windows_popup_kommt_nach_alt_tab_wieder_nach_vorn(lauf_mit_daten):
"""Gemeldet: Popup offen, woanders hingeklickt – danach kam das Popup nicht mehr nach vorn
(auch nicht per Alt+Tab). Geprüft wird: Popup gehört dem Hauptfenster, das Hauptfenster ist
gesperrt, und holt man das Hauptfenster zurück, liegt das Popup darüber."""
from provtool.gui import win32
from provtool.gui.dialogs import InfoDialog
lauf = lauf_mit_daten
m = {}
def messen():
d = lauf.dialog(InfoDialog)
fremd = tk.Toplevel(lauf.root) # steht für ein anderes Programm
fremd.geometry("400x300+50+50")
fremd.update()
m.update(besitzer=win32.besitzer_von(d), haupt=win32.hwnd(lauf.root),
haupt_bedienbar=win32.ist_bedienbar(lauf.root))
win32.nach_vorn(fremd) # „woanders hingeklickt“
lauf.root.update()
m["woanders"] = win32.stapel(d, lauf.root, fremd)
win32.nach_vorn(lauf.root) # Alt+Tab / Taskleiste zurück zu uns
lauf.root.update()
m["zurueck"] = win32.stapel(d, lauf.root, fremd)
fremd.destroy()
d.abbrechen()
lauf.ablauf([
(IMMER, lambda: InfoDialog(lauf.root).zeigen(), "Info öffnen"),
(lambda: lauf.dialog(InfoDialog) is not None, lambda: lauf.root.after(150, messen), "messen"),
(lambda: "zurueck" in m and lauf.dialog(InfoDialog) is None, lambda: None, "geschlossen"),
], sekunden=10)
assert not lauf.uebrig, lauf.uebrig
assert m["besitzer"] == m["haupt"], "Popup gehört nicht dem Hauptfenster"
assert m["haupt_bedienbar"] is False, "Hauptfenster muss gesperrt sein, solange das Popup offen ist"
assert m["woanders"][0] == 2, f"Testaufbau: das fremde Fenster sollte oben liegen ({m['woanders']})"
assert m["zurueck"][:2] == [0, 1], f"Popup liegt nach dem Zurückholen nicht über dem Hauptfenster: {m['zurueck']}"
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Schließen nicht wieder freigegeben"
lauf.pruefen()
@nur_windows
def test_windows_verschachtelte_rueckfrage(lauf_mit_daten):
"""Händler-Dialog → Rückfrage: Rückfrage gehört dem Händler-Dialog; beim Schließen der Reihe nach frei."""
from provtool.gui import win32
from provtool.gui import win95 as w
from provtool.gui.dialogs import HaendlerDialog
lauf = lauf_mit_daten
m = {}
rueckfrage = lambda: next((d for d in lauf.dialoge() if d.title() == "Test-Rückfrage"), None) # noqa: E731
def messen_innen():
h, r = lauf.dialog(HaendlerDialog), rueckfrage()
m.update(r_besitzer=win32.besitzer_von(r), h=win32.hwnd(h), h_an=win32.ist_bedienbar(h),
haupt_an=win32.ist_bedienbar(lauf.root))
r.destroy()
def messen_aussen():
h = lauf.dialog(HaendlerDialog)
m.update(h_an_danach=win32.ist_bedienbar(h), haupt_an_danach=win32.ist_bedienbar(lauf.root))
h.abbrechen()
lauf.ablauf([
(IMMER, lauf.app.haendler, "Händler-Dialog öffnen"),
(lambda: lauf.dialog(HaendlerDialog) is not None,
lambda: w.info(lauf.dialog(HaendlerDialog), "Test-Rückfrage", "?"), "Rückfrage öffnen"),
(lambda: rueckfrage() is not None, lambda: lauf.root.after(100, messen_innen), "innen messen"),
(lambda: "h" in m and rueckfrage() is None, messen_aussen, "außen messen"),
(lambda: lauf.dialog(HaendlerDialog) is None, lambda: None, "alles zu"),
], sekunden=12)
assert not lauf.uebrig, lauf.uebrig
assert m["r_besitzer"] == m["h"], "Rückfrage gehört nicht dem Händler-Dialog"
assert m["h_an"] is False and m["haupt_an"] is False
assert m["h_an_danach"] is True and m["haupt_an_danach"] is False
assert win32.ist_bedienbar(lauf.root)
lauf.pruefen()
@nur_windows
def test_windows_rundgang_sperrt_und_gibt_frei(lauf_mit_daten):
from provtool.gui import win32
lauf = lauf_mit_daten
rg = lambda: getattr(lauf.app, "_rundgang", None) # noqa: E731
m = {}
def messen_und_weiter():
m.setdefault("gesperrt", []).append(not win32.ist_bedienbar(lauf.root))
m.setdefault("besitzer", []).append(win32.besitzer_von(rg().blase) == win32.hwnd(lauf.root))
rg()._weiter()
schritte = [(IMMER, lauf.app.rundgang, "Rundgang starten")]
for i in range(3):
schritte.append((lambda i=i: rg() is not None and rg().blase is not None and rg().nr == i,
messen_und_weiter, f"Schritt {i + 1}"))
schritte.append((IMMER, lambda: rg().beenden(), "Rundgang beenden"))
lauf.ablauf(schritte, sekunden=12)
assert not lauf.uebrig, lauf.uebrig
assert all(m["gesperrt"]), "Hauptfenster während des Rundgangs nicht gesperrt"
assert all(m["besitzer"]), "Sprechblase gehört nicht dem Hauptfenster"
assert win32.ist_bedienbar(lauf.root), "Hauptfenster nach dem Rundgang nicht freigegeben"
lauf.pruefen()