Darstellung waehlbar: hell, dunkel oder wie Windows
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
20876102d5
commit
f7801f46f0
11 changed files with 724 additions and 109 deletions
|
|
@ -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
|
**Nicht von den Arbeiten am 06.09.2026 verursacht** — bei der Ausgangsmessung vor dem Umbau bereits
|
||||||
rot, nur mit anderem Text.
|
rot, nur mit anderem Text.
|
||||||
|
|
||||||
`pruef_wache.py` erwartet nach einem Push des Rechnungstools in der Statuszeile den Hinweis
|
`pruef_wache.py` erwartete nach einem Push des Rechnungstools in der Statuszeile den Hinweis
|
||||||
**„Rechnungstool meldet"**. Tatsächlich steht dort das Ergebnis des anschließenden Einlesens
|
**„Rechnungstool meldet"**. Tatsächlich stand 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.
|
|
||||||
|
|
||||||
**Zu klären:** Ist das ein Fehler im Programm (die Meldung soll stehen bleiben, das Ergebnis
|
**Die Frage war:** Fehler im Programm oder veraltete Erwartung im Prüfstand?
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ a = Analysis(
|
||||||
# nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt
|
# nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt
|
||||||
# das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders
|
# das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders
|
||||||
# aus als hier.
|
# 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/programm_logo.png', 'assets'),('assets/logo.png', 'assets'),
|
||||||
('assets/fonts', 'assets/fonts'),
|
('assets/fonts', 'assets/fonts'),
|
||||||
# Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe
|
# Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ a = Analysis(
|
||||||
# nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt
|
# nur fuer den Prozess geladen (theme.schrift_vorbereiten). Fehlt sie, faellt
|
||||||
# das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders
|
# das Programm still auf Segoe UI zurueck - dann sieht es beim Kunden anders
|
||||||
# aus als hier.
|
# 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/programm_logo.png', 'assets'),('assets/logo.png', 'assets'),
|
||||||
('assets/fonts', 'assets/fonts'),
|
('assets/fonts', 'assets/fonts'),
|
||||||
# Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe
|
# Die Icons (Material Symbols vom Hub) - ohne sie zeigen die Knoepfe
|
||||||
|
|
|
||||||
221
app.py
221
app.py
|
|
@ -29,6 +29,7 @@ from tkinter import ttk, messagebox
|
||||||
|
|
||||||
import api
|
import api
|
||||||
import config
|
import config
|
||||||
|
import menueleiste
|
||||||
import berechnung
|
import berechnung
|
||||||
import gemeinsam
|
import gemeinsam
|
||||||
import hinweis
|
import hinweis
|
||||||
|
|
@ -145,7 +146,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
def _build_menu(self):
|
def _build_menu(self):
|
||||||
# Vollstaendig eigene Menueleiste + eigene Dropdowns -> komplett theme-konform
|
# Vollstaendig eigene Menueleiste + eigene Dropdowns -> komplett theme-konform
|
||||||
# (native tk-Menubar/Popups lassen sich unter Windows nicht einfaerben).
|
# (native tk-Menubar/Popups lassen sich unter Windows nicht einfaerben).
|
||||||
self._open_pop = None
|
|
||||||
self._menus = {
|
self._menus = {
|
||||||
"Datei": [
|
"Datei": [
|
||||||
("Vorschau ansehen", self.vorschau_zeigen),
|
("Vorschau ansehen", self.vorschau_zeigen),
|
||||||
|
|
@ -163,72 +163,16 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
("Rechnungsnummer…", self.dlg_nummer),
|
("Rechnungsnummer…", self.dlg_nummer),
|
||||||
("Speicherort der Rechnungen…", self.dlg_speicherort),
|
("Speicherort der Rechnungen…", self.dlg_speicherort),
|
||||||
("Gemeinsamer Ordner (Nummernbuch)…", self.dlg_gemeinsam),
|
("Gemeinsamer Ordner (Nummernbuch)…", self.dlg_gemeinsam),
|
||||||
|
("-", None),
|
||||||
|
("Darstellung…", self.dlg_darstellung),
|
||||||
],
|
],
|
||||||
"Hilfe": [("Info", self._info)],
|
"Hilfe": [("Info", self._info)],
|
||||||
}
|
}
|
||||||
bar = tk.Frame(self, bg=self.col_bg)
|
self.menue = menueleiste.Menueleiste(self, self._menus, farben=self)
|
||||||
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("<Enter>", lambda e, w=lbl: w.configure(bg=self.col_sel))
|
|
||||||
lbl.bind("<Leave>", lambda e, w=lbl: w.configure(bg=self.col_bg))
|
|
||||||
lbl.bind("<Button-1>", lambda e, b=lbl, it=items: self._menu_popup(b, it))
|
|
||||||
ttk.Separator(self, orient="horizontal").pack(side="top", fill="x")
|
|
||||||
# bind statt bind_all: sonst feuert Strg+P auch aus einem offenen
|
# bind statt bind_all: sonst feuert Strg+P auch aus einem offenen
|
||||||
# Dialog heraus und schreibt im Formular dahinter eine Rechnung.
|
# Dialog heraus und schreibt im Formular dahinter eine Rechnung.
|
||||||
self.bind("<Control-p>", lambda e: self.pdf_erstellen())
|
self.bind("<Control-p>", 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("<Enter>", lambda e, w=it: w.configure(bg=self.col_sel))
|
|
||||||
it.bind("<Leave>", lambda e, w=it: w.configure(bg=self.col_field))
|
|
||||||
it.bind("<Button-1>", 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("<Button-1>", self._maybe_close_pop, add="+")
|
|
||||||
pop.bind("<Escape>", 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):
|
def _build_flaeche(self):
|
||||||
"""Die Kaesten und der Knopf, mit dem man sie verschieben darf.
|
"""Die Kaesten und der Knopf, mit dem man sie verschieben darf.
|
||||||
|
|
||||||
|
|
@ -504,6 +448,134 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
|
|
||||||
self._render_positionszeilen()
|
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):
|
def _umbruch_nachziehen(self, runde=0, vorige_breite=None):
|
||||||
"""Den Textumbruch der Leistungsspalte auf ihre echte Breite setzen.
|
"""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
|
# Das Programmlogo neben der Marke - klein, immer sichtbar. Es steht
|
||||||
# rechts bei "powered by ravokk", weil dort die Kennung des Werkzeugs
|
# rechts bei "powered by ravokk", weil dort die Kennung des Werkzeugs
|
||||||
# sitzt und nicht die des Kunden.
|
# 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:
|
if self._kopf_bild is not None:
|
||||||
ttk.Label(status, image=self._kopf_bild).pack(side="right", padx=(0, 6))
|
ttk.Label(status, image=self._kopf_bild).pack(side="right", padx=(0, 6))
|
||||||
status.pack(side="bottom", fill="x")
|
status.pack(side="bottom", fill="x")
|
||||||
|
|
@ -753,7 +826,8 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
|
|
||||||
def _wasserzeichen_setzen(self, groesse=240):
|
def _wasserzeichen_setzen(self, groesse=240):
|
||||||
"""Das Programmlogo blass hinter die Arbeitsflaeche legen."""
|
"""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:
|
if bild is None:
|
||||||
return
|
return
|
||||||
# Die Referenz MUSS bleiben - sonst raeumt Python das Bild weg und Tk
|
# 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):
|
def _fenstersymbol(self):
|
||||||
"""Das Programmsymbol auf das Fenster legen."""
|
"""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")
|
print("[symbol] assets/programm.ico nicht gefunden")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -1145,7 +1219,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
except Exception as e: # noqa: BLE001
|
except Exception as e: # noqa: BLE001
|
||||||
print("[push]", e)
|
print("[push]", e)
|
||||||
finally:
|
finally:
|
||||||
self.after(250, self._push_abholen)
|
self._wieder(250, self._push_abholen)
|
||||||
|
|
||||||
def _nachtragen(self):
|
def _nachtragen(self):
|
||||||
"""Alles, was schon im Rechnungsordner liegt, ins Buch schreiben.
|
"""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}"
|
return jahr, zaehler, f"{jahr}-A{zaehler:03d}"
|
||||||
|
|
||||||
# ------------------------------------------------- Bedienung ueber die API
|
# ------------------------------------------------- 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):
|
def _api_pumpe(self):
|
||||||
"""Holt Aktionen aus der Warteschlange und fuehrt sie HIER aus.
|
"""Holt Aktionen aus der Warteschlange und fuehrt sie HIER aus.
|
||||||
|
|
||||||
|
|
@ -1407,7 +1498,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
try:
|
try:
|
||||||
self._api_schlange.abarbeiten(self._api_aktion)
|
self._api_schlange.abarbeiten(self._api_aktion)
|
||||||
finally:
|
finally:
|
||||||
self.after(150, self._api_pumpe)
|
self._wieder(150, self._api_pumpe)
|
||||||
|
|
||||||
def _api_aktion(self, name, daten):
|
def _api_aktion(self, name, daten):
|
||||||
"""Eine Bedienung ausfuehren. Der Schluessel ist schon geprueft (api.py).
|
"""Eine Bedienung ausfuehren. Der Schluessel ist schon geprueft (api.py).
|
||||||
|
|
|
||||||
BIN
assets/programm_dunkel.ico
Normal file
BIN
assets/programm_dunkel.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 57 KiB |
9
bauen.py
9
bauen.py
|
|
@ -82,6 +82,15 @@ def main():
|
||||||
for ordner in ("build", "dist", "setup", "release"):
|
for ordner in ("build", "dist", "setup", "release"):
|
||||||
shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True)
|
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"],
|
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"],
|
||||||
f"{stamm} {version.VERSION}")
|
f"{stamm} {version.VERSION}")
|
||||||
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"],
|
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"],
|
||||||
|
|
|
||||||
120
menueleiste.py
Normal file
120
menueleiste.py
Normal file
|
|
@ -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("<Enter>", lambda e, w=lbl: w.configure(bg=self.f.col_sel))
|
||||||
|
lbl.bind("<Leave>", lambda e, w=lbl: w.configure(bg=self.f.col_bg))
|
||||||
|
lbl.bind("<Button-1>",
|
||||||
|
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("<Enter>",
|
||||||
|
lambda e, w=eintrag: w.configure(bg=self.f.col_sel))
|
||||||
|
eintrag.bind("<Leave>",
|
||||||
|
lambda e, w=eintrag: w.configure(bg=self.f.col_field))
|
||||||
|
eintrag.bind("<Button-1>", 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("<Button-1>", self._klick_ausserhalb, add="+")
|
||||||
|
pop.bind("<Escape>", 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
|
||||||
196
pruef_darstellung.py
Normal file
196
pruef_darstellung.py
Normal file
|
|
@ -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")
|
||||||
165
theme.py
165
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.
|
baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import ctypes.wintypes
|
||||||
|
import os
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import font as tkfont
|
from tkinter import font as tkfont
|
||||||
from tkinter import messagebox
|
from tkinter import messagebox
|
||||||
from tkinter import ttk
|
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
|
# 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"):
|
if not sys.platform.startswith("win"):
|
||||||
return SCHRIFT_NAME
|
return SCHRIFT_NAME
|
||||||
import ctypes
|
import ctypes
|
||||||
import os
|
|
||||||
FR_PRIVATE = 0x10
|
FR_PRIVATE = 0x10
|
||||||
geladen = 0
|
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")
|
ordner = os.path.join(basis, "assets", "fonts")
|
||||||
if not os.path.isdir(ordner):
|
if not os.path.isdir(ordner):
|
||||||
continue
|
continue
|
||||||
|
|
@ -100,15 +160,48 @@ def windows_dark_mode():
|
||||||
return False
|
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:
|
class ThemeMixin:
|
||||||
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
|
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
|
||||||
|
|
||||||
# ----------------------------------------------------------------- Theme
|
# ----------------------------------------------------------------- Theme
|
||||||
|
|
||||||
def _apply_theme(self):
|
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
|
schrift_vorbereiten() # Roboto, sonst Segoe UI
|
||||||
dark = windows_dark_mode()
|
dark = ist_dunkel(getattr(self, "cfg", None))
|
||||||
self._dark_mode = dark
|
self._dark_mode = dark
|
||||||
# Die Farben sind die aus seinen anderen Sachen (FiveM-Oberflaechen,
|
# Die Farben sind die aus seinen anderen Sachen (FiveM-Oberflaechen,
|
||||||
# Adminpage, Webseiten): Tuerkis #06C6A4 auf sehr dunklen, gestuften
|
# Adminpage, Webseiten): Tuerkis #06C6A4 auf sehr dunklen, gestuften
|
||||||
|
|
@ -147,6 +240,15 @@ class ThemeMixin:
|
||||||
self.col_zebra = "#e4e8ee"
|
self.col_zebra = "#e4e8ee"
|
||||||
|
|
||||||
self.configure(bg=self.col_bg)
|
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)
|
style = ttk.Style(self)
|
||||||
try:
|
try:
|
||||||
style.theme_use("clam")
|
style.theme_use("clam")
|
||||||
|
|
@ -292,6 +394,22 @@ class ThemeMixin:
|
||||||
arrowcolor=fg, relief="flat", gripcount=0)
|
arrowcolor=fg, relief="flat", gripcount=0)
|
||||||
style.map(sbs, background=[("active", accent)],
|
style.map(sbs, background=[("active", accent)],
|
||||||
troughcolor=[("disabled", bg)])
|
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)
|
style.configure("TSeparator", background=self.col_line)
|
||||||
# Reiter (Campingrechnung / Montage): ohne eigene Farben zeichnet clam den
|
# Reiter (Campingrechnung / Montage): ohne eigene Farben zeichnet clam den
|
||||||
# AKTIVEN Reiter dunkler als die ruhenden - man sieht dann nicht, wo man
|
# 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.
|
# Reiter hell und sieht aus wie der ausgewaehlte.
|
||||||
style.configure("TNotebook.Tab", background=bg, lightcolor=bg, darkcolor=bg,
|
style.configure("TNotebook.Tab", background=bg, lightcolor=bg, darkcolor=bg,
|
||||||
foreground=self.col_muted, bordercolor=self.col_line,
|
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",
|
style.map("TNotebook.Tab",
|
||||||
background=[("selected", self.col_card),
|
background=[("selected", self.col_card),
|
||||||
("active", mischen(accent, bg, 0.12))],
|
("active", mischen(accent, bg, 0.12))],
|
||||||
|
|
@ -356,7 +477,7 @@ class ThemeMixin:
|
||||||
und Tastatur fest, waehrend nebenan jemand schreibt.
|
und Tastatur fest, waehrend nebenan jemand schreibt.
|
||||||
"""
|
"""
|
||||||
d.configure(bg=self.col_bg)
|
d.configure(bg=self.col_bg)
|
||||||
if config.testlauf():
|
if testlauf():
|
||||||
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
|
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
|
||||||
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
|
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
|
||||||
# Menschen, der nebenher schreibt.
|
# Menschen, der nebenher schreibt.
|
||||||
|
|
@ -370,22 +491,16 @@ class ThemeMixin:
|
||||||
d.after(20, lambda: self._dark_titlebar(d))
|
d.after(20, lambda: self._dark_titlebar(d))
|
||||||
|
|
||||||
def _dark_titlebar(self, win):
|
def _dark_titlebar(self, win):
|
||||||
"""Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode)."""
|
"""Die Titelleiste eines Fensters an die Darstellung anpassen.
|
||||||
if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)):
|
|
||||||
return
|
Frueher machte diese Methode die Leiste nur DUNKEL und tat sonst
|
||||||
try:
|
nichts. Solange das Programm der Windows-Einstellung folgte, fiel das
|
||||||
import ctypes
|
nicht auf - seit man hell und dunkel selbst waehlen kann, sehr wohl:
|
||||||
win.update_idletasks()
|
wer von dunkel auf hell stellte, behielt eine schwarze Titelleiste ueber
|
||||||
hwnd = ctypes.windll.user32.GetParent(win.winfo_id())
|
einem hellen Fenster. Sie zeigt jetzt in beide Richtungen und ist nur
|
||||||
val = ctypes.c_int(1)
|
noch eine Weiterleitung; die Arbeit macht titelleiste_faerben().
|
||||||
for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19)
|
"""
|
||||||
ctypes.windll.dwmapi.DwmSetWindowAttribute(
|
titelleiste_faerben(win, bool(getattr(self, "_dark_mode", False)))
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
class MeldungMixin:
|
class MeldungMixin:
|
||||||
|
|
@ -417,7 +532,7 @@ class MeldungMixin:
|
||||||
|
|
||||||
def melden(self, titel, text, art="warnung"):
|
def melden(self, titel, text, art="warnung"):
|
||||||
"""Eine Meldung mit einem OK-Knopf."""
|
"""Eine Meldung mit einem OK-Knopf."""
|
||||||
if config.testlauf():
|
if testlauf():
|
||||||
{"info": messagebox.showinfo,
|
{"info": messagebox.showinfo,
|
||||||
"warnung": messagebox.showwarning,
|
"warnung": messagebox.showwarning,
|
||||||
"fehler": messagebox.showerror}.get(art, messagebox.showwarning)(titel, text)
|
"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):
|
def frage(self, titel, text="", ja="Ja", nein="Nein", vorgabe=True, **_kw):
|
||||||
"""Ja/Nein. Gibt True oder False zurueck - wie askyesno."""
|
"""Ja/Nein. Gibt True oder False zurueck - wie askyesno."""
|
||||||
if config.testlauf():
|
if testlauf():
|
||||||
return bool(messagebox.askyesno(titel, text))
|
return bool(messagebox.askyesno(titel, text))
|
||||||
return bool(self._meldungsfenster(titel, text, "frage",
|
return bool(self._meldungsfenster(titel, text, "frage",
|
||||||
knoepfe=(ja, nein), vorgabe=vorgabe))
|
knoepfe=(ja, nein), vorgabe=vorgabe))
|
||||||
|
|
|
||||||
|
|
@ -10,11 +10,11 @@ und NUR hier; alles andere (Info-Dialog, Updater, Release) liest sie von hier.
|
||||||
Regel
|
Regel
|
||||||
-----
|
-----
|
||||||
Die Nummer wird beim Ausliefern erhoeht und entspricht dem Release-Tag ohne das
|
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.
|
baut dem Updater eine Falle.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
VERSION = "1.7.1"
|
VERSION = "1.8.0"
|
||||||
|
|
||||||
# Zu welchem Repository dieses Programm gehoert - der Updater sucht dort.
|
# Zu welchem Repository dieses Programm gehoert - der Updater sucht dort.
|
||||||
REPOSITORY = "TheMockTv/rechnungstool"
|
REPOSITORY = "TheMockTv/rechnungstool"
|
||||||
|
|
|
||||||
|
|
@ -45,14 +45,51 @@ def pfad(name):
|
||||||
return ""
|
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.
|
"""Das Programmsymbol auf ein Fenster legen. True bei Erfolg.
|
||||||
|
|
||||||
Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das
|
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
|
steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes
|
||||||
hat.
|
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:
|
if not p:
|
||||||
melden("[symbol] assets/programm.ico nicht gefunden")
|
melden("[symbol] assets/programm.ico nicht gefunden")
|
||||||
return False
|
return False
|
||||||
|
|
@ -64,7 +101,45 @@ def symbol_setzen(fenster, melden=print):
|
||||||
return False
|
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.
|
"""Das Logo blass als PhotoImage - oder None.
|
||||||
|
|
||||||
`deckkraft` 0..1, klein halten. Ein Wasserzeichen, das man LIEST, stoert
|
`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
|
fenster = master if master is not None else tk._default_root
|
||||||
if fenster is None:
|
if fenster is None:
|
||||||
return None
|
return None
|
||||||
schluessel = (id(fenster), groesse, round(deckkraft, 3))
|
schluessel = (id(fenster), groesse, round(deckkraft, 3), bool(dunkel))
|
||||||
if schluessel in _lager:
|
if schluessel in _lager:
|
||||||
return _lager[schluessel]
|
return _lager[schluessel]
|
||||||
p = pfad("programm_logo.png")
|
p = pfad("programm_logo.png")
|
||||||
|
|
@ -87,6 +162,8 @@ def hole(groesse=240, deckkraft=0.06, master=None, melden=print):
|
||||||
try:
|
try:
|
||||||
from PIL import Image, ImageTk
|
from PIL import Image, ImageTk
|
||||||
bild = Image.open(p).convert("RGBA").resize((groesse, groesse), Image.LANCZOS)
|
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)))
|
bild.putalpha(bild.getchannel("A").point(lambda w: int(w * deckkraft)))
|
||||||
fertig = ImageTk.PhotoImage(bild, master=fenster)
|
fertig = ImageTk.PhotoImage(bild, master=fenster)
|
||||||
except Exception as e: # noqa: BLE001 - ein Wasserzeichen darf nichts blockieren
|
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
|
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.
|
"""Das Logo fuer die Kopfzeile - volle Deckkraft, kleine Hoehe.
|
||||||
|
|
||||||
Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein
|
Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein
|
||||||
Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon
|
Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon
|
||||||
einen Schalter, den man beim Lesen nachschlagen muss.
|
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)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue