Darstellung: das Programm hatte ueberhaupt kein Theme
Sein Befund "und beim steuertool fehlt das theam in genze oder wird nicht durch die automatik gesetzt" "nur die einstellunden hilfe leiste die ist noch hell und der icon und titel leiste auch" Er hatte recht, und zwar wortwoertlich: dieses Programm lief im Tk-Standard, mit dreimal fest eingetragenem foreground="black". Es gab hier gar keine theme.py. Gebaut theme.py aus dem Rechnungstool uebernommen und dabei von config.py geloest - eine Datei, die in BEIDEN Programmen liegt, darf nichts aus einem der beiden importieren. Beim ersten Uebernehmen flog sie genau daran beim Import auseinander. Die Einstellungen liegen hier in der Journal-Tabelle, nicht in einer config.json. _einstellungen() ist die Bruecke: etwas mit .get() und []=, wie hinweis.py und theme.py es erwarten. Menueleiste aus menueleiste.py statt tk.Menu - ein natives Menue laesst sich unter Windows nicht einfaerben, das war der letzte helle Rest. Treeview-Stil in theme.py ergaenzt (ttk zeichnet Listen nicht ueber den Grundstil), Reiter schmaler, damit alle zwoelf Monatsnamen lesbar bleiben. Die Summenleisten in gui_monat.py holen ihre Farben jetzt vom Fenster statt sie fest zu tragen - sie waren ein weisser Block unten im dunklen Bild. Logo und Fenstersymbol werden fuer die dunkle Darstellung aufgehellt. ⚠️ Altfehler behoben: die Meldung des Rechnungstools stand nie pruef_wache lief mal gruen und mal rot - je nachdem, wann er hinsah. Das war kein Wackler im Pruefstand, sondern sein Symptom: meldet das Rechnungstool eine neue Rechnung, laeuft sofort das Einlesen an, und dessen Ergebnis landete in derselben Zeile. Der Benutzer sah NIE, dass etwas hereingekommen war. melde() kennt jetzt einen Vorrang in Sekunden; die Meldung des Rechnungstools bekommt sechs. Dreimal hintereinander stabil gruen. Ein wackelnder Pruefstand ist keine Nebensache, die man wegdrueckt. AUFGABE_STEUERSAETZE_DYNAMISCH.md Seine naechste Ansage notiert: die festen Saetze sollen raus, dynamisch per Tabelle wie im Rechnungstool. Mit dem, worauf dabei zu achten ist - ein Satz gilt ab einem DATUM, und die Saetze bereits erfasster Buchungen bleiben stehen (§ 147 AO). Fassung 1.9.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
e4619259cb
commit
9d7aa1eec7
12 changed files with 1309 additions and 60 deletions
601
theme.py
Normal file
601
theme.py
Normal file
|
|
@ -0,0 +1,601 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
"""Farben nach dem Windows-Theme (hell/dunkel) - inklusive Titelleiste.
|
||||
|
||||
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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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
|
||||
# Rechner installiert, sondern nur fuer diesen einen Prozess geladen (die
|
||||
# Windows-Funktion AddFontResourceEx mit FR_PRIVATE). Damit braucht niemand
|
||||
# Adminrechte, und nach dem Schliessen ist der Rechner wieder wie vorher.
|
||||
# Findet sich die Datei nicht, bleibt es bei Segoe UI - das Programm muss auch
|
||||
# ohne mitgelieferte Schrift laufen.
|
||||
SCHRIFT_NAME = "Segoe UI"
|
||||
SCHRIFT = (SCHRIFT_NAME, 10)
|
||||
KLEIN = (SCHRIFT_NAME, 9)
|
||||
_schrift_geladen = False
|
||||
|
||||
|
||||
def schrift_vorbereiten():
|
||||
"""Roboto laden und zur Schrift des Programms machen. Gibt den Namen zurueck."""
|
||||
global SCHRIFT_NAME, SCHRIFT, KLEIN, _schrift_geladen
|
||||
if _schrift_geladen:
|
||||
return SCHRIFT_NAME
|
||||
_schrift_geladen = True
|
||||
if not sys.platform.startswith("win"):
|
||||
return SCHRIFT_NAME
|
||||
import ctypes
|
||||
FR_PRIVATE = 0x10
|
||||
geladen = 0
|
||||
for basis in _ordner():
|
||||
ordner = os.path.join(basis, "assets", "fonts")
|
||||
if not os.path.isdir(ordner):
|
||||
continue
|
||||
for datei in sorted(os.listdir(ordner)):
|
||||
if datei.lower().endswith((".ttf", ".otf")):
|
||||
geladen += ctypes.windll.gdi32.AddFontResourceExW(
|
||||
os.path.join(ordner, datei), FR_PRIVATE, 0)
|
||||
if geladen:
|
||||
break
|
||||
if not geladen:
|
||||
print("[schrift] Roboto nicht gefunden - bleibe bei Segoe UI")
|
||||
return SCHRIFT_NAME
|
||||
# Ob Windows sie wirklich angenommen hat, verraet erst die Schriftenliste.
|
||||
if "Roboto" not in tkfont.families():
|
||||
print("[schrift] Roboto liess sich nicht laden - bleibe bei Segoe UI")
|
||||
return SCHRIFT_NAME
|
||||
SCHRIFT_NAME = "Roboto"
|
||||
SCHRIFT = (SCHRIFT_NAME, 10)
|
||||
KLEIN = (SCHRIFT_NAME, 9)
|
||||
for name in ("TkDefaultFont", "TkTextFont", "TkMenuFont", "TkHeadingFont",
|
||||
"TkIconFont", "TkTooltipFont"):
|
||||
try:
|
||||
tkfont.nametofont(name).configure(family=SCHRIFT_NAME, size=10)
|
||||
except tk.TclError:
|
||||
pass
|
||||
print("[schrift] Roboto geladen")
|
||||
return SCHRIFT_NAME
|
||||
|
||||
|
||||
def mischen(vorn, hinten, anteil):
|
||||
"""Wie 'rgba(vorn, anteil)' auf 'hinten' - nur ausgerechnet statt gemalt.
|
||||
|
||||
Tk kann keine halbdurchsichtigen Flaechen (kein backdrop-filter, keine
|
||||
Alpha-Kanaele auf Widgets). Seine Web-Oberflaechen bauen den Glas-Effekt aus
|
||||
genau zwei Zutaten: eine getoente Flaeche und ein feiner Rand in
|
||||
Akzent-Transparenz. Beides laesst sich vorher ausrechnen - das Ergebnis sieht
|
||||
identisch aus, solange der Grund einfarbig ist. Und der ist es hier.
|
||||
"""
|
||||
v = vorn.lstrip("#")
|
||||
h = hinten.lstrip("#")
|
||||
teile = []
|
||||
for i in (0, 2, 4):
|
||||
a = int(v[i:i + 2], 16)
|
||||
b = int(h[i:i + 2], 16)
|
||||
teile.append(round(a * anteil + b * (1 - anteil)))
|
||||
return "#%02x%02x%02x" % tuple(teile)
|
||||
|
||||
|
||||
def windows_dark_mode():
|
||||
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
|
||||
if not sys.platform.startswith("win"):
|
||||
return False
|
||||
try:
|
||||
import winreg
|
||||
with winreg.OpenKey(winreg.HKEY_CURRENT_USER,
|
||||
r"Software\Microsoft\Windows\CurrentVersion\Themes\Personalize") as k:
|
||||
return winreg.QueryValueEx(k, "AppsUseLightTheme")[0] == 0
|
||||
except OSError:
|
||||
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 zur Einstellung setzen (hell, dunkel oder wie Windows)."""
|
||||
schrift_vorbereiten() # Roboto, sonst Segoe UI
|
||||
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
|
||||
# Flaechen. Das PROGRAMM traegt diese Handschrift - die RECHNUNG traegt
|
||||
# die Farbe der jeweiligen Firma (Einstellungen -> Farbe der Rechnung).
|
||||
# Beides zu vermischen waere falsch: das Blatt gehoert dem Kunden.
|
||||
if dark:
|
||||
self.col_bg = "#0d0e10" # Fenster-Hintergrund (tiefstes Grau)
|
||||
self.col_card = "#15171b" # Kachel / Abschnitt
|
||||
self.col_fg = "#e8e8ea" # Schrift
|
||||
self.col_field = "#2c2e32" # Eingabefelder (heller als die Kachel)
|
||||
self.col_feldrand = "#57585b" # Rahmen der Eingabefelder
|
||||
self.col_sel = "#1f3d38" # aktiv/hover (Akzent, stark abgedunkelt)
|
||||
self.col_accent = "#06C6A4" # Akzent (Pcore-Tuerkis)
|
||||
self.col_accent_hell = "#08dbb5"
|
||||
self.col_auf_accent = "#08221d" # Schrift AUF der Akzentflaeche
|
||||
self.col_gefahr = "#e85a5a"
|
||||
self.col_line = "#25282e"
|
||||
self.col_scroll = "#2f333a"
|
||||
self.col_muted = "#8a8e96"
|
||||
self.col_zebra = "#232830" # jede zweite Zeile der Liste
|
||||
else:
|
||||
self.col_bg = "#eef0f3"
|
||||
self.col_card = "#ffffff"
|
||||
self.col_fg = "#14161a"
|
||||
self.col_field = "#ffffff"
|
||||
self.col_feldrand = "#b9bec6"
|
||||
self.col_sel = "#d7f5ee"
|
||||
self.col_accent = "#04886f" # dunkler, damit es auf Weiss lesbar ist
|
||||
self.col_accent_hell = "#06C6A4"
|
||||
self.col_auf_accent = "#ffffff"
|
||||
self.col_gefahr = "#c0392b"
|
||||
self.col_line = "#d8dce1"
|
||||
self.col_scroll = "#c2c6cc"
|
||||
self.col_muted = "#5c626b"
|
||||
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")
|
||||
except tk.TclError:
|
||||
pass
|
||||
bg, fg, field, sel, accent = (self.col_bg, self.col_fg, self.col_field,
|
||||
self.col_sel, self.col_accent)
|
||||
style.configure(".", background=bg, foreground=fg, fieldbackground=field,
|
||||
bordercolor=self.col_line, lightcolor=bg, darkcolor=bg)
|
||||
style.configure("TFrame", background=bg)
|
||||
style.configure("TLabel", background=bg, foreground=fg, font=SCHRIFT)
|
||||
style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
|
||||
style.configure("TLabelframe.Label", background=bg, foreground=accent)
|
||||
|
||||
# --- Kacheln: ein Abschnitt ist eine Flaeche mit Rand, keine Linie ---
|
||||
style.configure("Karte.TFrame", background=self.col_card,
|
||||
bordercolor=self.col_line, relief="solid", borderwidth=1)
|
||||
# Ein Abschnitt ist eine Kachel: eigene Flaeche, feiner Rand, die
|
||||
# Ueberschrift in Akzentfarbe - genau wie die Kacheln in den
|
||||
# Web-Oberflaechen.
|
||||
# Der Kasten, den man gerade in der Hand hat: blass, gestrichelt.
|
||||
style.configure("Gegriffen.TFrame", background=self.col_bg,
|
||||
bordercolor=self.col_muted, relief="solid", borderwidth=1,
|
||||
lightcolor=self.col_muted, darkcolor=self.col_muted)
|
||||
# Der Kasten, auf den beim Anordnen gerade gezielt wird.
|
||||
style.configure("Ziel.TFrame", background=self.col_card,
|
||||
bordercolor=accent, relief="solid", borderwidth=2,
|
||||
lightcolor=accent, darkcolor=accent)
|
||||
style.configure("Karte.TLabelframe", background=self.col_card,
|
||||
bordercolor=self.col_line, relief="solid", borderwidth=1,
|
||||
lightcolor=self.col_card, darkcolor=self.col_card)
|
||||
style.configure("Karte.TLabelframe.Label", background=self.col_card,
|
||||
foreground=accent, font=(SCHRIFT_NAME, 9, "bold"))
|
||||
style.configure("Karte.TEntry", fieldbackground=field, foreground=fg)
|
||||
style.configure("Karte.TLabel", background=self.col_card, foreground=fg, font=SCHRIFT)
|
||||
style.configure("KarteMuted.TLabel", background=self.col_card,
|
||||
foreground=self.col_muted, font=KLEIN)
|
||||
# Ueberschrift eines Abschnitts: klein, gross geschrieben, in Akzentfarbe -
|
||||
# so wie die Kachelkoepfe in den Web-Oberflaechen.
|
||||
style.configure("Abschnitt.TLabel", background=self.col_card,
|
||||
foreground=accent, font=(SCHRIFT_NAME, 9, "bold"))
|
||||
style.configure("Wert.TLabel", background=self.col_card, foreground=fg,
|
||||
font=(SCHRIFT_NAME, 10))
|
||||
style.configure("Summe.TLabel", background=bg, foreground=accent,
|
||||
font=(SCHRIFT_NAME, 15, "bold"))
|
||||
style.configure("Zebra.TLabel", background=self.col_zebra, foreground=fg, font=SCHRIFT)
|
||||
|
||||
# --- Knoepfe im Glas-Stil (wie in seinen Oberflaechen) ---
|
||||
# Getoente Flaeche, feiner Rand in Akzent-Transparenz, Schrift in der
|
||||
# Akzentfarbe. Kein Vollton: der wirkt in dieser Palette wie ein
|
||||
# Fremdkoerper.
|
||||
glas = mischen(accent, bg, 0.10) # ruhige Flaeche
|
||||
glas_hover = mischen(accent, bg, 0.20)
|
||||
rand = mischen(accent, bg, 0.35)
|
||||
rand_hell = mischen(accent, bg, 0.55)
|
||||
style.configure("TButton", background=glas, foreground=fg,
|
||||
bordercolor=rand, padding=(14, 7), font=SCHRIFT,
|
||||
focuscolor=rand, relief="solid", borderwidth=1,
|
||||
lightcolor=rand, darkcolor=rand)
|
||||
style.map("TButton",
|
||||
background=[("pressed", mischen(accent, bg, 0.28)), ("active", glas_hover)],
|
||||
bordercolor=[("active", rand_hell)],
|
||||
lightcolor=[("active", rand_hell)], darkcolor=[("active", rand_hell)],
|
||||
foreground=[("disabled", self.col_muted)])
|
||||
|
||||
# Der Knopf, um den es geht: dieselbe Glasflaeche, aber kraeftiger
|
||||
# getoent, mit Akzentrand und Akzentschrift.
|
||||
haupt = mischen(accent, bg, 0.22)
|
||||
style.configure("Haupt.TButton", background=haupt,
|
||||
foreground=self.col_accent_hell, bordercolor=self.col_accent,
|
||||
lightcolor=self.col_accent, darkcolor=self.col_accent,
|
||||
relief="solid", borderwidth=1,
|
||||
padding=(18, 8), font=(SCHRIFT_NAME, 10, "bold"))
|
||||
style.map("Haupt.TButton",
|
||||
background=[("pressed", mischen(accent, bg, 0.45)),
|
||||
("active", mischen(accent, bg, 0.34)),
|
||||
("disabled", bg)],
|
||||
foreground=[("disabled", self.col_muted)])
|
||||
|
||||
# Storno greift in eine ausgestellte Rechnung ein - gleicher Stil, rot.
|
||||
g_rot = mischen(self.col_gefahr, bg, 0.10)
|
||||
style.configure("Gefahr.TButton", background=g_rot, foreground=self.col_gefahr,
|
||||
bordercolor=mischen(self.col_gefahr, bg, 0.35),
|
||||
lightcolor=mischen(self.col_gefahr, bg, 0.35),
|
||||
darkcolor=mischen(self.col_gefahr, bg, 0.35),
|
||||
relief="solid", borderwidth=1,
|
||||
padding=(14, 7), font=SCHRIFT)
|
||||
style.map("Gefahr.TButton",
|
||||
background=[("pressed", mischen(self.col_gefahr, bg, 0.30)),
|
||||
("active", mischen(self.col_gefahr, bg, 0.20))],
|
||||
bordercolor=[("active", mischen(self.col_gefahr, bg, 0.55))],
|
||||
lightcolor=[("active", mischen(self.col_gefahr, bg, 0.55))],
|
||||
darkcolor=[("active", mischen(self.col_gefahr, bg, 0.55))])
|
||||
style.configure("Menu.TMenubutton", background=bg, foreground=fg, relief="flat",
|
||||
arrowcolor=bg, padding=(10, 3))
|
||||
style.map("Menu.TMenubutton", background=[("active", sel)], foreground=[("active", fg)])
|
||||
style.configure("TCheckbutton", background=bg, foreground=fg)
|
||||
style.map("TCheckbutton", background=[("active", bg)], foreground=[("active", fg)])
|
||||
# Eingabefelder heben sich vom Grund ab - sonst sieht man nicht, wo etwas
|
||||
# einzutragen ist. JEDES Feld, in das man tippen kann, traegt diese zwei
|
||||
# Farben: die ttk-Felder ueber die Stile hier, die klassischen Widgets
|
||||
# (Textfelder, Auswahlliste) ueber feld_optionen() weiter unten.
|
||||
feld_hell, feld_rand = self.col_field, self.col_feldrand
|
||||
style.configure("TEntry", fieldbackground=feld_hell, foreground=fg,
|
||||
insertcolor=fg, bordercolor=feld_rand,
|
||||
lightcolor=feld_rand, darkcolor=feld_rand,
|
||||
relief="solid", borderwidth=1, padding=5)
|
||||
style.map("TEntry",
|
||||
bordercolor=[("focus", accent)], lightcolor=[("focus", accent)],
|
||||
darkcolor=[("focus", accent)],
|
||||
fieldbackground=[("focus", mischen(accent, self.col_card, 0.10))])
|
||||
# Die Felder IN der Leistungsliste stehen auf den Zeilenstreifen und
|
||||
# gingen darin unter - man sah nicht, wo man tippt. Sie bekommen einen
|
||||
# helleren Grund und einen deutlichen Rahmen, beim Tippen den Akzent.
|
||||
style.configure("Liste.TEntry", fieldbackground=feld_hell, foreground=fg,
|
||||
insertcolor=fg, bordercolor=feld_rand,
|
||||
lightcolor=feld_rand, darkcolor=feld_rand,
|
||||
relief="solid", borderwidth=1, padding=4)
|
||||
style.map("Liste.TEntry",
|
||||
bordercolor=[("focus", accent)], lightcolor=[("focus", accent)],
|
||||
darkcolor=[("focus", accent)],
|
||||
fieldbackground=[("focus", mischen(accent, self.col_card, 0.10))])
|
||||
# Bei state="readonly" zeichnet die Combobox ihren Text als MARKIERUNG,
|
||||
# sobald sie den Fokus hat - ohne diese beiden Farben steht der Wert
|
||||
# unsichtbar im Feld.
|
||||
style.configure("TCombobox", fieldbackground=feld_hell, foreground=fg,
|
||||
background=feld_hell, arrowcolor=fg,
|
||||
bordercolor=feld_rand, lightcolor=feld_rand, darkcolor=feld_rand,
|
||||
selectbackground=feld_hell, selectforeground=fg, padding=3)
|
||||
style.map("TCombobox", fieldbackground=[("readonly", feld_hell)],
|
||||
foreground=[("readonly", fg)], background=[("readonly", feld_hell)],
|
||||
bordercolor=[("focus", accent)],
|
||||
selectbackground=[("readonly", feld_hell)],
|
||||
selectforeground=[("readonly", fg)])
|
||||
# Die aufgeklappte Liste ist ein klassisches tk-Listbox-Widget
|
||||
self.option_add("*TCombobox*Listbox.background", feld_hell)
|
||||
self.option_add("*TCombobox*Listbox.foreground", fg)
|
||||
self.option_add("*TCombobox*Listbox.selectBackground", sel)
|
||||
self.option_add("*TCombobox*Listbox.selectForeground", fg)
|
||||
for sbs in ("Vertical.TScrollbar", "Horizontal.TScrollbar", "TScrollbar"):
|
||||
style.configure(sbs, troughcolor=bg, background=self.col_scroll,
|
||||
bordercolor=bg, lightcolor=bg, darkcolor=bg,
|
||||
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
|
||||
# ist. Der offene Reiter bekommt darum den Hintergrund des Blattes und die
|
||||
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
|
||||
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
|
||||
tabmargins=(2, 4, 2, 0), borderwidth=0)
|
||||
# Der OFFENE Reiter gehoert zur Kachel darunter und ist deshalb heller
|
||||
# als die ruhenden. Vorher war es andersherum: die ruhenden lagen auf der
|
||||
# helleren Flaeche und sahen aus, als waeren sie ausgewaehlt.
|
||||
# ⚠️ clam zeichnet die Reiterflaeche aus lightcolor/darkcolor, nicht aus
|
||||
# background allein - werden die nicht mitgesetzt, bleibt der ruhende
|
||||
# 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,
|
||||
# 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))],
|
||||
lightcolor=[("selected", self.col_card),
|
||||
("active", mischen(accent, bg, 0.12))],
|
||||
darkcolor=[("selected", self.col_card),
|
||||
("active", mischen(accent, bg, 0.12))],
|
||||
bordercolor=[("selected", accent)],
|
||||
foreground=[("selected", accent), ("active", fg)],
|
||||
font=[("selected", (SCHRIFT_NAME, 10, "bold"))])
|
||||
# Kein expand beim ausgewaehlten Reiter: er wuchs dadurch um ein paar
|
||||
# Pixel, die anderen rueckten nach - beim Umschalten sprang die ganze
|
||||
# Leiste. Erkannt wird der offene Reiter an Farbe und Fettschrift.
|
||||
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
|
||||
self.option_add("*background", bg)
|
||||
self.option_add("*foreground", fg)
|
||||
|
||||
def feld_optionen(self, mehrzeilig=False, einfuegemarke=True):
|
||||
"""Farben und Rahmen fuer die klassischen Eingabe-Widgets (Text, Listbox).
|
||||
|
||||
ttk-Felder holen sich das aus dem Stil; tk.Text und tk.Listbox koennen
|
||||
das nicht - sie muessen die Werte einzeln bekommen. Damit trotzdem JEDES
|
||||
Feld gleich aussieht, kommen sie aus dieser einen Stelle.
|
||||
"""
|
||||
opts = {
|
||||
"bg": self.col_field,
|
||||
"fg": self.col_fg,
|
||||
"relief": "solid",
|
||||
"borderwidth": 1,
|
||||
"highlightthickness": 0,
|
||||
}
|
||||
# Eine Auswahlliste hat keine Einfuegemarke - sie kennt die Option nicht
|
||||
# und bricht daran ab.
|
||||
if einfuegemarke:
|
||||
opts["insertbackground"] = self.col_fg
|
||||
if mehrzeilig:
|
||||
opts["padx"] = 4
|
||||
opts["pady"] = 3
|
||||
return opts
|
||||
|
||||
def _style_dialog(self, d):
|
||||
"""Toplevel-Dialog an das Theme angleichen.
|
||||
|
||||
Im Pruefstand wird derselbe Dialog gebaut und bedient, aber nicht
|
||||
gezeigt - und sein grab_set faellt weg. Sonst haelt jeder Dialog Maus
|
||||
und Tastatur fest, waehrend nebenan jemand schreibt.
|
||||
"""
|
||||
d.configure(bg=self.col_bg)
|
||||
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.
|
||||
d.geometry(f"+{d.winfo_screenwidth() + 20}+40")
|
||||
d.grab_set = lambda *a, **k: None
|
||||
try:
|
||||
d.attributes("-toolwindow", True) # nicht in die Taskleiste
|
||||
except tk.TclError:
|
||||
pass
|
||||
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
|
||||
d.after(20, lambda: self._dark_titlebar(d))
|
||||
|
||||
def _dark_titlebar(self, win):
|
||||
"""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:
|
||||
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.
|
||||
|
||||
Warum nicht `messagebox`: das sind Fenster von Windows selbst. Sie sind
|
||||
immer hell, egal was das Programm einstellt - mitten in einer dunklen
|
||||
Oberflaeche blendet das und sieht nach Fremdkoerper aus. Dieselbe Erfahrung
|
||||
steckt schon in der eigenen Menueleiste weiter oben.
|
||||
|
||||
⚠️ Im PRUEFLAUF geht beides weiter ueber `messagebox`. Die Pruefstaende
|
||||
haengen ihre Antworten dort ein ("auf diese Frage antworte Ja") und lesen
|
||||
dort mit, was das Programm gemeldet hat. Wuerde hier ein eigenes Fenster
|
||||
aufgehen, warteten sie auf einen Klick, den niemand macht. Geprueft wird
|
||||
damit derselbe Weg - nur die Anzeige unterscheidet sich.
|
||||
"""
|
||||
|
||||
# Die drei Sorten Meldung. `parent=`, `icon=` und `default=` kommen aus den
|
||||
# frueheren messagebox-Aufrufen mit - sie werden geschluckt, damit an den
|
||||
# Aufrufstellen nichts umgeschrieben werden muss.
|
||||
def melden_info(self, titel, text="", **_kw):
|
||||
return self.melden(titel, text, "info")
|
||||
|
||||
def melden_warnung(self, titel, text="", **_kw):
|
||||
return self.melden(titel, text, "warnung")
|
||||
|
||||
def melden_fehler(self, titel, text="", **_kw):
|
||||
return self.melden(titel, text, "fehler")
|
||||
|
||||
def melden(self, titel, text, art="warnung"):
|
||||
"""Eine Meldung mit einem OK-Knopf."""
|
||||
if testlauf():
|
||||
{"info": messagebox.showinfo,
|
||||
"warnung": messagebox.showwarning,
|
||||
"fehler": messagebox.showerror}.get(art, messagebox.showwarning)(titel, text)
|
||||
return None
|
||||
return self._meldungsfenster(titel, text, art, knoepfe=None)
|
||||
|
||||
def frage(self, titel, text="", ja="Ja", nein="Nein", vorgabe=True, **_kw):
|
||||
"""Ja/Nein. Gibt True oder False zurueck - wie askyesno."""
|
||||
if testlauf():
|
||||
return bool(messagebox.askyesno(titel, text))
|
||||
return bool(self._meldungsfenster(titel, text, "frage",
|
||||
knoepfe=(ja, nein), vorgabe=vorgabe))
|
||||
|
||||
def _meldungsfenster(self, titel, text, art, knoepfe=None, vorgabe=True):
|
||||
farbe = {"fehler": self.col_gefahr, "warnung": self.col_gefahr,
|
||||
"frage": self.col_accent}.get(art, self.col_accent)
|
||||
d = tk.Toplevel(self)
|
||||
d.title(titel)
|
||||
d.transient(self)
|
||||
d.resizable(False, False)
|
||||
self._style_dialog(d)
|
||||
antwort = {"wert": False}
|
||||
|
||||
rahmen = ttk.Frame(d, padding=20)
|
||||
rahmen.pack(fill="both", expand=True)
|
||||
tk.Label(rahmen, text=titel, bg=self.col_bg, fg=farbe, justify="left",
|
||||
font=(SCHRIFT_NAME, 12, "bold")).pack(anchor="w")
|
||||
tk.Label(rahmen, text=text, bg=self.col_bg, fg=self.col_fg, justify="left",
|
||||
wraplength=460, font=(SCHRIFT_NAME, 10)).pack(anchor="w", pady=(10, 0))
|
||||
|
||||
leiste = ttk.Frame(rahmen)
|
||||
leiste.pack(fill="x", pady=(20, 0))
|
||||
|
||||
def schliessen(wert):
|
||||
antwort["wert"] = wert
|
||||
d.destroy()
|
||||
|
||||
if knoepfe:
|
||||
ja, nein = knoepfe
|
||||
ttk.Button(leiste, text=ja, style="Haupt.TButton",
|
||||
command=lambda: schliessen(True)).pack(side="right")
|
||||
ttk.Button(leiste, text=nein,
|
||||
command=lambda: schliessen(False)).pack(side="right", padx=(0, 8))
|
||||
d.bind("<Return>", lambda _e: schliessen(vorgabe))
|
||||
else:
|
||||
ttk.Button(leiste, text="OK", style="Haupt.TButton",
|
||||
command=lambda: schliessen(True)).pack(side="right")
|
||||
d.bind("<Return>", lambda _e: schliessen(True))
|
||||
d.bind("<Escape>", lambda _e: schliessen(False))
|
||||
d.protocol("WM_DELETE_WINDOW", lambda: schliessen(False))
|
||||
|
||||
d.update_idletasks()
|
||||
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
|
||||
y = self.winfo_rooty() + 140
|
||||
d.geometry(f"+{max(0, x)}+{max(0, y)}")
|
||||
# Nach vorn holen und den Tastaturfokus nehmen. Ohne das kann der Dialog
|
||||
# beim Start hinter dem Hauptfenster liegen - der Nutzer sieht nur ein
|
||||
# Fenster, das nicht reagiert, und haelt es fuer haengen geblieben.
|
||||
d.lift()
|
||||
d.attributes("-topmost", True)
|
||||
d.after(100, lambda: d.attributes("-topmost", False))
|
||||
d.focus_force()
|
||||
d.grab_set()
|
||||
d.focus_set()
|
||||
self.wait_window(d)
|
||||
return antwort["wert"]
|
||||
Loading…
Add table
Add a link
Reference in a new issue