Popups kommen nach Klick woanders / Alt+Tab wieder nach vorn; Büroklammer im Tipp des Tages
Windows-Build / windows-exe (push) Failing after 3m10s
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>
This commit is contained in:
1 parent
0bcc22d8c9
commit
9f05cab952
10 files changed
+417
-29
No files matched your search
@@ -0,0 +1,133 @@
|
||||
"""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
|
||||
Reference in new issue
Block a user