Windows-Build / windows-exe (push) Successful in 5m30s
- Verschieben/Größe ziehen per PostMessage statt SendMessage: Die Windows-Verschiebeschleife lief innerhalb eines Tk-Ereignisses; Tk führte darin Python-Ereignisse (<Configure>) aus -> harter Absturz „PyEval_RestoreThread … thread state is NULL“ (normal und aus maximiert). - Größen-Mauszeiger am Rand wieder von Tk (blieb sonst im Fenster stehen); Windows zieht beim Drücken. - Neuer Maustest im Build (xdotool, echte Maus): Titelleiste/Rand ziehen, maximieren, herausziehen. - Export: Bei Vodafone steht in Spalte A das Aktivierungsdatum (statt Abrechnungslauf). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
208 lines
7.5 KiB
Python
208 lines
7.5 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
|
||
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 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
|