rechnungstool/menueleiste.py
TheMockTv f7801f46f0 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
2026-09-07 00:34:16 +02:00

120 lines
4.7 KiB
Python

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