# -*- coding: utf-8 -*- """Pruefstand: hell, dunkel, automatisch - und nichts geht dabei verloren. Sein Wortlaut vom 07.09.2026: "waere nun noch gut das mann in den einstellungen einstellen kann hell oder dunkel oder automatik das fehlt sollte eigendlich drin sein" "und beim steuertool fehlt das theam in genze oder wird nicht durch die automatik gesetzt" Er hatte recht: dieses Programm hatte ueberhaupt kein Theme - es lief im Tk-Standard, mit dreimal fest eingetragenem foreground="black". GEPRUEFT WIRD DREIERLEI 1. Die Einstellung wirkt. "hell" ist hell, "dunkel" ist dunkel, "auto" folgt Windows. 2. Beim Umschalten bleibt der Stand stehen - Jahr und Suchfeld. 3. NACH dem Wechsel traegt kein Widget mehr eine Farbe der anderen Palette. Das ist die eigentliche Falle beim Umfaerben: ein einzelnes vergessenes Label leuchtet hell im dunklen Fenster, und niemand findet die Stelle wieder. Deshalb wird der ganze Widget-Baum abgegangen und mit den Farben der jeweils ANDEREN Palette verglichen. """ import os import shutil import sys import tempfile import time import tkinter as tk QUELLE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, QUELLE) os.chdir(QUELLE) os.environ["CAMPINGHOF_TEST"] = "1" from tkinter import messagebox # noqa: E402 messagebox.askyesno = lambda *a, **k: True for _n in ("showinfo", "showwarning", "showerror"): setattr(messagebox, _n, lambda *a, **k: None) # Auf einer KOPIE des Programms arbeiten: der Datenordner liegt aus der Quelle # heraus neben app.py (ablage.py), also legt eine Kopie im Temp-Ordner auch ein # eigenes Journal an. Das echte Journal des Betriebs wird nicht angefasst - # hier wird eine Einstellung geschrieben, und die gehoert nicht in seine Daten. tmp = tempfile.mkdtemp(prefix="darstellung_") prog = os.path.join(tmp, "programm") os.makedirs(prog) for name in os.listdir(QUELLE): if name.endswith(".py"): shutil.copy(os.path.join(QUELLE, name), os.path.join(prog, name)) for name in ("assets",): if os.path.isdir(os.path.join(QUELLE, name)): shutil.copytree(os.path.join(QUELLE, name), os.path.join(prog, name)) sys.path.insert(0, prog) os.chdir(prog) import app as A # noqa: E402 import theme # noqa: E402 from pruefhelfer import fehler, pruefe f = A.App() def pumpen(sekunden=1.0): ende = time.time() + sekunden while time.time() < ende: f.update() time.sleep(0.02) pumpen(2.0) # ---- Ohne Einstellung: automatisch -------------------------------------- pruefe(theme.modus(f.cfg) == "auto", f"ohne Eintrag gilt 'auto' ({theme.modus(f.cfg)!r})") pruefe(f._dark_mode == theme.windows_dark_mode(), "und folgt damit dem Windows-Theme") # ---- Stand, der den Wechsel ueberleben muss ---------------------------- f.jahr.set(2025) f.suche.set("Muster") pumpen(0.5) def farben(dunkel): """Die Palette der einen oder anderen Darstellung, als Menge.""" merker = f._dark_mode f.cfg["darstellung"] = "dunkel" if dunkel else "hell" f._apply_theme() satz = {getattr(f, n).lower() for n in dir(f) if n.startswith("col_") and isinstance(getattr(f, n), str)} f._dark_mode = merker return satz HELL = farben(False) DUNKEL = farben(True) # Was in BEIDEN Paletten vorkommt, taugt nicht als Beweis. NUR_HELL = HELL - DUNKEL NUR_DUNKEL = DUNKEL - HELL def fremdfarben(verboten): """Alle Widgets, die eine Farbe der falschen Palette tragen.""" treffer = [] def durchgehen(w): for kind in w.winfo_children(): for eigenschaft in ("bg", "background", "fg", "foreground"): try: wert = str(kind.cget(eigenschaft)).lower() except tk.TclError: continue if wert in verboten: treffer.append((str(kind), eigenschaft, wert)) durchgehen(kind) durchgehen(f) return treffer # ---- Die drei Modi ------------------------------------------------------ for wunsch, soll_dunkel in (("hell", False), ("dunkel", True)): f.cfg["darstellung"] = wunsch f._oberflaeche_neu() pumpen(0.8) pruefe(f._dark_mode == soll_dunkel, f"'{wunsch}' zeichnet {'dunkel' if soll_dunkel else 'hell'} " f"(Hintergrund {f.col_bg})") # Nichts darf verlorengehen. pruefe(f.jahr.get() == 2025, f"nach dem Wechsel steht das gewaehlte Jahr noch da ({f.jahr.get()})") pruefe(f.suche.get() == "Muster", f"und der Suchbegriff ({f.suche.get()!r})") # Kein Rest der anderen Palette. reste = fremdfarben(NUR_DUNKEL if not soll_dunkel else NUR_HELL) pruefe(not reste, f"in '{wunsch}' ist keine Farbe der anderen Darstellung " f"uebrig ({reste[:3]})") # ---- Dialoge gehen mittig ueber dem Programmfenster auf ------------------ # Seine Ansage: "einfach so programmieren das die popup auf dem fenster mittig # vom main fenster auf gehen". Das ist zugleich die Antwort auf die # Monitorfrage - steht das Programm auf dem zweiten Bildschirm, geht der # Dialog dort auf. Vorher klemmte ein max(0, x) jede negative Koordinate auf # null und riss die Fenster auf den Hauptmonitor zurueck. d = f.dlg_darstellung() pumpen(1.2) d.update_idletasks() mitte_dialog = d.winfo_rootx() + d.winfo_width() // 2 mitte_fenster = f.winfo_rootx() + f.winfo_width() // 2 pruefe(abs(mitte_dialog - mitte_fenster) <= 30, f"der Dialog steht mittig ueber dem Fenster " f"({mitte_dialog} vs. {mitte_fenster})") pruefe(d.winfo_rootx() >= f.winfo_rootx() - 40 and d.winfo_rootx() + d.winfo_width() <= f.winfo_rootx() + f.winfo_width() + 40, "und liegt waagerecht innerhalb des Fensters") pruefe(d.winfo_rooty() >= f.winfo_rooty() - 40, "und nicht oberhalb davon") d.destroy() pumpen(0.3) # ---- Der Dialog muss den Klick UEBERLEBEN ------------------------------- # Gefunden am 07.09.2026 von der Pruefkette: _oberflaeche_neu() loeschte alle # Kinder des Hauptfensters - und ein Toplevel IST ein Kind. Der offene Dialog # wurde also beim ersten Klick mitgeloescht, die Zeile danach fasste eine # Leiche an ("invalid command name .!toplevel"). # # Dieser Pruefstand hat es NICHT gefunden, weil er cfg["darstellung"] von Hand # setzte. Hier wird jetzt der Radioknopf gedrueckt - derselbe Weg, den ein # Mensch geht. d = f.dlg_darstellung() pumpen(1.0) def radioknoepfe(fenster): """Alle Radiobuttons unterhalb eines Fensters, mit ihrer Beschriftung.""" treffer = [] def durchgehen(w): for kind in w.winfo_children(): if kind.winfo_class() == "TRadiobutton": try: treffer.append((str(kind.cget("text")), kind)) except tk.TclError: pass durchgehen(kind) durchgehen(fenster) return treffer knoepfe = radioknoepfe(d) pruefe(len(knoepfe) == len(theme.MODI), f"der Dialog zeigt alle {len(theme.MODI)} Moeglichkeiten " f"({[t for t, _ in knoepfe]})") ziel = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["dunkel"]), None) pruefe(ziel is not None, "der Knopf 'Dunkel' ist da") if ziel is not None: ziel.invoke() # der echte Klick pumpen(1.2) pruefe(bool(d.winfo_exists()), "der Dialog lebt nach dem Klick noch") pruefe(f._dark_mode is True, f"und die Darstellung ist umgestellt (Hintergrund {f.col_bg})") # Und noch einmal, denn genau das war vorher unmoeglich: hell = next((w for t, w in knoepfe if t == theme.MODUS_NAMEN["hell"]), None) if hell is not None: try: hell.invoke() pumpen(1.0) pruefe(f._dark_mode is False, "ein ZWEITER Klick im selben Dialog wirkt auch") except tk.TclError as e: pruefe(False, f"der zweite Klick scheiterte ({e})") try: d.destroy() except tk.TclError: pass pumpen(0.3) # ---- Zurueck auf automatisch -------------------------------------------- f.cfg["darstellung"] = "auto" f._oberflaeche_neu() pumpen(0.8) pruefe(f._dark_mode == theme.windows_dark_mode(), "'auto' folgt wieder dem Windows-Theme") # ---- Und die Einstellung ueberlebt den Neustart ------------------------- f.cfg["darstellung"] = "hell" f.destroy() f = A.App() pumpen(2.0) pruefe(f._dark_mode is False, "die Einstellung steht beim naechsten Start wieder da") try: f.destroy() except Exception: # noqa: BLE001 pass shutil.rmtree(tmp, ignore_errors=True) if fehler: print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") sys.exit(1) print("GRUEN | Hell, dunkel und automatisch wirken - ohne Datenverlust")