Files

216 lines
7.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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
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