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
479 lines
24 KiB
Python
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"]
|