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_bilder.py schrieb ein eigenes
  Format, pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende
  nicht sagen, WAS fehlschlug.

* 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.

* Sechs stumme "except OSError" sagen jetzt, warum sie schweigen duerfen, und
  einer meldet statt zu schweigen: schlaegt die Uebernahme einer alten
  config.json fehl, faengt das Programm ohne Firmendaten, Katalog und Zaehler
  neu an - und der Erfolgsfall schrieb eine Zeile, der Fehlerfall nicht. Die
  24 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
This commit is contained in:
TheMockTv 2026-09-07 18:39:03 +02:00
parent 5babfc3e2e
commit 2b09d15cd0
36 changed files with 563 additions and 364 deletions

130
theme.py
View file

@ -46,7 +46,6 @@ def titelleiste_faerben(fenster, dunkel):
if not sys.platform.startswith("win"):
return False
try:
import ctypes
fenster.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(fenster.winfo_id()) or int(fenster.frame(), 16)
wert = ctypes.c_int(1 if dunkel else 0)
@ -84,16 +83,13 @@ def pruefplatz():
if not sys.platform.startswith("win"):
return ""
try:
import ctypes
from ctypes import wintypes
class RECT(ctypes.Structure):
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
class MONITORINFO(ctypes.Structure):
_fields_ = [("cbSize", wintypes.DWORD), ("rcMonitor", RECT),
("rcWork", RECT), ("dwFlags", wintypes.DWORD)]
_fields_ = [("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", RECT),
("rcWork", RECT), ("dwFlags", ctypes.wintypes.DWORD)]
gefunden = []
@ -106,9 +102,9 @@ def pruefplatz():
r.left, r.top, r.right - r.left))
return 1
RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HMONITOR,
wintypes.HDC, ctypes.POINTER(RECT),
wintypes.LPARAM)
RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HMONITOR,
ctypes.wintypes.HDC, ctypes.POINTER(RECT),
ctypes.wintypes.LPARAM)
ctypes.windll.user32.EnumDisplayMonitors(0, 0, RUECKRUF(je_monitor), 0)
# Der breiteste Nebenmonitor - bei mehreren der linkeste.
neben = sorted((m for m in gefunden if not m[0]),
@ -137,7 +133,6 @@ def auf_bildschirm(x, y, breite=0, hoehe=0):
if not sys.platform.startswith("win"):
return int(x), int(y)
try:
import ctypes
hole = ctypes.windll.user32.GetSystemMetrics
links, oben = hole(76), hole(77)
rechts, unten = links + hole(78), oben + hole(79)
@ -207,7 +202,6 @@ def schrift_vorbereiten():
_schrift_geladen = True
if not sys.platform.startswith("win"):
return SCHRIFT_NAME
import ctypes
FR_PRIVATE = 0x10
geladen = 0
for basis in _ordner():
@ -615,6 +609,120 @@ class ThemeMixin:
"""
titelleiste_faerben(win, bool(getattr(self, "_dark_mode", False)))
# ------------------------------------------------ Hell / dunkel umstellen
# Der Hinweistext gehoert dem einzelnen Programm - er sagt etwas ueber
# DIESES Programm. Hier steht nur, was dasteht, wenn ihn niemand setzt.
DARSTELLUNG_HINWEIS = "Gilt nur für das Programmfenster."
def _cfg_speichern(self):
"""Die Einstellungen wegschreiben - falls das noetig ist.
Im Rechnungstool ist die Konfiguration ein gewoehnliches Woerterbuch
und muss ausdruecklich gespeichert werden. Im Steuerrechnungstool ist
sie eine Ablage, die jeden Schreibzugriff sofort in die
Journaltabelle durchreicht - dort ist hier nichts zu tun.
"""
return
def variablen_sichern(self):
"""Alle Tk-Variablen des Fensters als {Name: Wert}.
WARUM ALLE UND NICHT EINE AUSWAHL
---------------------------------
Die _build-Methoden legen ihre Variablen selbst an
(`self.k_name = tk.StringVar()`). Nach dem Neubau stehen also neue,
leere da. Beim ersten Versuch waren Kundenname und Rechnungsnummer
weg, waehrend die Mengen blieben - die hatte ich einzeln gesichert.
Durchgegangen wird deshalb, was das Objekt an Tk-Variablen traegt.
Wer spaeter ein Feld hinzufuegt, muss nichts eintragen; eine Liste zum
Pflegen waere genau die Sorte, die man beim naechsten Feld vergisst.
"""
gesichert = {}
for name in dir(self):
if name.startswith("__"):
continue
try:
wert = getattr(self, name)
except Exception: # noqa: BLE001 - Eigenschaften koennen werfen
continue
if isinstance(wert, (tk.StringVar, tk.IntVar, tk.DoubleVar,
tk.BooleanVar)):
try:
gesichert[name] = wert.get()
except tk.TclError:
pass
return gesichert
def variablen_zuruecksetzen(self, gesichert):
"""Die gesicherten Werte in die neu gebauten Variablen zurueckgeben."""
for name, wert in gesichert.items():
neu = getattr(self, name, None)
if isinstance(neu, (tk.StringVar, tk.IntVar, tk.DoubleVar,
tk.BooleanVar)):
try:
neu.set(wert)
except tk.TclError:
pass
def kinder_loeschen(self):
"""Die Oberflaeche abraeumen - aber keine fremden Fenster.
Ein tk.Toplevel IST ein Kind des Hauptfensters. Der offene
Darstellungsdialog wurde hier bisher mitgeloescht, und die Zeile
danach fasste eine Leiche an ("invalid command name .!toplevel").
Gefunden am 07.09.2026 von der Pruefkette, mit dem echten Bedienweg
gemessen: Klick auf "Dunkel" -> Farbe wechselt, Fenster ist weg,
Traceback im Log.
Der Pruefstand hatte es nicht gefunden, weil er cfg["darstellung"] von
Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser dem
Weg, den ein Mensch geht. Er klickt jetzt.
"""
for kind in self.winfo_children():
if not isinstance(kind, tk.Toplevel):
kind.destroy()
def dlg_darstellung(self):
"""Hell, dunkel oder wie Windows - und sofort sichtbar."""
d = tk.Toplevel(self)
d.title("Darstellung")
d.transient(self)
d.resizable(False, False)
self._style_dialog(d)
rahmen = ttk.Frame(d, padding=18)
rahmen.pack(fill="both", expand=True)
ttk.Label(rahmen, text="Darstellung",
font=(SCHRIFT_NAME, 11, "bold"),
foreground=self.col_accent).pack(anchor="w")
ttk.Label(rahmen, justify="left", wraplength=340,
foreground=self.col_muted,
text=self.DARSTELLUNG_HINWEIS).pack(anchor="w", pady=(6, 12))
gewaehlt = tk.StringVar(value=modus(self.cfg))
def uebernehmen():
neu = gewaehlt.get()
if neu == modus(self.cfg):
return
self.cfg["darstellung"] = neu
self._cfg_speichern()
self._oberflaeche_neu()
# Der Dialog gehoert noch zur alten Darstellung - er wird
# mitgefaerbt, sonst steht ein helles Fenster vor einem dunklen.
self._style_dialog(d)
for wert in MODI:
ttk.Radiobutton(rahmen, text=MODUS_NAMEN[wert], value=wert,
variable=gewaehlt, command=uebernehmen).pack(
anchor="w", pady=3)
ttk.Separator(rahmen, orient="horizontal").pack(fill="x", pady=(12, 10))
ttk.Button(rahmen, text="Schließen", width=14,
command=d.destroy).pack(anchor="e")
mittig(d, self)
return d
class MeldungMixin:
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.