rechnungstool/theme.py
TheMockTv 2277a12e0f Zwei getrennte Knoepfe und app.py in Module aufgeteilt
Knoepfe: "Storno" und "Berichtigen" setzen den Weg schon beim Oeffnen fest,
das Popup fragt dann nur noch die Nummer. Vorher entschied ein zweiter Knopf
im Popup - der ist raus.

app.py war 1802 Zeilen, jetzt 672. Neu daneben:
  helfer.py        Zahlen, Datum, Namen, Pruefungen        (87)
  bestand.py       was im Rechnungsordner liegt, nur lesen (171)
  storno.py        den spiegelbildlichen Storno rechnen     (57)
  einzelinstanz.py nur ein Fenster zulassen                 (90)
  theme.py         Farben nach Windows hell/dunkel         (120)
  korrektur.py     Storno- und Berichtigungsweg            (306)
  einstellungen.py die Dialoge aus dem Menue               (358)

Die Oberflaeche bleibt eine Klasse - die Teile sind Mixins, damit kein
einziger self-Zugriff umgeschrieben werden musste. Kein Modul importiert
app.py zurueck, keine ungenutzten Importe mehr. Die Pruefstaende holen die
Funktionen jetzt aus dem Modul, dem sie gehoeren.

pruef_storno.py 100 gruen, pruef_nummern.py 7 gruen, Fenster nach der
Aufteilung gestartet und angesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
2026-09-02 19:41:00 +02:00

120 lines
6 KiB
Python

# -*- 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 sys
import tkinter as tk
from tkinter import ttk
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
class ThemeMixin:
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
# ----------------------------------------------------------------- Theme
def _apply_theme(self):
"""Farben passend zum Windows-Theme (hell/dunkel) setzen."""
dark = windows_dark_mode()
self._dark_mode = dark
if dark:
self.col_bg = "#23272e" # Fenster-Hintergrund
self.col_fg = "#e6e6e6" # Schrift
self.col_field = "#2f343c" # Eingabefelder
self.col_sel = "#3a4a44" # aktiv/hover
self.col_accent = "#5fb98f" # Akzent (Gruen, hell)
self.col_line = "#3a3f47"
self.col_scroll = "#4a505a" # Scroll-Griff
self.col_muted = "#8a929c" # gedaempfte Schrift (z.B. "powered by")
else:
self.col_bg = "#f3f3f3"
self.col_fg = "#1a1a1a"
self.col_field = "#ffffff"
self.col_sel = "#cfe3d8"
self.col_accent = "#1f5c3d"
self.col_line = "#cccccc"
self.col_scroll = "#b8b8b8"
self.col_muted = "#777777" # gedaempfte Schrift (z.B. "powered by")
self.configure(bg=self.col_bg)
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)
style.configure("TLabelframe", background=bg, bordercolor=self.col_line)
style.configure("TLabelframe.Label", background=bg, foreground=accent)
style.configure("TButton", background=field, foreground=fg, bordercolor=self.col_line)
style.map("TButton", background=[("active", sel)], foreground=[("active", fg)])
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)])
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg)
# 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=field, foreground=fg,
background=field, arrowcolor=fg,
selectbackground=field, selectforeground=fg)
style.map("TCombobox", fieldbackground=[("readonly", field)],
foreground=[("readonly", fg)], background=[("readonly", field)],
selectbackground=[("readonly", field)], selectforeground=[("readonly", fg)])
# Die aufgeklappte Liste ist ein klassisches tk-Listbox-Widget
self.option_add("*TCombobox*Listbox.background", field)
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)])
style.configure("TSeparator", background=self.col_line)
# Klassische tk-Widgets (Canvas, Toplevel, Text) erben diese Defaults
self.option_add("*background", bg)
self.option_add("*foreground", fg)
def _style_dialog(self, d):
"""Toplevel-Dialog an das Theme angleichen."""
d.configure(bg=self.col_bg)
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
d.after(20, lambda: self._dark_titlebar(d))
def _dark_titlebar(self, win):
"""Windows-Titelleiste des Fensters dunkel machen (nur im Dark-Mode)."""
if not (sys.platform.startswith("win") and getattr(self, "_dark_mode", False)):
return
try:
import ctypes
win.update_idletasks()
hwnd = ctypes.windll.user32.GetParent(win.winfo_id())
val = ctypes.c_int(1)
for attr in (20, 19): # DWMWA_USE_IMMERSIVE_DARK_MODE (Win11=20, frühes Win10=19)
ctypes.windll.dwmapi.DwmSetWindowAttribute(
hwnd, attr, ctypes.byref(val), ctypes.sizeof(val))
# Frame neu zeichnen lassen (ohne Flackern/Fokusverlust)
SWP = 0x0001 | 0x0002 | 0x0004 | 0x0020 # NOSIZE|NOMOVE|NOZORDER|FRAMECHANGED
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
except Exception as e: # noqa: BLE001
print("[theme] Titelleiste:", e)