rechnungstool/theme.py
TheMockTv c1e328c087 Kaesten verschiebbar wie bei OPNsense: Knopf, schieben, speichern
Seine Ansage: "rechnungsdaten oben links, darunter rechnung an kunde, den
tabellen kasten rechts - oder noch besser dnd einbauen" und dann genauer: "wie
bei opnsense hast so einen kleinen knopf, dann drueckst drauf, ab da kannst du
schieben, dann hast du einen save button, und dann ist die fest".

- Seine Anordnung ist die Vorgabe (flaeche.STANDARD).
- Verschieben muss man EINSCHALTEN. Ohne den Knopf sitzt alles fest, damit beim
  taeglichen Arbeiten nichts verrutscht. Im Modus stehen "Speichern" und
  "Verwerfen" daneben; erst Speichern schreibt in die config.
- Waehrend des Ziehens haengt ein Etikett am Zeiger und der gegriffene Kasten
  wird blass - vorher sah man nur das Ziel und es wirkte "statisch" (sein Fund).
- Getauscht statt frei abgelegt: freies Verschieben auf Pixel braucht Regeln
  fuer Ueberlappung, Groesse und leere Zellen, und am Ende steht das Fenster
  schief. Ein Tausch ist in einem Griff erledigt.
- Der freie Platz folgt dem Kasten mit der langen Liste. Feste Gewichte gingen
  nicht: die Liste wandert, und der Kundenkasten wurde unten abgeschnitten.
- Eine unbrauchbare Anordnung in der config faellt auf die Vorgabe zurueck.

Icons: die Material Symbols vom Hub (M:/projekte/material-symbols), einmal nach
PNG gewandelt und zur Laufzeit eingefaerbt (icons.py) - eine Datei je Icon,
nicht drei Fassungen fuer hell/dunkel/Akzent.

⚠️ Ein PhotoImage gehoert GENAU EINEM Tk-Fenster. Der Icon-Cache reichte Bilder
an das naechste weiter, sobald ein Pruefstand Journal und Rechnungstool
nacheinander faehrt: 'image "pyimage1" doesn't exist'. Das Fenster wird jetzt
mitgegeben, tk._default_root reicht dafuer nicht.

Neu: pruef_flaeche.py. Alle 15 Pruefstaende gruen.

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

479 lines
24 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 font as tkfont
from tkinter import messagebox
from tkinter import ttk
import config
# 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
import os
FR_PRIVATE = 0x10
geladen = 0
for basis in (config.BASE_DIR, config.RESOURCE_DIR, os.path.dirname(os.path.abspath(__file__))):
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
class ThemeMixin:
"""Farben setzen. Erwartet ein tk.Tk als Basis."""
# ----------------------------------------------------------------- Theme
def _apply_theme(self):
"""Farben passend zum Windows-Theme (hell/dunkel) setzen."""
schrift_vorbereiten() # Roboto, sonst Segoe UI
dark = windows_dark_mode()
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)
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)])
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,
padding=(20, 9), 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 config.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):
"""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)
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 config.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 config.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)}")
d.grab_set()
d.focus_set()
self.wait_window(d)
return antwort["wert"]