From f7801f46f0029998f34ecdca328814483649af26 Mon Sep 17 00:00:00 2001 From: TheMockTv Date: Mon, 7 Sep 2026 00:34:16 +0200 Subject: [PATCH] Darstellung waehlbar: hell, dunkel oder wie Windows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seine Ansage "waere nun noch gut das mann in den einstellungen einstellen kann hell oder dunkel oder automatik das fehlt sollte eigendlich drin sein" Er hatte recht: beide Farbpaletten lagen laengst in theme.py, das Programm folgte aber starr dem Windows-Theme. Jetzt steht die Wahl unter Einstellungen -> Darstellung und wirkt SOFORT, nicht erst beim Neustart. Umschalten heisst neu bauen Die Farben stehen an ueber sechzig Stellen fest in den Widgets. Sie einzeln nachzuziehen hiesse, ein Verzeichnis aller eingefaerbten Widgets zu fuehren - und wer beim naechsten neuen Widget vergisst, es einzutragen, hat einen hellen Fleck im dunklen Fenster, den niemand mehr findet. Neu bauen kann nichts vergessen. ⚠️ Dabei gingen im ersten Versuch Kundenname und Rechnungsnummer verloren: die _build_-Methoden legen ihre StringVars SELBST an, nach dem Neubau standen leere da. Gesichert werden jetzt ALLE Tk-Variablen des Fensters, nicht eine gepflegte Auswahl - wer spaeter ein Feld hinzufuegt, muss nichts eintragen. Titelleiste, Symbol und Logo gehen mit Die Titelleiste gehoert Windows; sie folgt ueber DWMWA_USE_IMMERSIVE_DARK_MODE. _dark_titlebar konnte bisher nur DUNKEL und zeigt jetzt in beide Richtungen - vorher behielt man beim Wechsel auf hell eine schwarze Leiste ueber einem hellen Fenster. Sein Befund: "die svg fuer das dunkle heller machen weil die geht nun unter" und "icon musst wenn es dunkel ist mit aendern auf das hellere". Gemessen sind es zwei Farben: Petrol #004860 und ein fast schwarzes #181818 - letzteres liegt genau auf der Kachelfarbe #15171b. Umgestellt wird nur die HELLIGKEIT (L -> 1-L, gedeckelt bei 0.78), Farbton und Saettigung bleiben: sonst waere es nicht mehr sein Logo. Betrifft Wasserzeichen, Kopflogo und das Fenstersymbol; die aufgehellte ICO entsteht beim Bauen. menueleiste.py Die selbstgebaute Menueleiste ist jetzt eine eigene Datei und liegt in beiden Programmen gleich - das Steuerrechnungstool hatte noch das native tk.Menu, das sich unter Windows nicht einfaerben laesst. pruef_darstellung.py Prueft die drei Modi, dass beim Umschalten NICHTS verlorengeht, und dass danach kein Widget mehr eine Farbe der anderen Palette traegt - genau der helle Fleck, den man sonst nie wiederfindet. Nebenbei: die beiden Dauertimer melden beim Beenden kein "invalid command name ..._api_pumpe" mehr. Ein Log, in dem beim normalen Beenden immer zwei Fehler stehen, liest irgendwann niemand mehr. Fassung 1.8.0. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax --- AUFGABE_BEWEISKETTE_PRUEFBAR.md | 25 ++-- Diagnose.spec | 2 +- Rechnungstool.spec | 2 +- app.py | 221 ++++++++++++++++++++++---------- assets/programm_dunkel.ico | Bin 0 -> 58385 bytes bauen.py | 9 ++ menueleiste.py | 120 +++++++++++++++++ pruef_darstellung.py | 196 ++++++++++++++++++++++++++++ theme.py | 165 ++++++++++++++++++++---- version.py | 4 +- wasserzeichen.py | 89 ++++++++++++- 11 files changed, 724 insertions(+), 109 deletions(-) create mode 100644 assets/programm_dunkel.ico create mode 100644 menueleiste.py create mode 100644 pruef_darstellung.py diff --git a/AUFGABE_BEWEISKETTE_PRUEFBAR.md b/AUFGABE_BEWEISKETTE_PRUEFBAR.md index 7443f0c..426faf7 100644 --- a/AUFGABE_BEWEISKETTE_PRUEFBAR.md +++ b/AUFGABE_BEWEISKETTE_PRUEFBAR.md @@ -108,21 +108,28 @@ kein Nebenbei — Menüs, Tabellen, Dialoge, Statusleiste. Vorher planen. --- -## ⚠️ Gefundener Altfehler — `pruef_wache` (Steuerrechnungstool) ⬜ OFFEN +## ✅ Erledigter Altfehler — `pruef_wache` (Steuerrechnungstool) **07.09.2026 behoben** **Nicht von den Arbeiten am 06.09.2026 verursacht** — bei der Ausgangsmessung vor dem Umbau bereits rot, nur mit anderem Text. -`pruef_wache.py` erwartet nach einem Push des Rechnungstools in der Statuszeile den Hinweis -**„Rechnungstool meldet"**. Tatsächlich steht dort das Ergebnis des anschließenden Einlesens -(`0 neu, 0 aktualisiert, 1 unverändert`; bei der Ausgangsmessung: `Nummernbuch: 1 Nummern -eingetragen`). Das Einlesen **überschreibt die Meldung**, bevor der Nutzer sie lesen kann. +`pruef_wache.py` erwartete nach einem Push des Rechnungstools in der Statuszeile den Hinweis +**„Rechnungstool meldet"**. Tatsächlich stand dort das Ergebnis des anschließenden Einlesens. -**Zu klären:** Ist das ein Fehler im Programm (die Meldung soll stehen bleiben, das Ergebnis -danebengehören) oder eine veraltete Erwartung im Prüfstand? Nicht nebenbei entscheiden — die -Statuszeile ist das Einzige, was dem Nutzer sagt, dass die andere Anwendung etwas gemeldet hat. +**Die Frage war:** Fehler im Programm oder veraltete Erwartung im Prüfstand? -Folgefehler derselben Stelle: „dann kommt keine Meldung dazu". +**Die Antwort: Fehler im Programm.** Sichtbar wurde das, weil der Prüfstand *mal grün und mal rot* +lief — je nachdem, wann er hinsah. Ein Prüfstand, der würfelt, ist nicht der eigentliche Befund, +sondern sein Symptom: die Meldung stand nur Sekundenbruchteile da, danach überschrieb sie das +Einlese-Ergebnis. Der Benutzer sah **nie**, dass eine Rechnung hereingekommen war. + +**Behoben:** `melde()` kennt jetzt einen `vorrang` in Sekunden. Eine Meldung mit Vorrang lässt sich +in dieser Zeit nicht von einer gewöhnlichen überschreiben; die Meldung des Rechnungstools bekommt +sechs Sekunden. Dreimal hintereinander stabil grün. + +⚠️ **Zum Merken:** Erst die Warteschleife im Prüfstand („warten, bis der Text dasteht" statt einmal +hinsehen) machte den Fehler *reproduzierbar* — und drehte ihn von zufällig grün auf konstant rot. +Ein wackelnder Prüfstand ist keine Nebensache, die man wegdrückt: er zeigt auf eine echte Stelle. --- diff --git a/Diagnose.spec b/Diagnose.spec index d030d6f..bf199bd 100644 --- a/Diagnose.spec +++ b/Diagnose.spec @@ -9,7 +9,7 @@ a = Analysis( # nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt # das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders # aus als hier. - datas=[('assets/programm.ico', 'assets'), + datas=[('assets/programm.ico', 'assets'), ('assets/programm_dunkel.ico', 'assets'), ('assets/programm_logo.png', 'assets'),('assets/logo.png', 'assets'), ('assets/fonts', 'assets/fonts'), # Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe diff --git a/Rechnungstool.spec b/Rechnungstool.spec index 57d1265..9f4a7a6 100644 --- a/Rechnungstool.spec +++ b/Rechnungstool.spec @@ -9,7 +9,7 @@ a = Analysis( # nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt # das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders # aus als hier. - datas=[('assets/programm.ico', 'assets'), + datas=[('assets/programm.ico', 'assets'), ('assets/programm_dunkel.ico', 'assets'), ('assets/programm_logo.png', 'assets'),('assets/logo.png', 'assets'), ('assets/fonts', 'assets/fonts'), # Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe diff --git a/app.py b/app.py index fa858c7..b2f9dcf 100644 --- a/app.py +++ b/app.py @@ -29,6 +29,7 @@ from tkinter import ttk, messagebox import api import config +import menueleiste import berechnung import gemeinsam import hinweis @@ -145,7 +146,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, def _build_menu(self): # Vollstaendig eigene Menueleiste + eigene Dropdowns -> komplett theme-konform # (native tk-Menubar/Popups lassen sich unter Windows nicht einfaerben). - self._open_pop = None self._menus = { "Datei": [ ("Vorschau ansehen", self.vorschau_zeigen), @@ -163,72 +163,16 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, ("Rechnungsnummer…", self.dlg_nummer), ("Speicherort der Rechnungen…", self.dlg_speicherort), ("Gemeinsamer Ordner (Nummernbuch)…", self.dlg_gemeinsam), + ("-", None), + ("Darstellung…", self.dlg_darstellung), ], "Hilfe": [("Info", self._info)], } - bar = tk.Frame(self, bg=self.col_bg) - bar.pack(side="top", fill="x") - for text, items in self._menus.items(): - lbl = tk.Label(bar, text=text, bg=self.col_bg, fg=self.col_fg, padx=8, pady=3) - lbl.pack(side="left") - lbl.bind("", lambda e, w=lbl: w.configure(bg=self.col_sel)) - lbl.bind("", lambda e, w=lbl: w.configure(bg=self.col_bg)) - lbl.bind("", lambda e, b=lbl, it=items: self._menu_popup(b, it)) - ttk.Separator(self, orient="horizontal").pack(side="top", fill="x") + self.menue = menueleiste.Menueleiste(self, self._menus, farben=self) # bind statt bind_all: sonst feuert Strg+P auch aus einem offenen # Dialog heraus und schreibt im Formular dahinter eine Rechnung. self.bind("", lambda e: self.pdf_erstellen()) - def _menu_popup(self, anchor, items): - if self._open_pop: # offenes Menu zu -> Toggle - self._close_pop() - return - pop = tk.Toplevel(self) - pop.overrideredirect(True) - pop.configure(bg=self.col_line) # 1px Rahmen = col_line statt weiss - inner = tk.Frame(pop, bg=self.col_field) - inner.pack(padx=1, pady=1) - for label, cmd in items: - if label == "-": - tk.Frame(inner, bg=self.col_line, height=1).pack(fill="x", pady=3) - continue - it = tk.Label(inner, text=label, bg=self.col_field, fg=self.col_fg, - anchor="w", padx=16, pady=5) - it.pack(fill="x") - it.bind("", lambda e, w=it: w.configure(bg=self.col_sel)) - it.bind("", lambda e, w=it: w.configure(bg=self.col_field)) - it.bind("", lambda e, c=cmd: self._run_menu(c)) - pop.update_idletasks() - pop.geometry(f"+{anchor.winfo_rootx()}+{anchor.winfo_rooty() + anchor.winfo_height()}") - self._open_pop = pop - pop.grab_set() - pop.bind("", self._maybe_close_pop, add="+") - pop.bind("", lambda e: self._close_pop()) - self._dark_titlebar(pop) - - def _maybe_close_pop(self, event): - pop = self._open_pop - if not pop: - return - if (event.x_root < pop.winfo_rootx() or event.x_root > pop.winfo_rootx() + pop.winfo_width() - or event.y_root < pop.winfo_rooty() or event.y_root > pop.winfo_rooty() + pop.winfo_height()): - self._close_pop() - - def _run_menu(self, cmd): - self._close_pop() - if cmd: - cmd() - - def _close_pop(self): - if getattr(self, "_open_pop", None): - try: - self._open_pop.grab_release() - self._open_pop.destroy() - except tk.TclError: - pass - self._open_pop = None - - # ------------------------------------------------------- Arbeitsflaeche def _build_flaeche(self): """Die Kaesten und der Knopf, mit dem man sie verschieben darf. @@ -504,6 +448,134 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, self._render_positionszeilen() + # ------------------------------------------------------- Darstellung + def _oberflaeche_neu(self): + """Die ganze Oberflaeche neu zeichnen - fuer den Wechsel hell/dunkel. + + WARUM NEU BAUEN UND NICHT UMFAERBEN + ----------------------------------- + Die Farben stehen an ueber sechzig Stellen fest in den Widgets + (tk.Label mit bg=..., Streifen der Leistungsliste, Kopfzeilen der + Kaesten). Sie einzeln nachzuziehen hiesse, ein Verzeichnis aller + eingefaerbten Widgets zu fuehren - und wer beim naechsten neuen Widget + vergisst, es einzutragen, hat einen hellen Fleck im dunklen Fenster, + den niemand mehr findet. Neu bauen kann nichts vergessen. + + WAS DABEI ERHALTEN BLEIBT + ------------------------- + Alles Eingetippte. Ein Themenwechsel darf keine halb getippte Rechnung + wegwerfen. + + Dass die Werte an StringVars haengen, reicht dafuer NICHT: die + _build_-Methoden legen ihre Vars selbst an (`self.k_name = + tk.StringVar()`), beim Neubau stehen also neue, leere Vars da. Beim + ersten Versuch waren Kundenname und Rechnungsnummer weg, waehrend die + Mengen blieben - die hatte ich einzeln gesichert. + + Deshalb werden hier ALLE Variablen des Fensters gesichert, nicht eine + Auswahl: durchgegangen wird, was das Objekt an Tk-Variablen traegt. + Wer spaeter ein Feld hinzufuegt, muss nichts eintragen - es ist + automatisch dabei. 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 + mengen = {nr: v.get() for nr, v in self.anzahl_vars.items()} + personen = {nr: v.get() for nr, v in self.personen_vars.items()} + blatt = None + try: + blatt = self.blaetter.index(self.blaetter.select()) + except Exception: # noqa: BLE001 - dann eben das erste Blatt + pass + + for kind in self.winfo_children(): + kind.destroy() + self.flaeche = None + self._text_zellen = [] + + self._apply_theme() + self._build_menu() + self._build_summen() + self._build_flaeche() + self._build_kopf() + self._build_positionen() + self.flaeche.anordnen() + + 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 + for nr, wert in mengen.items(): + if nr in self.anzahl_vars: + self.anzahl_vars[nr].set(wert) + for nr, wert in personen.items(): + if nr in self.personen_vars: + self.personen_vars[nr].set(wert) + if blatt is not None: + try: + self.blaetter.select(blatt) + except Exception: # noqa: BLE001 + pass + self._buch_anzeigen() + self.recalc() + + 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=(theme.SCHRIFT_NAME, 11, "bold"), + foreground=self.col_accent).pack(anchor="w") + ttk.Label(rahmen, justify="left", wraplength=340, foreground=self.col_muted, + text="Gilt nur für das Programmfenster. Die Farbe der Rechnung " + "steht unter Einstellungen → Firmendaten – das Blatt " + "gehört dem Kunden, nicht dem Programm.").pack( + anchor="w", pady=(6, 12)) + + gewaehlt = tk.StringVar(value=theme.modus(self.cfg)) + + def uebernehmen(): + neu = gewaehlt.get() + if neu == theme.modus(self.cfg): + return + self.cfg["darstellung"] = neu + config.save_config(self.cfg) + 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 theme.MODI: + ttk.Radiobutton(rahmen, text=theme.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") + d.geometry(f"+{self.winfo_rootx() + 120}+{self.winfo_rooty() + 140}") + return d + def _umbruch_nachziehen(self, runde=0, vorige_breite=None): """Den Textumbruch der Leistungsspalte auf ihre echte Breite setzen. @@ -714,7 +786,8 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, # Das Programmlogo neben der Marke - klein, immer sichtbar. Es steht # rechts bei "powered by ravokk", weil dort die Kennung des Werkzeugs # sitzt und nicht die des Kunden. - self._kopf_bild = wasserzeichen.kopf(18, master=self) + self._kopf_bild = wasserzeichen.kopf(18, master=self, + dunkel=getattr(self, "_dark_mode", False)) if self._kopf_bild is not None: ttk.Label(status, image=self._kopf_bild).pack(side="right", padx=(0, 6)) status.pack(side="bottom", fill="x") @@ -753,7 +826,8 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, def _wasserzeichen_setzen(self, groesse=240): """Das Programmlogo blass hinter die Arbeitsflaeche legen.""" - bild = wasserzeichen.hole(groesse, 0.06, master=self) + bild = wasserzeichen.hole(groesse, 0.06, master=self, + dunkel=getattr(self, "_dark_mode", False)) if bild is None: return # Die Referenz MUSS bleiben - sonst raeumt Python das Bild weg und Tk @@ -766,7 +840,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, def _fenstersymbol(self): """Das Programmsymbol auf das Fenster legen.""" - return wasserzeichen.symbol_setzen(self) + return wasserzeichen.symbol_setzen(self, dunkel=getattr(self, "_dark_mode", False)) print("[symbol] assets/programm.ico nicht gefunden") return False @@ -1145,7 +1219,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, except Exception as e: # noqa: BLE001 print("[push]", e) finally: - self.after(250, self._push_abholen) + self._wieder(250, self._push_abholen) def _nachtragen(self): """Alles, was schon im Rechnungsordner liegt, ins Buch schreiben. @@ -1398,6 +1472,23 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, return jahr, zaehler, f"{jahr}-A{zaehler:03d}" # ------------------------------------------------- Bedienung ueber die API + def _wieder(self, ms, was): + """Einen sich selbst planenden Timer neu setzen - nur solange es das + Fenster noch gibt. + + Ohne das melden die beiden Dauertimer beim Beenden + `invalid command name ..._api_pumpe`: Tk hat das Fenster samt Callback + schon abgeraeumt, der eingeplante Aufruf laeuft aber noch. Das + stuerzt nichts ab, landet aber als Fehler im Log - und ein Log, in dem + beim normalen Beenden immer zwei Fehler stehen, liest irgendwann + niemand mehr. + """ + try: + if self.winfo_exists(): + self.after(ms, was) + except tk.TclError: + pass # Fenster ist weg - dann gibt es nichts mehr zu tun + def _api_pumpe(self): """Holt Aktionen aus der Warteschlange und fuehrt sie HIER aus. @@ -1407,7 +1498,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, try: self._api_schlange.abarbeiten(self._api_aktion) finally: - self.after(150, self._api_pumpe) + self._wieder(150, self._api_pumpe) def _api_aktion(self, name, daten): """Eine Bedienung ausfuehren. Der Schluessel ist schon geprueft (api.py). diff --git a/assets/programm_dunkel.ico b/assets/programm_dunkel.ico new file mode 100644 index 0000000000000000000000000000000000000000..ce2d86d4d734802cd80009de929e57eaa209bcf9 GIT binary patch literal 58385 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 literal 0 HcmV?d00001 diff --git a/bauen.py b/bauen.py index 5ef69f5..efd4381 100644 --- a/bauen.py +++ b/bauen.py @@ -82,6 +82,15 @@ def main(): for ordner in ("build", "dist", "setup", "release"): shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True) + # Die aufgehellte Fassung des Symbols fuer die dunkle Darstellung. Sie + # wird HIER erzeugt und nicht erst beim ersten dunklen Start: sonst muesste + # das Programm in seinen eigenen Ordner schreiben duerfen - unter + # "Programme" darf es das nicht. + sys.path.insert(0, HIER) + import wasserzeichen + dunkles = wasserzeichen.symbol_datei_dunkel() + print(f"--- Symbol dunkel: {dunkles or 'FEHLT'}") + lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"], f"{stamm} {version.VERSION}") lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"], diff --git a/menueleiste.py b/menueleiste.py new file mode 100644 index 0000000..6eb4ce6 --- /dev/null +++ b/menueleiste.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +"""Eine Menueleiste, die sich einfaerben laesst. + +WARUM NICHT DAS MENUE VON TK +---------------------------- +`tk.Menu` ist unter Windows kein Tk-Widget, sondern ein Menue des +Betriebssystems. Es nimmt Farben zwar entgegen, zeichnet aber trotzdem im +Systemgrau - ein weisser Balken ueber einem dunklen Fenster. Sein Befund am +07.09.2026 zum Steuerrechnungstool: *"nur die einstellunden hilfe leiste die +ist noch hell"*. + +Diese Leiste ist deshalb aus gewoehnlichen Labels gebaut: eine Zeile mit den +Titeln, und beim Klick geht ein randloses Toplevel als Klappliste auf. Beides +traegt die Farben des Programms. + +WAS SIE KANN UND WAS NICHT +-------------------------- +Sie kann, was diese beiden Programme brauchen: Titel, Eintraege, Trennlinien. +Sie kann bewusst NICHT: Untermenues, Tastenkuerzel neben dem Eintrag, +Ankreuzfelder. Wer das braucht, baut es hier ein - nicht daneben. + +BENUTZUNG + + self.menue = menueleiste.Menueleiste(self, farben=self, menues={ + "Datei": [("Beenden", self._beenden)], + "Hilfe": [("Info", self._info)], + }) + +`farben` ist das Fenster mit den col_*-Feldern (das ThemeMixin). Ein "-" als +Beschriftung ergibt eine Trennlinie. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber. +""" +import tkinter as tk +from tkinter import ttk + + +class Menueleiste(tk.Frame): + def __init__(self, master, menues, farben=None, mit_trennlinie=True): + self.f = farben if farben is not None else master + super().__init__(master, bg=self.f.col_bg) + self.menues = menues + self._offen = None + self.pack(side="top", fill="x") + + for titel, eintraege in menues.items(): + lbl = tk.Label(self, text=titel, bg=self.f.col_bg, fg=self.f.col_fg, + padx=8, pady=3) + lbl.pack(side="left") + lbl.bind("", lambda e, w=lbl: w.configure(bg=self.f.col_sel)) + lbl.bind("", lambda e, w=lbl: w.configure(bg=self.f.col_bg)) + lbl.bind("", + lambda e, b=lbl, it=eintraege: self.aufklappen(b, it)) + + if mit_trennlinie: + ttk.Separator(master, orient="horizontal").pack(side="top", fill="x") + + # ---------------------------------------------------------------- Klappen + def aufklappen(self, anker, eintraege): + if self._offen: # zweiter Klick schliesst wieder + self.zuklappen() + return + pop = tk.Toplevel(self) + pop.overrideredirect(True) + # Der Rahmen ist der Hintergrund des Toplevels, einen Pixel breit - + # sonst zeichnet Windows einen weissen Rand um die Liste. + pop.configure(bg=self.f.col_line) + innen = tk.Frame(pop, bg=self.f.col_field) + innen.pack(padx=1, pady=1) + + for beschriftung, befehl in eintraege: + if beschriftung == "-": + tk.Frame(innen, bg=self.f.col_line, height=1).pack(fill="x", pady=3) + continue + eintrag = tk.Label(innen, text=beschriftung, bg=self.f.col_field, + fg=self.f.col_fg, anchor="w", padx=16, pady=5) + eintrag.pack(fill="x") + eintrag.bind("", + lambda e, w=eintrag: w.configure(bg=self.f.col_sel)) + eintrag.bind("", + lambda e, w=eintrag: w.configure(bg=self.f.col_field)) + eintrag.bind("", lambda e, c=befehl: self._ausfuehren(c)) + + pop.update_idletasks() + pop.geometry(f"+{anker.winfo_rootx()}" + f"+{anker.winfo_rooty() + anker.winfo_height()}") + self._offen = pop + pop.grab_set() + pop.bind("", self._klick_ausserhalb, add="+") + pop.bind("", lambda e: self.zuklappen()) + faerben = getattr(self.f, "_dark_titlebar", None) + if faerben: + faerben(pop) + + def _klick_ausserhalb(self, ereignis): + pop = self._offen + if not pop: + return + if (ereignis.x_root < pop.winfo_rootx() + or ereignis.x_root > pop.winfo_rootx() + pop.winfo_width() + or ereignis.y_root < pop.winfo_rooty() + or ereignis.y_root > pop.winfo_rooty() + pop.winfo_height()): + self.zuklappen() + + def _ausfuehren(self, befehl): + # Erst zuklappen, dann ausfuehren: sonst haelt der grab_set der Liste + # einen Dialog fest, der aus dem Eintrag heraus aufgeht. + self.zuklappen() + if befehl: + befehl() + + def zuklappen(self): + if self._offen: + try: + self._offen.grab_release() + self._offen.destroy() + except tk.TclError: + pass + self._offen = None diff --git a/pruef_darstellung.py b/pruef_darstellung.py new file mode 100644 index 0000000..be96ebd --- /dev/null +++ b/pruef_darstellung.py @@ -0,0 +1,196 @@ +# -*- 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" + +GEPRUEFT WIRD DREIERLEI + +1. Die Einstellung wirkt. "hell" ist hell, "dunkel" ist dunkel, "auto" folgt + Windows. + +2. Beim Umschalten bleibt jede Eingabe stehen. Das ist der Punkt, an dem es + beim Bauen schiefging: die Mengen der Leistungszeilen hatte ich einzeln + gesichert, Kundenname und Rechnungsnummer nicht - die _build_-Methoden + legen ihre Variablen selbst an, beim Neubau standen also leere da. Wer + waehrend einer halb getippten Rechnung auf dunkel stellt, darf sie nicht + verlieren. + +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 json +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) + +fehler = [] + + +def pruefe(bedingung, text): + zeile = (" OK | " if bedingung else "FEHLER | ") + text + kod = sys.stdout.encoding or "utf-8" + print(zeile.encode(kod, "replace").decode(kod)) + if not bedingung: + fehler.append(text) + + +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) + +import config # noqa: E402 + +# Auf einer Kopie arbeiten - die echte Konfiguration wird nicht angefasst. +tmp = tempfile.mkdtemp(prefix="darstellung_") +shutil.copy(os.path.join(QUELLE, "config.json"), os.path.join(tmp, "config.json")) +config.CONFIG_PATH = os.path.join(tmp, "config.json") +roh = json.load(open(config.CONFIG_PATH, encoding="utf-8")) +roh["nummer_eingerichtet"] = True +roh.pop("darstellung", None) +json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), + ensure_ascii=False, indent=2) + +import app as A # noqa: E402 +import theme # noqa: E402 + +f = A.RechnungsApp() + + +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") + +# ---- Eingaben, die den Wechsel ueberleben muessen ------------------------ +f.k_vorname.set("Erika") +f.k_name.set("Mustermann") +f.k_strasse.set("Musterweg 12") +f.k_plzort.set("04416 Markkleeberg") +f.r_nummer.set("2026-099") +f.r_anreise.set("01.09.2026") +erste = sorted(f.anzahl_vars)[0] +f.anzahl_vars[erste].set("3") +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 + config.save_config(f.cfg) + 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.k_name.get() == "Mustermann", + f"nach dem Wechsel steht der Kundenname noch da ({f.k_name.get()!r})") + pruefe(f.r_nummer.get() == "2026-099", + f"und die Rechnungsnummer ({f.r_nummer.get()!r})") + pruefe(f.k_plzort.get() == "04416 Markkleeberg", + f"und die Anschrift ({f.k_plzort.get()!r})") + pruefe(f.r_anreise.get() == "01.09.2026", + f"und das Anreisedatum ({f.r_anreise.get()!r})") + pruefe(f.anzahl_vars.get(erste) is not None + and f.anzahl_vars[erste].get() == "3", + "und die eingetragene Menge in der Leistungszeile") + + # 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]})") + +# ---- Zurueck auf automatisch -------------------------------------------- +f.cfg["darstellung"] = "auto" +config.save_config(f.cfg) +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" +config.save_config(f.cfg) +f.destroy() +f = A.RechnungsApp() +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") diff --git a/theme.py b/theme.py index ce12a81..6da642c 100644 --- a/theme.py +++ b/theme.py @@ -5,13 +5,74 @@ Native tk-Menues und -Popups lassen sich unter Windows nicht einfaerben, darum baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben. """ +import ctypes.wintypes +import os import sys import tkinter as tk from tkinter import font as tkfont from tkinter import messagebox from tkinter import ttk -import config + +# --------------------------------------------------------------------------- +# Diese Datei liegt in BEIDEN Programmen gleich und darf deshalb NICHTS aus +# einem der beiden importieren. Frueher holte sie sich die Ordner und den +# Testlauf-Schalter aus config.py - die gibt es aber nur im Rechnungstool, und +# beim Uebernehmen ins Steuerrechnungstool flog sie beim Import auseinander. +# Die zwei Dinge, die gebraucht werden, stehen jetzt hier. +# --------------------------------------------------------------------------- +def _ordner(): + """Alle Orte, an denen assets/ liegen kann - Quelle wie gepackte EXE.""" + hier = os.path.dirname(os.path.abspath(__file__)) + aus_exe = getattr(sys, "_MEIPASS", "") + neben_exe = (os.path.dirname(os.path.abspath(sys.executable)) + if getattr(sys, "frozen", False) else "") + return [p for p in (aus_exe, neben_exe, hier) if p] + + +def titelleiste_faerben(fenster, dunkel): + """Die Titelleiste des Fensters mit einfaerben (Windows 10 ab 1809). + + Tk kann das nicht - die Leiste mit Symbol, Titel und den drei Knoepfen + gehoert Windows, nicht dem Programm. Windows nimmt dafuer aber einen + Hinweis entgegen: DWMWA_USE_IMMERSIVE_DARK_MODE. Ohne den steht ein weisser + Balken ueber einem dunklen Fenster (sein Befund am 07.09.2026: "der icon + und titel leiste auch ... die sind noch hell"). + + Die Nummer des Attributs hat sich einmal geaendert: 19 in den fruehen + 1809er-Fassungen, 20 seit 2004. Beide werden gesetzt; die falsche wird + ignoriert. + """ + 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) + gesetzt = False + for attribut in (20, 19): + ergebnis = ctypes.windll.dwmapi.DwmSetWindowAttribute( + ctypes.wintypes.HWND(hwnd), ctypes.c_int(attribut), + ctypes.byref(wert), ctypes.sizeof(wert)) + gesetzt = gesetzt or ergebnis == 0 + # Den Rahmen neu zeichnen lassen, ohne Groesse, Platz, Reihenfolge oder + # Fokus anzufassen. Ohne das bleibt die Leiste stehen, wie sie war - + # die Umstellung wirkt dann erst beim naechsten Start. + SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED + ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP) + return gesetzt + except Exception: # noqa: BLE001 - ohne dunkle Titelleiste laeuft es weiter + return False + + +def testlauf(): + """Faehrt gerade ein Pruefstand das Programm? + + Muss mit der Definition in app.py uebereinstimmen - beide Programme + schalten ueber `--test` oder die Umgebungsvariable. + """ + return ("--test" in sys.argv) or os.environ.get("CAMPINGHOF_TEST") == "1" # Schrift: Roboto, so wie in den Web-Oberflaechen. Sie wird NICHT auf dem @@ -35,10 +96,9 @@ def schrift_vorbereiten(): if not sys.platform.startswith("win"): return SCHRIFT_NAME import ctypes - import os FR_PRIVATE = 0x10 geladen = 0 - for basis in (config.BASE_DIR, config.RESOURCE_DIR, os.path.dirname(os.path.abspath(__file__))): + for basis in _ordner(): ordner = os.path.join(basis, "assets", "fonts") if not os.path.isdir(ordner): continue @@ -100,15 +160,48 @@ def windows_dark_mode(): return False +# --------------------------------------------------------------------------- +# Welche Darstellung ist gewuenscht? +# --------------------------------------------------------------------------- +# Drei Moeglichkeiten, gespeichert in der config unter "darstellung": +# +# "auto" - folgt Windows (Voreinstellung, wie es bisher immer war) +# "hell" - immer hell +# "dunkel" - immer dunkel +# +# "auto" ist die Voreinstellung, weil sich das Programm dann verhaelt wie jedes +# andere: wer sein Windows abends auf dunkel stellt, bekommt es auch hier. +MODI = ("auto", "hell", "dunkel") +MODUS_NAMEN = {"auto": "Automatisch (wie Windows)", + "hell": "Hell", + "dunkel": "Dunkel"} + + +def modus(cfg): + """Der eingestellte Modus - immer einer aus MODI.""" + wert = str((cfg or {}).get("darstellung", "auto")).strip().lower() + return wert if wert in MODI else "auto" + + +def ist_dunkel(cfg): + """Soll dunkel gezeichnet werden? Die eine Stelle, die das entscheidet.""" + m = modus(cfg) + if m == "hell": + return False + if m == "dunkel": + return True + return windows_dark_mode() + + class ThemeMixin: """Farben setzen. Erwartet ein tk.Tk als Basis.""" # ----------------------------------------------------------------- Theme def _apply_theme(self): - """Farben passend zum Windows-Theme (hell/dunkel) setzen.""" + """Farben passend zur Einstellung setzen (hell, dunkel oder wie Windows).""" schrift_vorbereiten() # Roboto, sonst Segoe UI - dark = windows_dark_mode() + dark = ist_dunkel(getattr(self, "cfg", None)) self._dark_mode = dark # Die Farben sind die aus seinen anderen Sachen (FiveM-Oberflaechen, # Adminpage, Webseiten): Tuerkis #06C6A4 auf sehr dunklen, gestuften @@ -147,6 +240,15 @@ class ThemeMixin: self.col_zebra = "#e4e8ee" self.configure(bg=self.col_bg) + # Die Titelleiste gehoert Windows - sie muss eigens umgestellt werden. + titelleiste_faerben(self, dark) + # Und das Fenstersymbol: in der dunklen Titelleiste geht die dunkle + # Haelfte des Zeichens sonst unter. Es haengt hier und nicht im + # __init__, damit es beim Umschalten mitgeht - dort wird _apply_theme + # erneut gerufen, das __init__ nicht. + symbol = getattr(self, "_fenstersymbol", None) + if callable(symbol): + symbol() style = ttk.Style(self) try: style.theme_use("clam") @@ -292,6 +394,22 @@ class ThemeMixin: arrowcolor=fg, relief="flat", gripcount=0) style.map(sbs, background=[("active", accent)], troughcolor=[("disabled", bg)]) + # Treeview - die Tabelle des Steuerrechnungstools. Ohne diesen Block + # bleibt sie weiss im dunklen Fenster: ttk zeichnet Listen nicht ueber + # den "."-Grundstil, sondern nur ueber den eigenen. + style.configure("Treeview", background=self.col_card, + fieldbackground=self.col_card, foreground=fg, + bordercolor=self.col_line, rowheight=24, + font=SCHRIFT) + style.configure("Treeview.Heading", background=bg, foreground=accent, + relief="flat", font=(SCHRIFT_NAME, 9, "bold")) + style.map("Treeview.Heading", background=[("active", self.col_sel)]) + style.map("Treeview", + background=[("selected", self.col_sel)], + foreground=[("selected", fg)]) + # Der Rahmen um die Liste - ttk zeichnet ihn sonst im Systemgrau. + style.configure("Treeview.field", bordercolor=self.col_line) + style.configure("TSeparator", background=self.col_line) # Reiter (Campingrechnung / Montage): ohne eigene Farben zeichnet clam den # AKTIVEN Reiter dunkler als die ruhenden - man sieht dann nicht, wo man @@ -307,7 +425,10 @@ class ThemeMixin: # Reiter hell und sieht aus wie der ausgewaehlte. style.configure("TNotebook.Tab", background=bg, lightcolor=bg, darkcolor=bg, foreground=self.col_muted, bordercolor=self.col_line, - padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=1) + # Schmal genug, dass zwoelf Monatsreiter nebeneinander + # passen: bei (20, 9) waren "September" und "Dezember" + # im Steuerrechnungstool abgeschnitten. + padding=(9, 7), font=(SCHRIFT_NAME, 10), borderwidth=1) style.map("TNotebook.Tab", background=[("selected", self.col_card), ("active", mischen(accent, bg, 0.12))], @@ -356,7 +477,7 @@ class ThemeMixin: und Tastatur fest, waehrend nebenan jemand schreibt. """ d.configure(bg=self.col_bg) - if config.testlauf(): + if testlauf(): # Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set - # ein Dialog, der Maus und Tastatur festhaelt, blockiert den # Menschen, der nebenher schreibt. @@ -370,22 +491,16 @@ class ThemeMixin: d.after(20, lambda: self._dark_titlebar(d)) def _dark_titlebar(self, win): - """Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode).""" - if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)): - return - try: - import ctypes - win.update_idletasks() - hwnd = ctypes.windll.user32.GetParent(win.winfo_id()) - val = ctypes.c_int(1) - for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19) - ctypes.windll.dwmapi.DwmSetWindowAttribute( - hwnd, attr, ctypes.byref(val), ctypes.sizeof(val)) - # Frame neu zeichnen lassen (ohne Flackern/Fokusverlust) - SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED - ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP) - except Exception as e: # noqa: BLE001 - print("[theme] Titelleiste:", e) + """Die Titelleiste eines Fensters an die Darstellung anpassen. + + Frueher machte diese Methode die Leiste nur DUNKEL und tat sonst + nichts. Solange das Programm der Windows-Einstellung folgte, fiel das + nicht auf - seit man hell und dunkel selbst waehlen kann, sehr wohl: + wer von dunkel auf hell stellte, behielt eine schwarze Titelleiste ueber + einem hellen Fenster. Sie zeigt jetzt in beide Richtungen und ist nur + noch eine Weiterleitung; die Arbeit macht titelleiste_faerben(). + """ + titelleiste_faerben(win, bool(getattr(self, "_dark_mode", False))) class MeldungMixin: @@ -417,7 +532,7 @@ class MeldungMixin: def melden(self, titel, text, art="warnung"): """Eine Meldung mit einem OK-Knopf.""" - if config.testlauf(): + if testlauf(): {"info": messagebox.showinfo, "warnung": messagebox.showwarning, "fehler": messagebox.showerror}.get(art, messagebox.showwarning)(titel, text) @@ -426,7 +541,7 @@ class MeldungMixin: def frage(self, titel, text="", ja="Ja", nein="Nein", vorgabe=True, **_kw): """Ja/Nein. Gibt True oder False zurueck - wie askyesno.""" - if config.testlauf(): + if testlauf(): return bool(messagebox.askyesno(titel, text)) return bool(self._meldungsfenster(titel, text, "frage", knoepfe=(ja, nein), vorgabe=vorgabe)) diff --git a/version.py b/version.py index 7df3ba3..747e13b 100644 --- a/version.py +++ b/version.py @@ -10,11 +10,11 @@ und NUR hier; alles andere (Info-Dialog, Updater, Release) liest sie von hier. Regel ----- Die Nummer wird beim Ausliefern erhoeht und entspricht dem Release-Tag ohne das -"v": Tag `v1.7.1` -> VERSION "1.7.1". Wer das eine ohne das andere aendert, +"v": Tag `v1.8.0` -> VERSION "1.8.0". Wer das eine ohne das andere aendert, baut dem Updater eine Falle. """ -VERSION = "1.7.1" +VERSION = "1.8.0" # Zu welchem Repository dieses Programm gehoert - der Updater sucht dort. REPOSITORY = "TheMockTv/rechnungstool" diff --git a/wasserzeichen.py b/wasserzeichen.py index 935ba0a..e582a93 100644 --- a/wasserzeichen.py +++ b/wasserzeichen.py @@ -45,14 +45,51 @@ def pfad(name): return "" -def symbol_setzen(fenster, melden=print): +def symbol_datei_dunkel(): + """Pfad zur aufgehellten ICO - wird angelegt, wenn sie fehlt. + + Sein Befund am 07.09.2026: *"icon musst wenn es dunkel ist mit ändern auf + das hellere geht sonst mit unter"*. Die dunkle Haelfte des Zeichens liegt + genau auf der Farbe der dunklen Titelleiste. + + `iconbitmap` braucht eine DATEI - ein Bild im Speicher genuegt nicht. + Deshalb wird sie einmal neben die normale gelegt. Gelingt das nicht (kein + Schreibrecht im Programmordner), landet sie im Temp-Ordner; und wenn auch + das nicht geht, bleibt es beim normalen Symbol. + """ + fertig = pfad("programm_dunkel.ico") + if fertig: + return fertig + quelle = pfad("programm_logo.png") + if not quelle: + return "" + ziel = os.path.join(os.path.dirname(quelle), "programm_dunkel.ico") + try: + from PIL import Image + bild = _fuer_dunkel(Image.open(quelle).convert("RGBA")) + groessen = [(g, g) for g in (16, 24, 32, 48, 64, 128, 256)] + try: + bild.save(ziel, sizes=groessen) + except OSError: + import tempfile + ziel = os.path.join(tempfile.gettempdir(), "programm_dunkel.ico") + bild.save(ziel, sizes=groessen) + return ziel + except Exception: # noqa: BLE001 - ohne helles Symbol laeuft es weiter + return "" + + +def symbol_setzen(fenster, melden=print, dunkel=False): """Das Programmsymbol auf ein Fenster legen. True bei Erfolg. Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes hat. + + In der dunklen Darstellung wird die aufgehellte Fassung genommen, sonst + verschwindet die dunkle Haelfte des Zeichens in der dunklen Titelleiste. """ - p = pfad("programm.ico") + p = (symbol_datei_dunkel() if dunkel else "") or pfad("programm.ico") if not p: melden("[symbol] assets/programm.ico nicht gefunden") return False @@ -64,7 +101,45 @@ def symbol_setzen(fenster, melden=print): return False -def hole(groesse=240, deckkraft=0.06, master=None, melden=print): +def _fuer_dunkel(bild): + """Das Logo fuer die dunkle Darstellung aufhellen. + + Sein Befund am 07.09.2026: *"wir muessen mal schauen das wir die svg fuer + das dunkle heller machen weil die geht nun unter"*. + + Gemessen sind es zwei Farben: das Petrol #004860 (Helligkeit 53) und ein + fast schwarzes #181818 (Helligkeit 24). Der zweite Wert liegt genau auf der + Kachelfarbe #15171b - deshalb verschwindet die Haelfte des Zeichens. + + Umgestellt wird nur die HELLIGKEIT, Farbton und Saettigung bleiben: sonst + waere es nicht mehr sein Logo, sondern ein anderes. Aus dunkel wird hell, + aus hell dunkel (L -> 1-L), und weil eine reine Umkehr das kraeftige Petrol + fast weiss werden liesse, wird sie auf 0.78 gedeckelt. + + Die Kanten (halbdurchsichtige Punkte aus dem Skalieren) gehen denselben Weg + - deshalb Pixel fuer Pixel und nicht ein Farbtausch mit Toleranz. + """ + import colorsys + bild = bild.convert("RGBA") + punkte = list(bild.getdata()) + neu = [] + zwischen = {} + for r, g, b, a in punkte: + if a == 0: + neu.append((r, g, b, a)) + continue + schluessel = (r, g, b) + if schluessel not in zwischen: + h, l, s2 = colorsys.rgb_to_hls(r / 255, g / 255, b / 255) + hell = min(1.0 - l, 0.78) + r2, g2, b2 = colorsys.hls_to_rgb(h, hell, s2) + zwischen[schluessel] = (int(r2 * 255), int(g2 * 255), int(b2 * 255)) + neu.append(zwischen[schluessel] + (a,)) + bild.putdata(neu) + return bild + + +def hole(groesse=240, deckkraft=0.06, master=None, melden=print, dunkel=False): """Das Logo blass als PhotoImage - oder None. `deckkraft` 0..1, klein halten. Ein Wasserzeichen, das man LIEST, stoert @@ -77,7 +152,7 @@ def hole(groesse=240, deckkraft=0.06, master=None, melden=print): fenster = master if master is not None else tk._default_root if fenster is None: return None - schluessel = (id(fenster), groesse, round(deckkraft, 3)) + schluessel = (id(fenster), groesse, round(deckkraft, 3), bool(dunkel)) if schluessel in _lager: return _lager[schluessel] p = pfad("programm_logo.png") @@ -87,6 +162,8 @@ def hole(groesse=240, deckkraft=0.06, master=None, melden=print): try: from PIL import Image, ImageTk bild = Image.open(p).convert("RGBA").resize((groesse, groesse), Image.LANCZOS) + if dunkel: + bild = _fuer_dunkel(bild) bild.putalpha(bild.getchannel("A").point(lambda w: int(w * deckkraft))) fertig = ImageTk.PhotoImage(bild, master=fenster) except Exception as e: # noqa: BLE001 - ein Wasserzeichen darf nichts blockieren @@ -96,11 +173,11 @@ def hole(groesse=240, deckkraft=0.06, master=None, melden=print): return fertig -def kopf(hoehe=28, master=None, melden=print): +def kopf(hoehe=28, master=None, melden=print, dunkel=False): """Das Logo fuer die Kopfzeile - volle Deckkraft, kleine Hoehe. Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon einen Schalter, den man beim Lesen nachschlagen muss. """ - return hole(hoehe, 1.0, master=master, melden=melden) + return hole(hoehe, 1.0, master=master, melden=melden, dunkel=dunkel)