Files
provisionsmanager95/provtool/gui/win32.py
T
adminandClaude Opus 5.5 dcb1d0378c Eigene Kennungen ausblenden, Win95-Menüs, verschiebbares Hauptfenster bei offenem Dialog u. a.
Neu:
- Eigene Kennungen (eigene Filiale) ausblenden: nicht angezeigt, nicht mitgerechnet, nicht
  exportiert; per Rechtsklick, Händler-Menü oder Import-Bericht; Händler-Zuordnung hat Vorrang
- Menüs im Win95-Stil: wechseln beim Überfahren der Menüleiste, Tastatur, Kontextmenü gleich
- Dialoge sperren nur den Inhalt: Hauptfenster bleibt verschiebbar/maximierbar; Dialoge haben
  eigenen Taskleisten-Eintrag; Klick auf gesperrten Inhalt holt den Dialog nach vorn (blinkt)
- Trennlinie zwischen Händler- und Positionsliste sichtbar und ziehbar, Position wird gemerkt
- Strg+Rücktaste / Strg+Entf löschen ganze Wörter in allen Eingabefeldern

Behoben:
- Gedrückte Knöpfe zeigten keine Beschriftung (activeForeground gehört in Tk zur Klasse Background)
- Tipp des Tages: Text lief aus dem sichtbaren Bereich
- Größenänderung: breiterer Griffbereich mit Ecken, Zeiger wird zuverlässig zurückgesetzt,
  flüssiger; Verschieben über Windows selbst (inkl. Andocken), Minimieren über Windows
- Zeichen, die MS Sans Serif nicht hat (…, ▸, →, •), durch Win95-übliche ersetzt
- Menü/Tooltip/Dialog: Größe+Position zusammen setzen (Tk ignoriert sonst die Position)
- Suche reagiert erst nach kurzer Tipp-Pause (flüssigeres Tippen)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 12:58:18 +01:00

204 lines
7.2 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.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()
user32().SendMessageW(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