Absturz beim Ziehen des Fensters behoben, Zeiger am Rand wieder über Tk, Vodafone-Datum in Spalte A

- 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>
This commit is contained in:
Kerim AtilganandClaude Opus 5.5 committed 2026-11-02 18:03:33 +01:00
1 parent 0880ab2d42
commit 97c73b82c7
11 files changed
+166 -71

No files matched your search

+22 -48
View File
@@ -11,11 +11,17 @@ Größenänderungen also weiter ohne Rahmenaufschlag (sonst würde das Fenster b
Größenanpassung um die Rahmenbreite wachsen). Windows erledigt alles Weitere selbst:
- Ziehen an der Titelleiste (WM_NCLBUTTONDOWN/HTCAPTION) mit Andocken links/rechts/oben (Maximieren)
- Win+Pfeiltasten, Doppelklick, Systemmenü (Alt+Leertaste), Taskleiste, Alt+Tab
- Größe ändern: Am Rand meldet das Fenster per WM_NCHITTEST „linker Rand“ usw. -> Windows zeigt den
passenden Mauszeiger und zieht die Größe selbst (flüssig, kein hängender Zeiger).
- Größe ändern: Den Größen-Mauszeiger am Rand zeigt Tk (rahmen.py) – so weiß Tk immer, welcher Zeiger
gilt, und setzt ihn beim Zurückfahren in das Fenster wieder richtig. Beim Drücken auf den Rand zieht
Windows die Größe (flüssig).
WICHTIG: Die Fensterprozeduren hier laufen mitten in Tk. Sie dürfen Tk NICHT aufrufen (Tk-Sperre ->
Verklemmung), nur Win32-Funktionen.
WICHTIG:
- Die Fensterprozeduren hier laufen mitten in Tk. Sie dürfen Tk NICHT aufrufen (Tk-Sperre ->
Verklemmung), nur Win32-Funktionen.
- Verschieben/Größe ziehen NIE synchron (SendMessage) aus einem Tk-Ereignis heraus starten: Windows
läuft dann in einer eigenen Schleife, in der Tk Python-Ereignisse (z. B. <Configure>) ausführt,
während Python das nicht erwartet -> harter Absturz („PyEval_RestoreThread … thread state is
NULL“). Deshalb PostMessage: Die Schleife startet, sobald Tk wieder in seiner Ereignisschleife ist.
"""
from __future__ import annotations
@@ -27,15 +33,13 @@ from ctypes import wintypes
IS_WIN = sys.platform.startswith("win")
WM_NCDESTROY, WM_NCCALCSIZE, WM_NCHITTEST, WM_NCACTIVATE = 0x0082, 0x0083, 0x0084, 0x0086
WM_NCLBUTTONDOWN, WM_SYSCOMMAND, WM_SETCURSOR = 0x00A1, 0x0112, 0x0020
HTCLIENT, HTCAPTION, HTTRANSPARENT = 1, 2, -1
WM_NCLBUTTONDOWN, WM_SYSCOMMAND = 0x00A1, 0x0112
HTCLIENT, HTCAPTION = 1, 2
HT = {"w": 10, "e": 11, "n": 12, "nw": 13, "ne": 14, "s": 15, "sw": 16, "se": 17}
GWL_STYLE, GWL_EXSTYLE = -16, -20
WS_CAPTION, WS_THICKFRAME, WS_SYSMENU = 0x00C00000, 0x00040000, 0x00080000
WS_MINIMIZEBOX, WS_MAXIMIZEBOX = 0x00020000, 0x00010000
WS_EX_TOOLWINDOW, WS_EX_APPWINDOW = 0x00000080, 0x00040000
ZEIGER = {"n": 32645, "s": 32645, "w": 32644, "e": 32644, "nw": 32642, "se": 32642, "ne": 32643, "sw": 32643}
HT_RAND = {v: k for k, v in HT.items()}
SWP_NOSIZE, SWP_NOMOVE, SWP_NOZORDER, SWP_NOACTIVATE, SWP_FRAMECHANGED = 0x1, 0x2, 0x4, 0x10, 0x20
SC_MAXIMIZE, SC_RESTORE, SC_MINIMIZE = 0xF030, 0xF120, 0xF020
@@ -73,9 +77,7 @@ def _api():
("GetMonitorInfoW", [wintypes.HMONITOR, ctypes.POINTER(MONITORINFO)], wintypes.BOOL),
("ReleaseCapture", [], wintypes.BOOL), ("SendMessageW", [H, U, W, L], LRESULT),
("PostMessageW", [H, U, W, L], wintypes.BOOL), ("ShowWindow", [H, ctypes.c_int], wintypes.BOOL),
("WindowFromPoint", [wintypes.POINT], H),
("LoadCursorW", [wintypes.HINSTANCE, ctypes.c_void_p], wintypes.HANDLE),
("SetCursor", [wintypes.HANDLE], wintypes.HANDLE)):
("WindowFromPoint", [wintypes.POINT], H)):
f = getattr(u, name)
f.argtypes, f.restype = args, res
c.SetWindowSubclass.argtypes = [H, SUBCLASSPROC, ctypes.c_size_t, ctypes.c_size_t]
@@ -109,11 +111,7 @@ def arbeitsbereich_von(rect: wintypes.RECT) -> wintypes.RECT | None:
class NativerRahmen:
"""Macht das Tk-Hauptfenster zu einem echten Windows-Fenster ohne sichtbaren Windows-Rahmen.
rand_widgets: Tk-Widgets, die am Fensterrand liegen (deren Randbereich wird für die Maus
„durchsichtig“, damit Windows dort den Größenrahmen erkennt).
"""
"""Macht das Tk-Hauptfenster zu einem echten Windows-Fenster ohne sichtbaren Windows-Rahmen."""
def __init__(self, root: tk.Tk, rand: int, ecke: int):
self.root = root
@@ -121,11 +119,10 @@ class NativerRahmen:
self.hwnd = None
self.aktiv = False
self._procs: dict[int, object] = {} # Referenzen halten, sonst räumt Python die Callbacks weg
self._rand_hwnds: set[int] = set()
# ------------------------------------------------------------------ Einrichten
def einrichten(self, rand_widgets: list[tk.Misc]) -> bool:
"""Nach dem ersten Anzeigen aufrufen (vorher gibt es das äußere Windows-Fenster noch nicht)."""
def einrichten(self) -> bool:
"""Sobald Tk das äußere Windows-Fenster angelegt hat (möglichst vor dem ersten Anzeigen)."""
if self.aktiv:
return True
try:
@@ -142,10 +139,6 @@ class NativerRahmen:
u.SetWindowLongW(h, GWL_EXSTYLE, (ex & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW)
if not self._unterklasse(h, self._aussen_proc):
return False
for wdg in rand_widgets:
hw = wdg.winfo_id()
if self._unterklasse(hw, self._rand_proc):
self._rand_hwnds.add(hw)
self.aktiv = True
# Rahmen neu berechnen lassen (jetzt: unsichtbar)
u.SetWindowPos(h, None, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
@@ -167,10 +160,9 @@ class NativerRahmen:
"""Hat Tk das äußere Fenster neu angelegt (z. B. nach „wm resizable“)? Dann neu einrichten."""
return self.aktiv and self.aeusseres_fenster() not in (None, self.hwnd)
def neu_einrichten(self, rand_widgets: list[tk.Misc]) -> bool:
def neu_einrichten(self) -> bool:
self.aktiv = False
self._rand_hwnds.clear()
return self.einrichten(rand_widgets)
return self.einrichten()
def _unterklasse(self, h: int, fn) -> bool:
_, c = _api()
@@ -229,27 +221,12 @@ class NativerRahmen:
if rand:
return HT[rand]
return HTCLIENT
if msg == WM_SETCURSOR and (lparam & 0xFFFF) in HT_RAND:
u = _api()[0]
u.SetCursor(u.LoadCursorW(None, ZEIGER[HT_RAND[lparam & 0xFFFF]]))
return 1
if msg == WM_NCDESTROY:
self._entfernen(hwnd)
except Exception: # niemals eine Ausnahme in die Fensterprozedur durchlassen
pass
return c.DefSubclassProc(hwnd, msg, wparam, lparam)
def _rand_proc(self, hwnd, msg, wparam, lparam, _id, _ref):
_, c = _api()
try:
if msg == WM_NCHITTEST and self.randbereich(*_xy(lparam)):
return HTTRANSPARENT # Maus „fällt durch“ zum äußeren Fenster -> Größenrahmen
if msg == WM_NCDESTROY:
self._entfernen(hwnd)
except Exception:
pass
return c.DefSubclassProc(hwnd, msg, wparam, lparam)
def _entfernen(self, hwnd):
proc = self._procs.pop(hwnd, None)
if proc is not None:
@@ -262,15 +239,16 @@ class NativerRahmen:
# ------------------------------------------------------------------ Bedienung
def verschieben(self) -> None:
"""Titelleiste gedrückt: Windows zieht das Fenster (mit Andocken an den Bildschirmrand)."""
"""Titelleiste gedrückt: Windows zieht das Fenster (mit Andocken an den Bildschirmrand).
PostMessage, nicht SendMessage – siehe Modulbeschreibung."""
u, _ = _api()
u.ReleaseCapture()
u.SendMessageW(self.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0)
u.PostMessageW(self.hwnd, WM_NCLBUTTONDOWN, HTCAPTION, 0)
def groesse_ziehen(self, rand: str) -> None:
u, _ = _api()
u.ReleaseCapture()
u.SendMessageW(self.hwnd, WM_NCLBUTTONDOWN, HT[rand], 0)
u.PostMessageW(self.hwnd, WM_NCLBUTTONDOWN, HT[rand], 0)
def maximiert(self) -> bool:
return bool(self.hwnd and _api()[0].IsZoomed(self.hwnd))
@@ -298,10 +276,6 @@ class NativerRahmen:
u, _ = _api()
return u.GetWindowLongW(self.hwnd, GWL_STYLE), u.GetWindowLongW(self.hwnd, GWL_EXSTYLE)
def fenster_bei(self, x: int, y: int) -> int | None:
"""Für Tests: Welches Fenster bekommt die Maus am Bildschirmpunkt (berücksichtigt HTTRANSPARENT)?"""
return _api()[0].WindowFromPoint(wintypes.POINT(x, y))
def trefferpruefung(self, hwnd: int, x: int, y: int) -> int:
"""Für Tests: Was meldet das Fenster `hwnd` für den Bildschirmpunkt (WM_NCHITTEST)?"""
lp = ((y & 0xFFFF) << 16) | (x & 0xFFFF)