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
221
app.py
221
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("<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")
|
||||
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("<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):
|
||||
"""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).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue