Windows-Build / windows-exe (push) Failing after 3m10s
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>
134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
"""Win32-Hilfen für modale Fenster (nur unter Windows wirksam, sonst ohne Funktion).
|
||
|
||
Wichtig: Rahmenlose Fenster NICHT zusätzlich mit Tk `transient()` versehen – Tk legt sie dann als
|
||
Kindfenster des Desktops (WS_CHILD) an, und denen kann Windows keinen Besitzer geben.
|
||
|
||
Rahmenlose Tk-Fenster (overrideredirect) gehören für Windows zu keinem anderen Fenster. Holt man das
|
||
Programm per Taskleiste oder Alt+Tab zurück, kommt dann nur das Hauptfenster nach vorn – ein offener
|
||
Dialog bliebe dahinter verborgen. Deshalb machen wir es wie Windows bei echten modalen Dialogen:
|
||
der Dialog bekommt das aufrufende Fenster als *Besitzer*, und der Besitzer wird solange *deaktiviert*.
|
||
Windows bringt dann bei Klick aufs Hauptfenster, Taskleiste und Alt+Tab automatisch den Dialog nach vorn.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import tkinter as tk
|
||
|
||
IS_WIN = sys.platform.startswith("win")
|
||
GWLP_HWNDPARENT = -8
|
||
GW_OWNER = 4
|
||
|
||
_u32 = None
|
||
|
||
|
||
def user32():
|
||
"""user32 mit korrekten 64-Bit-Signaturen (Fensterhandles dürfen nicht abgeschnitten werden)."""
|
||
global _u32
|
||
if _u32 is None:
|
||
import ctypes
|
||
from ctypes import wintypes
|
||
u = ctypes.WinDLL("user32", use_last_error=True)
|
||
u.GetParent.argtypes = [wintypes.HWND]
|
||
u.GetParent.restype = wintypes.HWND
|
||
u.SetWindowLongPtrW.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_void_p]
|
||
u.SetWindowLongPtrW.restype = ctypes.c_void_p
|
||
u.EnableWindow.argtypes = [wintypes.HWND, wintypes.BOOL]
|
||
u.EnableWindow.restype = wintypes.BOOL
|
||
u.IsWindowEnabled.argtypes = [wintypes.HWND]
|
||
u.IsWindowEnabled.restype = wintypes.BOOL
|
||
u.GetWindow.argtypes = [wintypes.HWND, wintypes.UINT]
|
||
u.GetWindow.restype = wintypes.HWND
|
||
u.GetLastActivePopup.argtypes = [wintypes.HWND]
|
||
u.GetLastActivePopup.restype = wintypes.HWND
|
||
u.SetForegroundWindow.argtypes = [wintypes.HWND]
|
||
u.SetForegroundWindow.restype = wintypes.BOOL
|
||
u.IsWindow.argtypes = [wintypes.HWND]
|
||
u.IsWindow.restype = wintypes.BOOL
|
||
u.GetAncestor.argtypes = [wintypes.HWND, wintypes.UINT]
|
||
u.GetAncestor.restype = wintypes.HWND
|
||
u.GetTopWindow.argtypes = [wintypes.HWND]
|
||
u.GetTopWindow.restype = wintypes.HWND
|
||
u.BringWindowToTop.argtypes = [wintypes.HWND]
|
||
u.BringWindowToTop.restype = wintypes.BOOL
|
||
_u32 = u
|
||
return _u32
|
||
|
||
|
||
def hwnd(fenster: tk.Misc) -> int | None:
|
||
"""Handle des äußeren (obersten) Windows-Fensters eines Tk-Toplevels."""
|
||
if not IS_WIN:
|
||
return None
|
||
try:
|
||
return user32().GetAncestor(fenster.winfo_toplevel().winfo_id(), 2) # GA_ROOT
|
||
except (tk.TclError, OSError):
|
||
return None
|
||
|
||
|
||
def stapel(*fenster: tk.Misc) -> list[int]:
|
||
"""Reihenfolge der übergebenen Fenster im Windows-Fensterstapel, oberstes zuerst (Indizes)."""
|
||
ziele = [hwnd(f) for f in fenster]
|
||
out, w = [], user32().GetTopWindow(None)
|
||
while w:
|
||
if w in ziele:
|
||
out.append(ziele.index(w))
|
||
w = user32().GetWindow(w, 2) # GW_HWNDNEXT
|
||
return out
|
||
|
||
|
||
def nach_vorn(fenster: tk.Misc) -> None:
|
||
h = hwnd(fenster)
|
||
if h:
|
||
user32().SetForegroundWindow(h)
|
||
user32().BringWindowToTop(h)
|
||
|
||
|
||
def besitzer_setzen(fenster: tk.Misc, besitzer: tk.Misc) -> None:
|
||
h, b = hwnd(fenster), hwnd(besitzer)
|
||
if h and b:
|
||
user32().SetWindowLongPtrW(h, GWLP_HWNDPARENT, b)
|
||
|
||
|
||
def besitzer_von(fenster: tk.Misc) -> int | None:
|
||
h = hwnd(fenster)
|
||
return user32().GetWindow(h, GW_OWNER) if h else None
|
||
|
||
|
||
def bedienbar(fenster: tk.Misc, an: bool) -> None:
|
||
h = hwnd(fenster)
|
||
if h and user32().IsWindow(h):
|
||
user32().EnableWindow(h, bool(an))
|
||
|
||
|
||
def ist_bedienbar(fenster: tk.Misc) -> bool:
|
||
h = hwnd(fenster)
|
||
return bool(user32().IsWindowEnabled(h)) if h else True
|
||
|
||
|
||
def letztes_popup(fenster: tk.Misc) -> int | None:
|
||
h = hwnd(fenster)
|
||
return user32().GetLastActivePopup(h) if h else None
|
||
|
||
|
||
class Modal:
|
||
"""Macht `fenster` modal zu `besitzer` (Windows-Art). beenden() muss VOR dem Zerstören des
|
||
Fensters laufen: erst Besitzer wieder freigeben, dann schließen – sonst aktiviert Windows
|
||
beim Schließen ein fremdes Programm statt unseres Hauptfensters."""
|
||
|
||
def __init__(self, fenster: tk.Misc, besitzer: tk.Misc, sperren: bool = True):
|
||
self.besitzer = besitzer
|
||
self.gesperrt = False
|
||
if not IS_WIN:
|
||
return
|
||
besitzer_setzen(fenster, besitzer)
|
||
if sperren and ist_bedienbar(besitzer):
|
||
bedienbar(besitzer, False)
|
||
self.gesperrt = True
|
||
|
||
def beenden(self) -> None:
|
||
if self.gesperrt:
|
||
self.gesperrt = False
|
||
try:
|
||
bedienbar(self.besitzer, True)
|
||
except tk.TclError:
|
||
pass
|