Steuerrechnungstool/pruef_menueleiste.py
TheMockTv 96671027c8 Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle
Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe
Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer
sich pruefbar - nach jeder liefen beide Pruefketten gruen.

* Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks.
  Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung
  des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt
  hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine
  Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird
  ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm()
  begrenzt; am unteren Fensterrand lief sie vorher hinaus.
  Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse.

* %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt
  fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren
  die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn
  eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG).

* pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py.
  Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in
  einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py
  im Rechnungstool schrieb ein eigenes Format.

* dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden
  app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen
  denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es
  eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31)
  und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen.

* pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft
  ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt
  == 1, und der Dateiname wird mitgeprueft.
  Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden
  Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die
  Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF.

* .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen.
  Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen
  Unterschied, den es im Repository gar nicht gibt.

* Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py
  sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben:
  dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung
  waere Rauschen.

* Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=),
  kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von
  theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes".

39 Pruefstaende, 0 rot.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 18:39:03 +02:00

184 lines
6.7 KiB
Python

# -*- coding: utf-8 -*-
"""Pruefstand: die eigene Menueleiste - Wechsel mit EINEM Klick, und am Rand.
WARUM ES DIESE LEISTE GIBT
--------------------------
`tk.Menu` ist unter Windows ein Menue des Betriebssystems. Es nimmt Farben
entgegen und zeichnet trotzdem im Systemgrau - ein weisser Balken ueber einem
dunklen Fenster. Sein Befund am 07.09.2026: *"nur die einstellunden hilfe
leiste die ist noch hell"*. Deshalb ist die Leiste aus gewoehnlichen Labels
gebaut.
WAS HIER GEPRUEFT WIRD
1. Ein Klick auf einen Titel klappt seine Liste auf, ein zweiter auf denselben
Titel klappt sie wieder zu.
2. Steht eine Liste offen und man klickt auf einen ANDEREN Titel, klappt
dessen Liste auf - mit EINEM Klick.
Das ging bis zum 07.09.2026 nicht. Solange eine Liste offen ist, haelt ihr
`grab_set` alle Mausklicks der Anwendung fest; die `<Button-1>`-Bindung des
Menuetitels feuert dann gar nicht. Der Klick landete beim Zuklappen und war
damit verbraucht - man musste ein zweites Mal klicken, um von "Datei" zu
"Hilfe" zu kommen. Das native tk.Menu wechselte schon beim blossen
Ueberfahren; zwei Klicks waren also eine Verschlechterung gegenueber dem,
was ersetzt wurde.
Geprueft wird das ueber ein echtes Klickereignis auf der offenen Liste, mit
den Bildschirmkoordinaten des anderen Titels - also genau der Weg, den der
grab_set erzwingt. Ein direkter Aufruf von aufklappen() wuerde am Problem
vorbeipruefen.
3. Die Liste bleibt auf dem Bildschirm. Steht das Fenster am unteren Rand,
lief sie vorher darueber hinaus und war nicht mehr zu treffen: die Position
wurde ungeprueft gesetzt, waehrend jede andere neue Position im Programm
ueber theme.auf_bildschirm() laeuft.
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
hinueber - sonst steht in zwei Programmen zweierlei.
"""
import os
import sys
import time
import tkinter as tk
QUELLE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, QUELLE)
import menueleiste # noqa: E402
import theme # noqa: E402
from pruefhelfer import fehler, pruefe # noqa: E402
class Fenster(theme.ThemeMixin, tk.Tk):
"""Ein nacktes Fenster mit Farben - mehr braucht die Leiste nicht."""
def __init__(self):
super().__init__()
self.cfg = {} # theme.modus() will etwas mit .get()
self._apply_theme()
self.title("Pruefstand Menueleiste")
self.geometry("560x320" + theme.pruefplatz())
f = Fenster()
gerufen = []
f.menue = menueleiste.Menueleiste(f, {
"Datei": [("Neu", lambda: gerufen.append("neu")), ("-", None),
("Beenden", lambda: gerufen.append("beenden"))],
"Bearbeiten": [("Rueckgaengig", lambda: gerufen.append("rueck"))],
"Hilfe": [("Info", lambda: gerufen.append("info"))],
})
def pumpen(sekunden=0.4):
ende = time.time() + sekunden
while time.time() < ende:
f.update()
time.sleep(0.02)
pumpen(1.0)
titel = {str(w.cget("text")): w for w in f.menue.winfo_children()
if isinstance(w, tk.Label)}
pruefe(set(titel) == {"Datei", "Bearbeiten", "Hilfe"},
f"alle drei Titel stehen in der Leiste ({sorted(titel)})")
def klick_auf_titel(name):
"""Auf einen Menuetitel klicken - so, wie es die Maus tut.
Steht schon eine Liste offen, MUSS das Ereignis an sie gehen: ihr grab_set
faengt jeden Klick der Anwendung ab. Genau das ist der Fall, um den es
unter Punkt 2 geht.
"""
w = titel[name]
x = w.winfo_rootx() + w.winfo_width() // 2
y = w.winfo_rooty() + w.winfo_height() // 2
ziel = f.menue._offen or w
if ziel is w:
# Achtung: Tk nennt die Bildschirmkoordinaten beim Erzeugen -rootx
# und -rooty; im Ereignis heissen sie dann x_root und y_root.
w.event_generate("<Button-1>", x=w.winfo_width() // 2,
y=w.winfo_height() // 2, rootx=x, rooty=y)
else:
ziel.event_generate("<Button-1>", x=-1, y=-1, rootx=x, rooty=y)
pumpen(0.4)
def offener_titel():
"""Welcher Titel gerade offen ist - oder None."""
anker = f.menue._anker
if anker is None or not f.menue._offen:
return None
return next((t for t, w in titel.items() if w is anker), "?")
# ---- 1. Aufklappen und wieder zuklappen ---------------------------------
klick_auf_titel("Datei")
pruefe(offener_titel() == "Datei", f"'Datei' klappt auf ({offener_titel()})")
klick_auf_titel("Datei")
pruefe(offener_titel() is None,
f"und derselbe Titel klappt wieder zu ({offener_titel()})")
# ---- 2. Der Wechsel: EIN Klick ------------------------------------------
klick_auf_titel("Datei")
pruefe(offener_titel() == "Datei", "'Datei' steht offen")
klick_auf_titel("Hilfe")
pruefe(offener_titel() == "Hilfe",
f"ein Klick auf 'Hilfe' wechselt hinueber ({offener_titel()})")
klick_auf_titel("Bearbeiten")
pruefe(offener_titel() == "Bearbeiten",
f"und weiter auf 'Bearbeiten' ({offener_titel()})")
# Ein Klick irgendwo neben die Leiste schliesst - und oeffnet nichts.
pop = f.menue._offen
pop.event_generate("<Button-1>", x=-1, y=-1,
rootx=f.winfo_rootx() + 300,
rooty=f.winfo_rooty() + 250)
pumpen(0.4)
pruefe(offener_titel() is None,
f"ein Klick daneben schliesst und oeffnet nichts ({offener_titel()})")
# ---- 3. Ein Eintrag fuehrt seinen Befehl aus ----------------------------
klick_auf_titel("Hilfe")
eintrag = None
for kind in f.menue._offen.winfo_children():
for enkel in kind.winfo_children():
if isinstance(enkel, tk.Label) and str(enkel.cget("text")) == "Info":
eintrag = enkel
eintrag.event_generate("<Button-1>", x=2, y=2)
pumpen(0.4)
pruefe(gerufen == ["info"], f"der Eintrag 'Info' fuehrt seinen Befehl aus ({gerufen})")
pruefe(offener_titel() is None, "und die Liste ist danach zu")
# ---- 4. Am unteren Bildschirmrand bleibt die Liste sichtbar -------------
# Das Fenster so weit nach unten schieben, dass unter der Leiste kein Platz
# mehr fuer die Klappliste ist.
links, oben = theme.auf_bildschirm(f.winfo_rootx(), f.winfo_rooty())
unten = oben + f.winfo_screenheight()
f.geometry(f"560x320+{links}+{unten - 120}")
pumpen(0.6)
klick_auf_titel("Datei")
pop = f.menue._offen
pruefe(pop is not None, "die Liste geht auch am unteren Rand auf")
if pop is not None:
x, y = pop.winfo_rootx(), pop.winfo_rooty()
hoehe, breite = pop.winfo_height(), pop.winfo_width()
grenze_x, grenze_y = theme.auf_bildschirm(x, y, breite, hoehe)
pruefe((x, y) == (grenze_x, grenze_y),
f"und steht vollstaendig auf dem Bildschirm "
f"(Liste {x},{y} bis {x + breite},{y + hoehe})")
f.destroy()
if fehler:
print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt")
sys.exit(1)
print("GRUEN | Die Menueleiste wechselt mit einem Klick und bleibt im Bild")