"""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 u.GetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int] u.GetWindowLongW.restype = ctypes.c_long u.SetWindowLongW.argtypes = [wintypes.HWND, ctypes.c_int, ctypes.c_long] u.SetWindowLongW.restype = ctypes.c_long u.ReleaseCapture.argtypes = [] u.ReleaseCapture.restype = wintypes.BOOL u.SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] u.SendMessageW.restype = wintypes.LPARAM u.PostMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM] u.PostMessageW.restype = wintypes.BOOL u.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int] u.ShowWindow.restype = wintypes.BOOL u.IsIconic.argtypes = [wintypes.HWND] u.IsIconic.restype = wintypes.BOOL u.FlashWindow.argtypes = [wintypes.HWND, wintypes.BOOL] u.FlashWindow.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 dunkle_titelleiste(fenster: tk.Misc) -> None: """Windows-Titelleiste dunkel (Design „dunkel“, ab Windows 10 20H1).""" h = hwnd(fenster) if not h: return try: import ctypes an = ctypes.c_int(1) ctypes.windll.dwmapi.DwmSetWindowAttribute(h, 20, ctypes.byref(an), ctypes.sizeof(an)) # IMMERSIVE_DARK_MODE except (OSError, AttributeError): pass 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) GWL_EXSTYLE, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW = -20, 0x00040000, 0x00000080 def hat_taskleisteneintrag(fenster: tk.Misc) -> bool: h = hwnd(fenster) if not h: return False stil = user32().GetWindowLongW(h, GWL_EXSTYLE) return bool(stil & WS_EX_APPWINDOW) and not (stil & WS_EX_TOOLWINDOW) def taskleisteneintrag(fenster: tk.Misc) -> bool: """Eigener Eintrag in Taskleiste/Alt+Tab (rahmenlose Tk-Fenster haben sonst keinen). Wirkt erst nach erneutem Einblenden – gibt True zurück, wenn das nötig ist.""" h = hwnd(fenster) if not h: return False stil = user32().GetWindowLongW(h, GWL_EXSTYLE) neu = (stil & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW if neu != stil: user32().SetWindowLongW(h, GWL_EXSTYLE, neu) return True return False def nativ_verschieben(fenster: tk.Misc) -> bool: """Windows übernimmt das Ziehen (flüssig, mit Andocken am Bildschirmrand). True bei Erfolg.""" h = hwnd(fenster) if not h: return False user32().ReleaseCapture() # PostMessage statt SendMessage: Die Verschiebe-Schleife von Windows darf nicht innerhalb eines # Tk-Ereignisses laufen (Tk führt darin Python-Ereignisse aus -> harter Absturz, siehe nativ.py). user32().PostMessageW(h, 0x00A1, 2, 0) # WM_NCLBUTTONDOWN, HTCAPTION return True def minimieren(fenster: tk.Misc) -> bool: """Direkt über Windows minimieren – Tk verweigert das bei rahmenlosen Fenstern. Eigene Popups (Dialoge) werden dabei von Windows mit ausgeblendet und beim Wiederherstellen mit eingeblendet.""" h = hwnd(fenster) if not h: return False user32().ShowWindow(h, 6) # SW_MINIMIZE return True def ist_minimiert(fenster: tk.Misc) -> bool: h = hwnd(fenster) return bool(user32().IsIconic(h)) if h else False def blinken(fenster: tk.Misc) -> None: h = hwnd(fenster) if h: user32().FlashWindow(h, True) 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