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:
1 parent
0880ab2d42
commit
97c73b82c7
11 files changed
+165
-70
No files matched your search
@@ -23,7 +23,7 @@ jobs:
|
|||||||
- name: Werkzeuge für Checkout/Upload installieren
|
- name: Werkzeuge für Checkout/Upload installieren
|
||||||
run: |
|
run: |
|
||||||
apt-get update -qq
|
apt-get update -qq
|
||||||
apt-get install -y -qq --no-install-recommends git nodejs ca-certificates >/dev/null
|
apt-get install -y -qq --no-install-recommends git nodejs ca-certificates xdotool >/dev/null
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ echo "== Tests inkl. Oberfläche (Windows-Python unter Wine) =="
|
|||||||
# -v: im Log ist live sichtbar, welcher Test läuft; timeout: Obergrenze für alle Tests zusammen
|
# -v: im Log ist live sichtbar, welcher Test läuft; timeout: Obergrenze für alle Tests zusammen
|
||||||
PROVTOOL_GUI_TESTS=1 timeout 900 $PY -m pytest -v -p no:cacheprovider
|
PROVTOOL_GUI_TESTS=1 timeout 900 $PY -m pytest -v -p no:cacheprovider
|
||||||
|
|
||||||
|
echo "== Maustest (echte Maus: Fenster ziehen, Rand ziehen, maximiert ziehen) =="
|
||||||
|
PY="$PY" timeout 300 bash build/maustest.sh
|
||||||
|
|
||||||
echo "== Version =="
|
echo "== Version =="
|
||||||
VERSION=$($PY build/version_info.py | tr -d '\r')
|
VERSION=$($PY build/version_info.py | tr -d '\r')
|
||||||
echo "Version $VERSION"
|
echo "Version $VERSION"
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
"""Für build/maustest.sh: startet die App und schreibt laufend Lage und Zustand des Hauptfensters.
|
||||||
|
|
||||||
|
Kein pytest-Test – wird im Windows-Build mit echter Maus (xdotool) bedient. Hintergrund: v1.3.0 stürzte
|
||||||
|
beim Ziehen an der Titelleiste hart ab; das sieht man nur mit echter Maus.
|
||||||
|
"""
|
||||||
|
import faulthandler
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import tkinter as tk
|
||||||
|
|
||||||
|
aus = sys.argv[1]
|
||||||
|
faulthandler.enable(open(os.path.join(aus, "absturz.txt"), "w"))
|
||||||
|
home = tempfile.mkdtemp()
|
||||||
|
os.environ["PROVTOOL_HOME"] = home
|
||||||
|
with open(os.path.join(home, "einstellungen.json"), "w") as f:
|
||||||
|
json.dump({"assistent_erledigt": True, "tipps_beim_start": False, "fenster": "900x560+100+100"}, f)
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
from provtool.gui import app as A # noqa: E402
|
||||||
|
|
||||||
|
root = tk.Tk()
|
||||||
|
a = A.App(root)
|
||||||
|
fehler = []
|
||||||
|
root.report_callback_exception = lambda *e: fehler.append(repr(e[1]))
|
||||||
|
|
||||||
|
|
||||||
|
def melden():
|
||||||
|
with open(os.path.join(aus, "status.txt"), "a") as f:
|
||||||
|
f.write(f"{root.winfo_x()} {root.winfo_y()} {root.winfo_width()} {root.winfo_height()} "
|
||||||
|
f"{int(a.rahmen.maximiert)} {len(fehler)}\n")
|
||||||
|
root.after(250, melden)
|
||||||
|
|
||||||
|
|
||||||
|
root.after(250, melden)
|
||||||
|
root.mainloop()
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Echte Maus im virtuellen Bildschirm (xdotool): Fenster an der Titelleiste ziehen, am Rand die Größe
|
||||||
|
# ziehen, maximieren und aus „maximiert“ herausziehen. v1.3.0 stürzte dabei hart ab – das sieht nur
|
||||||
|
# ein Test mit echter Maus (Windows-Verschiebeschleife). Aufruf aus build_windows.sh (DISPLAY gesetzt).
|
||||||
|
set -uo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
PY="${PY:-wine python}"
|
||||||
|
AUS="$(mktemp -d)"
|
||||||
|
command -v xdotool >/dev/null || { apt-get update -qq >/dev/null && apt-get install -y -qq xdotool >/dev/null; }
|
||||||
|
command -v xdotool >/dev/null || { echo "xdotool fehlt"; exit 1; }
|
||||||
|
|
||||||
|
$PY build/maus_app.py "$(winepath -w "$AUS" 2>/dev/null || echo "$AUS")" > "$AUS/app.log" 2>&1 &
|
||||||
|
PID=$!
|
||||||
|
for i in $(seq 1 120); do [ -s "$AUS/status.txt" ] && break; sleep 0.5; done
|
||||||
|
sleep 3
|
||||||
|
lage() { tail -1 "$AUS/status.txt" | tr -d '\r'; }
|
||||||
|
lebt() { kill -0 "$PID" 2>/dev/null; }
|
||||||
|
fehler=0
|
||||||
|
pruefe() { # $1 = Schritt
|
||||||
|
sleep 1.5
|
||||||
|
if ! lebt; then echo "ABSTURZ bei: $1"; fehler=1; return 1; fi
|
||||||
|
echo "$1: $(lage)"
|
||||||
|
}
|
||||||
|
ziehen() { # x y dx dy
|
||||||
|
xdotool mousemove "$1" "$2"; sleep 0.4; xdotool mousedown 1; sleep 0.3
|
||||||
|
for i in 1 2 3 4 5 6; do xdotool mousemove_relative -- "$(( $3 / 6 ))" "$(( $4 / 6 ))"; sleep 0.15; done
|
||||||
|
sleep 0.2; xdotool mouseup 1
|
||||||
|
}
|
||||||
|
|
||||||
|
read -r X Y W H M F <<< "$(lage)"
|
||||||
|
echo "Start: $X $Y $W $H maximiert=$M"
|
||||||
|
ziehen $(( X + 300 )) $(( Y + 14 )) 60 42; pruefe "Titelleiste ziehen" || true
|
||||||
|
read -r X2 Y2 W2 H2 M2 F2 <<< "$(lage)"
|
||||||
|
[ "$X2" -ne "$X" ] || [ "$Y2" -ne "$Y" ] || { echo "Fenster hat sich nicht bewegt"; fehler=1; }
|
||||||
|
ziehen $(( X2 + W2 - 2 )) $(( Y2 + H2 / 2 )) 60 0; pruefe "Rand ziehen" || true
|
||||||
|
read -r X3 Y3 W3 H3 M3 F3 <<< "$(lage)"
|
||||||
|
[ "$W3" -gt "$W2" ] || { echo "Größe am Rand nicht geändert ($W2 -> $W3)"; fehler=1; }
|
||||||
|
xdotool mousemove $(( X3 + 300 )) $(( Y3 + 14 )); sleep 0.3
|
||||||
|
xdotool click --repeat 2 --delay 60 1; pruefe "Doppelklick (maximieren)" || true
|
||||||
|
read -r X4 Y4 W4 H4 M4 F4 <<< "$(lage)"
|
||||||
|
[ "$M4" = "1" ] || { echo "nicht maximiert"; fehler=1; }
|
||||||
|
# Unter echtem Windows wird das Fenster dabei wiederhergestellt, unter Wine nicht – hier zählt: kein Absturz
|
||||||
|
ziehen $(( X4 + 400 )) $(( Y4 + 12 )) 0 150; pruefe "aus maximiert herausziehen" || true
|
||||||
|
|
||||||
|
if lebt; then kill "$PID" 2>/dev/null; fi
|
||||||
|
sleep 1
|
||||||
|
if [ -s "$AUS/absturz.txt" ]; then echo "== Absturzprotokoll:"; cat "$AUS/absturz.txt"; fehler=1; fi
|
||||||
|
if grep -q "Fatal Python error" "$AUS/app.log"; then cat "$AUS/app.log"; fehler=1; fi
|
||||||
|
read -r _ _ _ _ _ FE <<< "$(lage)"
|
||||||
|
[ "${FE:-0}" = "0" ] || { echo "Fehler in Tk-Callbacks: $FE"; fehler=1; }
|
||||||
|
[ "$fehler" = 0 ] && echo "Maustest bestanden" || echo "Maustest FEHLGESCHLAGEN"
|
||||||
|
exit "$fehler"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach."""
|
"""Provisions-Manager 95 – Provisionsabrechnung für Unterhändler der Handy-Insel Miesbach."""
|
||||||
|
|
||||||
__version__ = "1.3.0"
|
__version__ = "1.3.1"
|
||||||
APP_NAME = "Provisions-Manager 95"
|
APP_NAME = "Provisions-Manager 95"
|
||||||
APP_ORG = "Handy-Insel Miesbach"
|
APP_ORG = "Handy-Insel Miesbach"
|
||||||
+11
-1
@@ -45,6 +45,14 @@ def dateiname(text: str) -> str:
|
|||||||
AIRTIME_TEXT = "Airtime"
|
AIRTIME_TEXT = "Airtime"
|
||||||
|
|
||||||
|
|
||||||
|
def spalte_a(p: dict):
|
||||||
|
"""Spalte A: bei freenet der Zeitraum, bei Vodafone das Aktivierungsdatum (dort steht als Zeitraum
|
||||||
|
nur der Abrechnungslauf wie „VF2609“ – kein Datum)."""
|
||||||
|
if p.get("anbieter") == "vodafone" and p.get("datum"):
|
||||||
|
return date.fromisoformat(p["datum"])
|
||||||
|
return p.get("zeitraum") or None
|
||||||
|
|
||||||
|
|
||||||
def airtime_zeile(airtime: list[dict]) -> dict:
|
def airtime_zeile(airtime: list[dict]) -> dict:
|
||||||
"""Eine Zeile „Airtime = Summe“ für die Abrechnung."""
|
"""Eine Zeile „Airtime = Summe“ für die Abrechnung."""
|
||||||
zeitraeume = sorted({p.get("zeitraum") or "" for p in airtime} - {""})
|
zeitraeume = sorted({p.get("zeitraum") or "" for p in airtime} - {""})
|
||||||
@@ -76,7 +84,7 @@ def schreibe_xlsx(ziel: str | Path, positionen: list[dict], *, kopfzeile: bool =
|
|||||||
erste = zeile
|
erste = zeile
|
||||||
for p in sortiert(positionen) + list(schluss or []):
|
for p in sortiert(positionen) + list(schluss or []):
|
||||||
werte = [
|
werte = [
|
||||||
p.get("zeitraum") or None,
|
spalte_a(p),
|
||||||
p.get("provisionsart") or None,
|
p.get("provisionsart") or None,
|
||||||
_zahl_oder_text(p.get("kennung")),
|
_zahl_oder_text(p.get("kennung")),
|
||||||
date.fromisoformat(p["datum"]) if p.get("datum") else None,
|
date.fromisoformat(p["datum"]) if p.get("datum") else None,
|
||||||
@@ -95,6 +103,8 @@ def schreibe_xlsx(ziel: str | Path, positionen: list[dict], *, kopfzeile: bool =
|
|||||||
cell = ws.cell(zeile, c, v)
|
cell = ws.cell(zeile, c, v)
|
||||||
cell.font = bold if p.get("_summe") and c in (2, 11) else font
|
cell.font = bold if p.get("_summe") and c in (2, 11) else font
|
||||||
ws.cell(zeile, 4).number_format = "DD.MM.YYYY"
|
ws.cell(zeile, 4).number_format = "DD.MM.YYYY"
|
||||||
|
if isinstance(werte[0], date):
|
||||||
|
ws.cell(zeile, 1).number_format = "DD.MM.YYYY"
|
||||||
ws.cell(zeile, 5).number_format = "0"
|
ws.cell(zeile, 5).number_format = "0"
|
||||||
ws.cell(zeile, 6).number_format = "0"
|
ws.cell(zeile, 6).number_format = "0"
|
||||||
ws.cell(zeile, 11).number_format = EURO_FORMAT
|
ws.cell(zeile, 11).number_format = EURO_FORMAT
|
||||||
|
|||||||
+21
-47
@@ -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:
|
Größenanpassung um die Rahmenbreite wachsen). Windows erledigt alles Weitere selbst:
|
||||||
- Ziehen an der Titelleiste (WM_NCLBUTTONDOWN/HTCAPTION) mit Andocken links/rechts/oben (Maximieren)
|
- Ziehen an der Titelleiste (WM_NCLBUTTONDOWN/HTCAPTION) mit Andocken links/rechts/oben (Maximieren)
|
||||||
- Win+Pfeiltasten, Doppelklick, Systemmenü (Alt+Leertaste), Taskleiste, Alt+Tab
|
- 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
|
- Größe ändern: Den Größen-Mauszeiger am Rand zeigt Tk (rahmen.py) – so weiß Tk immer, welcher Zeiger
|
||||||
passenden Mauszeiger und zieht die Größe selbst (flüssig, kein hängender 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 ->
|
WICHTIG:
|
||||||
|
- Die Fensterprozeduren hier laufen mitten in Tk. Sie dürfen Tk NICHT aufrufen (Tk-Sperre ->
|
||||||
Verklemmung), nur Win32-Funktionen.
|
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
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -27,15 +33,13 @@ from ctypes import wintypes
|
|||||||
IS_WIN = sys.platform.startswith("win")
|
IS_WIN = sys.platform.startswith("win")
|
||||||
|
|
||||||
WM_NCDESTROY, WM_NCCALCSIZE, WM_NCHITTEST, WM_NCACTIVATE = 0x0082, 0x0083, 0x0084, 0x0086
|
WM_NCDESTROY, WM_NCCALCSIZE, WM_NCHITTEST, WM_NCACTIVATE = 0x0082, 0x0083, 0x0084, 0x0086
|
||||||
WM_NCLBUTTONDOWN, WM_SYSCOMMAND, WM_SETCURSOR = 0x00A1, 0x0112, 0x0020
|
WM_NCLBUTTONDOWN, WM_SYSCOMMAND = 0x00A1, 0x0112
|
||||||
HTCLIENT, HTCAPTION, HTTRANSPARENT = 1, 2, -1
|
HTCLIENT, HTCAPTION = 1, 2
|
||||||
HT = {"w": 10, "e": 11, "n": 12, "nw": 13, "ne": 14, "s": 15, "sw": 16, "se": 17}
|
HT = {"w": 10, "e": 11, "n": 12, "nw": 13, "ne": 14, "s": 15, "sw": 16, "se": 17}
|
||||||
GWL_STYLE, GWL_EXSTYLE = -16, -20
|
GWL_STYLE, GWL_EXSTYLE = -16, -20
|
||||||
WS_CAPTION, WS_THICKFRAME, WS_SYSMENU = 0x00C00000, 0x00040000, 0x00080000
|
WS_CAPTION, WS_THICKFRAME, WS_SYSMENU = 0x00C00000, 0x00040000, 0x00080000
|
||||||
WS_MINIMIZEBOX, WS_MAXIMIZEBOX = 0x00020000, 0x00010000
|
WS_MINIMIZEBOX, WS_MAXIMIZEBOX = 0x00020000, 0x00010000
|
||||||
WS_EX_TOOLWINDOW, WS_EX_APPWINDOW = 0x00000080, 0x00040000
|
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
|
SWP_NOSIZE, SWP_NOMOVE, SWP_NOZORDER, SWP_NOACTIVATE, SWP_FRAMECHANGED = 0x1, 0x2, 0x4, 0x10, 0x20
|
||||||
SC_MAXIMIZE, SC_RESTORE, SC_MINIMIZE = 0xF030, 0xF120, 0xF020
|
SC_MAXIMIZE, SC_RESTORE, SC_MINIMIZE = 0xF030, 0xF120, 0xF020
|
||||||
|
|
||||||
@@ -73,9 +77,7 @@ def _api():
|
|||||||
("GetMonitorInfoW", [wintypes.HMONITOR, ctypes.POINTER(MONITORINFO)], wintypes.BOOL),
|
("GetMonitorInfoW", [wintypes.HMONITOR, ctypes.POINTER(MONITORINFO)], wintypes.BOOL),
|
||||||
("ReleaseCapture", [], wintypes.BOOL), ("SendMessageW", [H, U, W, L], LRESULT),
|
("ReleaseCapture", [], wintypes.BOOL), ("SendMessageW", [H, U, W, L], LRESULT),
|
||||||
("PostMessageW", [H, U, W, L], wintypes.BOOL), ("ShowWindow", [H, ctypes.c_int], wintypes.BOOL),
|
("PostMessageW", [H, U, W, L], wintypes.BOOL), ("ShowWindow", [H, ctypes.c_int], wintypes.BOOL),
|
||||||
("WindowFromPoint", [wintypes.POINT], H),
|
("WindowFromPoint", [wintypes.POINT], H)):
|
||||||
("LoadCursorW", [wintypes.HINSTANCE, ctypes.c_void_p], wintypes.HANDLE),
|
|
||||||
("SetCursor", [wintypes.HANDLE], wintypes.HANDLE)):
|
|
||||||
f = getattr(u, name)
|
f = getattr(u, name)
|
||||||
f.argtypes, f.restype = args, res
|
f.argtypes, f.restype = args, res
|
||||||
c.SetWindowSubclass.argtypes = [H, SUBCLASSPROC, ctypes.c_size_t, ctypes.c_size_t]
|
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:
|
class NativerRahmen:
|
||||||
"""Macht das Tk-Hauptfenster zu einem echten Windows-Fenster ohne sichtbaren Windows-Rahmen.
|
"""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).
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, root: tk.Tk, rand: int, ecke: int):
|
def __init__(self, root: tk.Tk, rand: int, ecke: int):
|
||||||
self.root = root
|
self.root = root
|
||||||
@@ -121,11 +119,10 @@ class NativerRahmen:
|
|||||||
self.hwnd = None
|
self.hwnd = None
|
||||||
self.aktiv = False
|
self.aktiv = False
|
||||||
self._procs: dict[int, object] = {} # Referenzen halten, sonst räumt Python die Callbacks weg
|
self._procs: dict[int, object] = {} # Referenzen halten, sonst räumt Python die Callbacks weg
|
||||||
self._rand_hwnds: set[int] = set()
|
|
||||||
|
|
||||||
# ------------------------------------------------------------------ Einrichten
|
# ------------------------------------------------------------------ Einrichten
|
||||||
def einrichten(self, rand_widgets: list[tk.Misc]) -> bool:
|
def einrichten(self) -> bool:
|
||||||
"""Nach dem ersten Anzeigen aufrufen (vorher gibt es das äußere Windows-Fenster noch nicht)."""
|
"""Sobald Tk das äußere Windows-Fenster angelegt hat (möglichst vor dem ersten Anzeigen)."""
|
||||||
if self.aktiv:
|
if self.aktiv:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
@@ -142,10 +139,6 @@ class NativerRahmen:
|
|||||||
u.SetWindowLongW(h, GWL_EXSTYLE, (ex & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW)
|
u.SetWindowLongW(h, GWL_EXSTYLE, (ex & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW)
|
||||||
if not self._unterklasse(h, self._aussen_proc):
|
if not self._unterklasse(h, self._aussen_proc):
|
||||||
return False
|
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
|
self.aktiv = True
|
||||||
# Rahmen neu berechnen lassen (jetzt: unsichtbar)
|
# Rahmen neu berechnen lassen (jetzt: unsichtbar)
|
||||||
u.SetWindowPos(h, None, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE
|
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."""
|
"""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)
|
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.aktiv = False
|
||||||
self._rand_hwnds.clear()
|
return self.einrichten()
|
||||||
return self.einrichten(rand_widgets)
|
|
||||||
|
|
||||||
def _unterklasse(self, h: int, fn) -> bool:
|
def _unterklasse(self, h: int, fn) -> bool:
|
||||||
_, c = _api()
|
_, c = _api()
|
||||||
@@ -229,27 +221,12 @@ class NativerRahmen:
|
|||||||
if rand:
|
if rand:
|
||||||
return HT[rand]
|
return HT[rand]
|
||||||
return HTCLIENT
|
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:
|
if msg == WM_NCDESTROY:
|
||||||
self._entfernen(hwnd)
|
self._entfernen(hwnd)
|
||||||
except Exception: # niemals eine Ausnahme in die Fensterprozedur durchlassen
|
except Exception: # niemals eine Ausnahme in die Fensterprozedur durchlassen
|
||||||
pass
|
pass
|
||||||
return c.DefSubclassProc(hwnd, msg, wparam, lparam)
|
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):
|
def _entfernen(self, hwnd):
|
||||||
proc = self._procs.pop(hwnd, None)
|
proc = self._procs.pop(hwnd, None)
|
||||||
if proc is not None:
|
if proc is not None:
|
||||||
@@ -262,15 +239,16 @@ class NativerRahmen:
|
|||||||
|
|
||||||
# ------------------------------------------------------------------ Bedienung
|
# ------------------------------------------------------------------ Bedienung
|
||||||
def verschieben(self) -> None:
|
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, _ = _api()
|
||||||
u.ReleaseCapture()
|
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:
|
def groesse_ziehen(self, rand: str) -> None:
|
||||||
u, _ = _api()
|
u, _ = _api()
|
||||||
u.ReleaseCapture()
|
u.ReleaseCapture()
|
||||||
u.SendMessageW(self.hwnd, WM_NCLBUTTONDOWN, HT[rand], 0)
|
u.PostMessageW(self.hwnd, WM_NCLBUTTONDOWN, HT[rand], 0)
|
||||||
|
|
||||||
def maximiert(self) -> bool:
|
def maximiert(self) -> bool:
|
||||||
return bool(self.hwnd and _api()[0].IsZoomed(self.hwnd))
|
return bool(self.hwnd and _api()[0].IsZoomed(self.hwnd))
|
||||||
@@ -298,10 +276,6 @@ class NativerRahmen:
|
|||||||
u, _ = _api()
|
u, _ = _api()
|
||||||
return u.GetWindowLongW(self.hwnd, GWL_STYLE), u.GetWindowLongW(self.hwnd, GWL_EXSTYLE)
|
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:
|
def trefferpruefung(self, hwnd: int, x: int, y: int) -> int:
|
||||||
"""Für Tests: Was meldet das Fenster `hwnd` für den Bildschirmpunkt (WM_NCHITTEST)?"""
|
"""Für Tests: Was meldet das Fenster `hwnd` für den Bildschirmpunkt (WM_NCHITTEST)?"""
|
||||||
lp = ((y & 0xFFFF) << 16) | (x & 0xFFFF)
|
lp = ((y & 0xFFFF) << 16) | (x & 0xFFFF)
|
||||||
|
|||||||
@@ -290,9 +290,9 @@ class Hauptrahmen:
|
|||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
sichtbar = True
|
sichtbar = True
|
||||||
if self.nativ.veraltet(): # Tk hat das äußere Fenster neu angelegt
|
if self.nativ.veraltet(): # Tk hat das äußere Fenster neu angelegt
|
||||||
ok = self.nativ.neu_einrichten([self.root, self.aussen])
|
ok = self.nativ.neu_einrichten()
|
||||||
else:
|
else:
|
||||||
ok = self.nativ.einrichten([self.root, self.aussen])
|
ok = self.nativ.einrichten()
|
||||||
if ok:
|
if ok:
|
||||||
# Erst eingerichtet, als das Fenster schon zu sehen war -> Taskleiste muss es neu erfassen
|
# Erst eingerichtet, als das Fenster schon zu sehen war -> Taskleiste muss es neu erfassen
|
||||||
self._taskleiste_neu = sichtbar
|
self._taskleiste_neu = sichtbar
|
||||||
@@ -502,7 +502,8 @@ class Hauptrahmen:
|
|||||||
"sw": "bottom_left_corner"}
|
"sw": "bottom_left_corner"}
|
||||||
|
|
||||||
def _rand_cursor(self, e):
|
def _rand_cursor(self, e):
|
||||||
if self.maximiert or self.nativ_an: # nativ: Windows zeigt den Zeiger selbst
|
if self.maximiert:
|
||||||
|
self._cursor_zurueck()
|
||||||
return
|
return
|
||||||
rand = self._welcher_rand(e.x, e.y)
|
rand = self._welcher_rand(e.x, e.y)
|
||||||
zeiger = (self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, "")
|
zeiger = (self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, "")
|
||||||
@@ -515,7 +516,8 @@ class Hauptrahmen:
|
|||||||
def _resize_start(self, e, rand: str | None = None):
|
def _resize_start(self, e, rand: str | None = None):
|
||||||
self._rand = rand or self._welcher_rand(e.x, e.y)
|
self._rand = rand or self._welcher_rand(e.x, e.y)
|
||||||
if self._rand and self.nativ_an and not self.maximiert:
|
if self._rand and self.nativ_an and not self.maximiert:
|
||||||
# sollte nicht vorkommen (der Rand ist für Windows durchlässig) – dann eben so
|
# Den Zeiger zeigt Tk (weiß dann immer, welcher gilt); ziehen lässt Windows (flüssig,
|
||||||
|
# mit Andocken). Angestoßen per PostMessage – siehe nativ.py.
|
||||||
self.nativ.groesse_ziehen(self._rand)
|
self.nativ.groesse_ziehen(self._rand)
|
||||||
self._rand = ""
|
self._rand = ""
|
||||||
return "break"
|
return "break"
|
||||||
|
|||||||
@@ -58,6 +58,8 @@ def user32():
|
|||||||
u.ReleaseCapture.restype = wintypes.BOOL
|
u.ReleaseCapture.restype = wintypes.BOOL
|
||||||
u.SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
|
u.SendMessageW.argtypes = [wintypes.HWND, wintypes.UINT, wintypes.WPARAM, wintypes.LPARAM]
|
||||||
u.SendMessageW.restype = 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.argtypes = [wintypes.HWND, ctypes.c_int]
|
||||||
u.ShowWindow.restype = wintypes.BOOL
|
u.ShowWindow.restype = wintypes.BOOL
|
||||||
u.IsIconic.argtypes = [wintypes.HWND]
|
u.IsIconic.argtypes = [wintypes.HWND]
|
||||||
@@ -127,7 +129,9 @@ def nativ_verschieben(fenster: tk.Misc) -> bool:
|
|||||||
if not h:
|
if not h:
|
||||||
return False
|
return False
|
||||||
user32().ReleaseCapture()
|
user32().ReleaseCapture()
|
||||||
user32().SendMessageW(h, 0x00A1, 2, 0) # WM_NCLBUTTONDOWN, HTCAPTION
|
# 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
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-14
@@ -836,13 +836,8 @@ def test_rand_zeiger_wird_zurueckgesetzt(lauf_mit_daten):
|
|||||||
lauf.ablauf([]) # erst wenn das Hauptfenster zu sehen ist (Größen stimmen dann)
|
lauf.ablauf([]) # erst wenn das Hauptfenster zu sehen ist (Größen stimmen dann)
|
||||||
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2})()
|
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2})()
|
||||||
r._rand_cursor(ev)
|
r._rand_cursor(ev)
|
||||||
if r.nativ_an:
|
# Den Zeiger verwaltet immer Tk – auch mit nativem Windows-Rahmen (gemeldet in 1.3.0: setzte
|
||||||
# Windows: den Zeiger setzt Windows selbst (WM_SETCURSOR am äußeren Fenster, siehe
|
# Windows ihn am Rand, blieb er im Fenster der Größenpfeil, weil Tk davon nichts wusste).
|
||||||
# test_windows_echtes_fenster_andocken_und_rand) – Tk darf keinen eigenen setzen, der hängen bleibt.
|
|
||||||
assert r.aussen.cget("cursor") == "", "Tk-Zeiger trotz nativem Rahmen gesetzt"
|
|
||||||
assert r._welcher_rand(2, 10) == "nw" and r._welcher_rand(r.aussen.winfo_width() - 3, 10) == "ne"
|
|
||||||
lauf.pruefen()
|
|
||||||
return
|
|
||||||
assert r.aussen.cget("cursor"), "am Rand muss der Größenänderungs-Zeiger erscheinen"
|
assert r.aussen.cget("cursor"), "am Rand muss der Größenänderungs-Zeiger erscheinen"
|
||||||
r.inhalt.event_generate("<Enter>")
|
r.inhalt.event_generate("<Enter>")
|
||||||
lauf.root.update()
|
lauf.root.update()
|
||||||
@@ -923,11 +918,6 @@ def test_windows_echtes_fenster_andocken_und_rand(lauf_mit_daten):
|
|||||||
m["ecke"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + bh - 3) == nativ.HT["se"]
|
m["ecke"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + bh - 3) == nativ.HT["se"]
|
||||||
m["ecke_lang"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + 10) == nativ.HT["ne"]
|
m["ecke_lang"] = n.trefferpruefung(n.hwnd, x + bw - 3, y + 10) == nativ.HT["ne"]
|
||||||
m["innen"] = n.trefferpruefung(n.hwnd, x + bw // 2, y + bh // 2) == nativ.HTCLIENT
|
m["innen"] = n.trefferpruefung(n.hwnd, x + bw // 2, y + bh // 2) == nativ.HTCLIENT
|
||||||
m["rand_durchlaessig"] = n.trefferpruefung(r.aussen.winfo_id(), x + 2, my) == nativ.HTTRANSPARENT
|
|
||||||
m["innen_nicht"] = n.trefferpruefung(r.aussen.winfo_id(), x + bw // 2, y + 3 + 30) != nativ.HTTRANSPARENT
|
|
||||||
# die Maus landet am Rand wirklich beim äußeren Fenster (durch Tk-Fenster hindurch)
|
|
||||||
m["maus_am_rand"] = n.fenster_bei(x + 2, my) == n.hwnd
|
|
||||||
m["maus_innen"] = n.fenster_bei(x + bw // 2, y + bh // 2) != n.hwnd
|
|
||||||
|
|
||||||
# Tk darf das Fenster bei internen Größenanfragen nicht um einen Rahmen vergrößern
|
# Tk darf das Fenster bei internen Größenanfragen nicht um einen Rahmen vergrößern
|
||||||
n.setzen(x, y, bw - 37, bh - 23) # wie Größe ziehen durch den Benutzer
|
n.setzen(x, y, bw - 37, bh - 23) # wie Größe ziehen durch den Benutzer
|
||||||
@@ -960,8 +950,7 @@ def test_windows_echtes_fenster_andocken_und_rand(lauf_mit_daten):
|
|||||||
assert all(m["stil"].values()), f"Fensterstil unvollständig: {m['stil']}"
|
assert all(m["stil"].values()), f"Fensterstil unvollständig: {m['stil']}"
|
||||||
assert m["programmfenster"], "Fenster ist für Windows noch ein Werkzeugfenster (kein Andocken/Win+Pfeil)"
|
assert m["programmfenster"], "Fenster ist für Windows noch ein Werkzeugfenster (kein Andocken/Win+Pfeil)"
|
||||||
assert m["rahmen_unsichtbar"] and m["tk_gleich_aussen"], "Windows-Rahmen ist sichtbar / Tk-Größe passt nicht"
|
assert m["rahmen_unsichtbar"] and m["tk_gleich_aussen"], "Windows-Rahmen ist sichtbar / Tk-Größe passt nicht"
|
||||||
for k in ("links", "ecke", "ecke_lang", "innen", "rand_durchlaessig", "innen_nicht", "maus_am_rand",
|
for k in ("links", "ecke", "ecke_lang", "innen"):
|
||||||
"maus_innen"):
|
|
||||||
assert m[k], f"Trefferprüfung am Rand falsch: {k} ({m})"
|
assert m[k], f"Trefferprüfung am Rand falsch: {k} ({m})"
|
||||||
assert m["waechst_nicht"][0] == m["waechst_nicht"][1], f"Fenster ist gewachsen: {m['waechst_nicht']}"
|
assert m["waechst_nicht"][0] == m["waechst_nicht"][1], f"Fenster ist gewachsen: {m['waechst_nicht']}"
|
||||||
assert m["max"] == (True, True), f"Maximieren über Windows nicht erkannt: {m['max']}"
|
assert m["max"] == (True, True), f"Maximieren über Windows nicht erkannt: {m['max']}"
|
||||||
@@ -1046,3 +1035,20 @@ def test_export_dialog_standard_eine_datei_ohne_null(lauf_mit_daten, tmp_path):
|
|||||||
zeilen = [r for r in openpyxl.load_workbook(dateien[0]).active.iter_rows(values_only=True) if r[1]]
|
zeilen = [r for r in openpyxl.load_workbook(dateien[0]).active.iter_rows(values_only=True) if r[1]]
|
||||||
assert [r[1] for r in zeilen] == ["Sonderbonus Aktion"], "0-€-Position wurde exportiert"
|
assert [r[1] for r in zeilen] == ["Sonderbonus Aktion"], "0-€-Position wurde exportiert"
|
||||||
lauf.pruefen()
|
lauf.pruefen()
|
||||||
|
|
||||||
|
|
||||||
|
def test_rand_druecken_startet_natives_ziehen(lauf_mit_daten, monkeypatch):
|
||||||
|
"""Mit nativem Rahmen zieht Windows die Größe – angestoßen beim Drücken auf den Rand."""
|
||||||
|
lauf = lauf_mit_daten
|
||||||
|
r = lauf.app.rahmen
|
||||||
|
if not r.nativ_an:
|
||||||
|
pytest.skip("nur mit nativem Windows-Rahmen")
|
||||||
|
lauf.ablauf([])
|
||||||
|
gezogen = []
|
||||||
|
monkeypatch.setattr(r.nativ, "groesse_ziehen", gezogen.append)
|
||||||
|
ev = type("E", (), {"x": 2, "y": lauf.root.winfo_height() // 2, "x_root": 0, "y_root": 0})()
|
||||||
|
assert r._resize_start(ev) == "break"
|
||||||
|
ev.x, ev.y = r.aussen.winfo_width() - 2, r.aussen.winfo_height() - 2
|
||||||
|
r._resize_start(ev)
|
||||||
|
assert gezogen == ["w", "se"]
|
||||||
|
lauf.pruefen()
|
||||||
+9
-1
@@ -165,8 +165,16 @@ def test_zuordnung_regeln_und_export(db, freenet_csv, vodafone_xlsx, tmp_path):
|
|||||||
# Standard: freenet UND Vodafone in einer Datei (gemeldet: Vodafone fehlte im Export)
|
# Standard: freenet UND Vodafone in einer Datei (gemeldet: Vodafone fehlte im Export)
|
||||||
eine = export.exportiere_haendler(tmp_path / "out1", "Händler A", eigene, "Oktober 2026")
|
eine = export.exportiere_haendler(tmp_path / "out1", "Händler A", eigene, "Oktober 2026")
|
||||||
assert [d.name for d in eine] == ["Händler A - Oktober 2026.xlsx"]
|
assert [d.name for d in eine] == ["Händler A - Oktober 2026.xlsx"]
|
||||||
arten = [r[1] for r in openpyxl.load_workbook(eine[0]).active.iter_rows(values_only=True) if r[1]]
|
zeilen1 = [r for r in openpyxl.load_workbook(eine[0]).active.iter_rows(values_only=True) if r[1]]
|
||||||
|
arten = [r[1] for r in zeilen1]
|
||||||
assert "Freischaltungsvergütung Vodafone" in arten and "SIM-Karten" in arten
|
assert "Freischaltungsvergütung Vodafone" in arten and "SIM-Karten" in arten
|
||||||
|
# Spalte A: Vodafone mit Aktivierungsdatum, freenet mit Zeitraum
|
||||||
|
vf = next(r for r in zeilen1 if r[1] == "Freischaltungsvergütung Vodafone")
|
||||||
|
assert vf[0] == datetime(2026, 9, 8)
|
||||||
|
assert next(r for r in zeilen1 if r[1] == "SIM-Karten")[0] == "2026.10.01-2026.10.07"
|
||||||
|
ws1 = openpyxl.load_workbook(eine[0]).active
|
||||||
|
zeile_vf = next(c.row for c in ws1["B"] if c.value == "Freischaltungsvergütung Vodafone")
|
||||||
|
assert ws1.cell(zeile_vf, 1).number_format == "DD.MM.YYYY"
|
||||||
ws = openpyxl.load_workbook(dateien[0]).active
|
ws = openpyxl.load_workbook(dateien[0]).active
|
||||||
zeilen = [[c.value for c in r] for r in ws.iter_rows(min_row=1, max_row=4)]
|
zeilen = [[c.value for c in r] for r in ws.iter_rows(min_row=1, max_row=4)]
|
||||||
# Sortierung Zeitraum → Provisionsart → Datum
|
# Sortierung Zeitraum → Provisionsart → Datum
|
||||||
|
|||||||
Reference in new issue
Block a user