Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer sich pruefbar - nach jeder liefen beide Pruefketten gruen. * Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks. Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung des Titels feuert gar nicht. Das native tk.Menu, das diese Leiste ersetzt hat, wechselte schon beim blossen Ueberfahren - zwei Klicks waren also eine Verschlechterung gegenueber dem, was vorher da war. Die Klappliste wird ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm() begrenzt; am unteren Fensterrand lief sie vorher hinaus. Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse. * %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG). * pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py. Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31) und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen. * pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt == 1, und der Dateiname wird mitgeprueft. Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF. * .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen. Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen Unterschied, den es im Repository gar nicht gibt. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung waere Rauschen. * Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=), kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes". 39 Pruefstaende, 0 rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
819 lines
39 KiB
Python
819 lines
39 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 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:
|
|
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 pruefplatz():
|
|
"""Wo ein Pruefstand seine Fenster aufmachen soll: "+x+y" oder "".
|
|
|
|
Seine Ansage am 07.09.2026: *"teste links auf dem 1080p monitor nicht mehr
|
|
auf dem main ... weil da schau ich nun video"*.
|
|
|
|
Vorher lag der Platz bei `Bildschirmbreite + 20` - also RECHTS neben dem
|
|
Hauptmonitor. Bei ihm steht der zweite Monitor aber LINKS (x = -1920), und
|
|
rechts ist gar nichts: die Fenster gingen ins Leere. Sie liefen zwar, waren
|
|
aber nirgends zu sehen - das hat beim Bildermachen einige Zeit gekostet.
|
|
|
|
Gesucht wird deshalb der nicht-primaere Monitor. Gibt es keinen, bleibt es
|
|
beim alten Verhalten (neben den Bildschirm), denn im Weg sein soll ein
|
|
Pruefstand auch dann nicht.
|
|
"""
|
|
if not sys.platform.startswith("win"):
|
|
return ""
|
|
try:
|
|
class RECT(ctypes.Structure):
|
|
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
|
|
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
|
|
|
|
class MONITORINFO(ctypes.Structure):
|
|
_fields_ = [("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", RECT),
|
|
("rcWork", RECT), ("dwFlags", ctypes.wintypes.DWORD)]
|
|
|
|
gefunden = []
|
|
|
|
def je_monitor(hmon, hdc, lprect, daten):
|
|
info = MONITORINFO()
|
|
info.cbSize = ctypes.sizeof(MONITORINFO)
|
|
if ctypes.windll.user32.GetMonitorInfoW(hmon, ctypes.byref(info)):
|
|
r = info.rcWork
|
|
gefunden.append((bool(info.dwFlags & 1), # 1 = MONITORINFOF_PRIMARY
|
|
r.left, r.top, r.right - r.left))
|
|
return 1
|
|
|
|
RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HMONITOR,
|
|
ctypes.wintypes.HDC, ctypes.POINTER(RECT),
|
|
ctypes.wintypes.LPARAM)
|
|
ctypes.windll.user32.EnumDisplayMonitors(0, 0, RUECKRUF(je_monitor), 0)
|
|
# Der breiteste Nebenmonitor - bei mehreren der linkeste.
|
|
neben = sorted((m for m in gefunden if not m[0]),
|
|
key=lambda m: (-m[3], m[1]))
|
|
if neben:
|
|
_, x, y, _ = neben[0]
|
|
return f"+{x + 30}+{y + 40}"
|
|
except Exception: # noqa: BLE001 - dann eben der alte Weg
|
|
pass
|
|
return ""
|
|
|
|
|
|
def auf_bildschirm(x, y, breite=0, hoehe=0):
|
|
"""Eine Fensterposition auf den sichtbaren Bereich begrenzen.
|
|
|
|
Frueher stand an diesen Stellen `max(0, x)`. Das sollte verhindern, dass
|
|
ein Dialog ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE
|
|
Koordinate auf null. Steht der zweite Monitor links (bei ihm x = -1920),
|
|
sprang damit jedes Popup zurueck auf den Hauptbildschirm, obwohl das
|
|
Programm nebenan lief. Sein Befund am 07.09.2026: *"da gehen noch die
|
|
popups auf dem video monitor auf"*.
|
|
|
|
Begrenzt wird deshalb auf den GESAMTEN Desktop ueber alle Monitore
|
|
(SM_*VIRTUALSCREEN), nicht auf den Hauptmonitor.
|
|
"""
|
|
if not sys.platform.startswith("win"):
|
|
return int(x), int(y)
|
|
try:
|
|
hole = ctypes.windll.user32.GetSystemMetrics
|
|
links, oben = hole(76), hole(77)
|
|
rechts, unten = links + hole(78), oben + hole(79)
|
|
if breite:
|
|
x = min(x, rechts - breite)
|
|
if hoehe:
|
|
y = min(y, unten - hoehe)
|
|
return int(max(links, x)), int(max(oben, y))
|
|
except Exception: # noqa: BLE001 - dann eben ungebremst
|
|
return int(x), int(y)
|
|
|
|
|
|
def mittig(dialog, ueber):
|
|
"""Einen Dialog mittig ueber sein Elternfenster setzen.
|
|
|
|
Seine Ansage am 07.09.2026: *"einfach so programmieren das die popup auf
|
|
dem fenster mittig vom main fenster auf gehen"*.
|
|
|
|
Das ist zugleich die Antwort auf die Monitorfrage: steht das Programm auf
|
|
dem zweiten Bildschirm, geht der Dialog dort auf - ohne dass irgendwo ein
|
|
Monitor gesucht werden muesste. Vorher standen an den Dialogen feste
|
|
Versaetze ("+120+140") und ein `max(0, x)`, das jede negative Koordinate
|
|
auf null klemmte und die Fenster damit auf den Hauptmonitor zurueckriss.
|
|
|
|
Etwas oberhalb der Mitte (ein Drittel statt der Haelfte): so sitzt der
|
|
Dialog dort, wo der Blick ohnehin ist, und verdeckt nicht die Mitte des
|
|
Formulars darunter.
|
|
"""
|
|
try:
|
|
dialog.update_idletasks()
|
|
breite = dialog.winfo_reqwidth() or dialog.winfo_width()
|
|
hoehe = dialog.winfo_reqheight() or dialog.winfo_height()
|
|
x = ueber.winfo_rootx() + (ueber.winfo_width() - breite) // 2
|
|
y = ueber.winfo_rooty() + (ueber.winfo_height() - hoehe) // 3
|
|
x, y = auf_bildschirm(x, y, breite, hoehe)
|
|
dialog.geometry(f"+{x}+{y}")
|
|
except tk.TclError:
|
|
pass # Fenster schon zu - dann ist die Position gleichgueltig
|
|
|
|
|
|
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
|
|
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)
|
|
# Mittig ueber dem Programmfenster - und damit automatisch auf dem
|
|
# Bildschirm, auf dem das Programm gerade steht.
|
|
d.after(1, lambda: mittig(d, self))
|
|
if testlauf():
|
|
# Ohne grab_set: ein Dialog, der Maus und Tastatur festhaelt,
|
|
# blockiert den Menschen, der nebenher arbeitet.
|
|
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)))
|
|
|
|
# ------------------------------------------------ Hell / dunkel umstellen
|
|
# Der Hinweistext gehoert dem einzelnen Programm - er sagt etwas ueber
|
|
# DIESES Programm. Hier steht nur, was dasteht, wenn ihn niemand setzt.
|
|
DARSTELLUNG_HINWEIS = "Gilt nur für das Programmfenster."
|
|
|
|
def _cfg_speichern(self):
|
|
"""Die Einstellungen wegschreiben - falls das noetig ist.
|
|
|
|
Im Rechnungstool ist die Konfiguration ein gewoehnliches Woerterbuch
|
|
und muss ausdruecklich gespeichert werden. Im Steuerrechnungstool ist
|
|
sie eine Ablage, die jeden Schreibzugriff sofort in die
|
|
Journaltabelle durchreicht - dort ist hier nichts zu tun.
|
|
"""
|
|
return
|
|
|
|
def variablen_sichern(self):
|
|
"""Alle Tk-Variablen des Fensters als {Name: Wert}.
|
|
|
|
WARUM ALLE UND NICHT EINE AUSWAHL
|
|
---------------------------------
|
|
Die _build-Methoden legen ihre Variablen selbst an
|
|
(`self.k_name = tk.StringVar()`). Nach dem Neubau stehen also neue,
|
|
leere da. Beim ersten Versuch waren Kundenname und Rechnungsnummer
|
|
weg, waehrend die Mengen blieben - die hatte ich einzeln gesichert.
|
|
|
|
Durchgegangen wird deshalb, was das Objekt an Tk-Variablen traegt.
|
|
Wer spaeter ein Feld hinzufuegt, muss nichts eintragen; 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
|
|
return gesichert
|
|
|
|
def variablen_zuruecksetzen(self, gesichert):
|
|
"""Die gesicherten Werte in die neu gebauten Variablen zurueckgeben."""
|
|
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
|
|
|
|
def kinder_loeschen(self):
|
|
"""Die Oberflaeche abraeumen - aber keine fremden Fenster.
|
|
|
|
Ein tk.Toplevel IST ein Kind des Hauptfensters. Der offene
|
|
Darstellungsdialog wurde hier bisher mitgeloescht, und die Zeile
|
|
danach fasste eine Leiche an ("invalid command name .!toplevel").
|
|
Gefunden am 07.09.2026 von der Pruefkette, mit dem echten Bedienweg
|
|
gemessen: Klick auf "Dunkel" -> Farbe wechselt, Fenster ist weg,
|
|
Traceback im Log.
|
|
|
|
Der Pruefstand hatte es nicht gefunden, weil er cfg["darstellung"] von
|
|
Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser dem
|
|
Weg, den ein Mensch geht. Er klickt jetzt.
|
|
"""
|
|
for kind in self.winfo_children():
|
|
if not isinstance(kind, tk.Toplevel):
|
|
kind.destroy()
|
|
|
|
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=(SCHRIFT_NAME, 11, "bold"),
|
|
foreground=self.col_accent).pack(anchor="w")
|
|
ttk.Label(rahmen, justify="left", wraplength=340,
|
|
foreground=self.col_muted,
|
|
text=self.DARSTELLUNG_HINWEIS).pack(anchor="w", pady=(6, 12))
|
|
|
|
gewaehlt = tk.StringVar(value=modus(self.cfg))
|
|
|
|
def uebernehmen():
|
|
neu = gewaehlt.get()
|
|
if neu == modus(self.cfg):
|
|
return
|
|
self.cfg["darstellung"] = neu
|
|
self._cfg_speichern()
|
|
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 MODI:
|
|
ttk.Radiobutton(rahmen, text=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")
|
|
mittig(d, self)
|
|
return d
|
|
|
|
|
|
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))
|
|
|
|
mittig(d, self)
|
|
# 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"]
|