Oberflaeche im Pcore-Stil: Tuerkis, Kacheln, Glas-Knoepfe, Roboto
Seine Ansage: "schau dir unsern fivem server an und unsere webseiten, dann
weisst du wie mein design ist" - also die Palette aus _ui_www und der
Adminpage: Tuerkis #06C6A4 auf gestuften dunklen Flaechen (#0d0e10 Grund,
#15171b Kachel), Raender #25282e, Text #e8e8ea / #8a8e96, Rot #e85a5a.
Das PROGRAMM traegt diese Handschrift, die RECHNUNG die Farbe der jeweiligen
Firma - das Blatt gehoert dem Kunden, nicht uns.
- Roboto wird mitgeliefert und nur fuer den Prozess geladen
(AddFontResourceEx mit FR_PRIVATE): keine Installation, keine Adminrechte,
nach dem Schliessen ist der Rechner wie vorher. Fehlt sie, bleibt es bei
Segoe UI. Die Schriftdateien gehen mit in die exe (spec).
- Abschnitte sind Kacheln mit Ueberschrift statt grauer Rahmen.
- Knoepfe im Glas-Stil wie in seinen Oberflaechen: getoente Flaeche, Rand in
Akzent-Transparenz. Tk kann kein backdrop-filter - die Toenung wird
ausgerechnet (theme.mischen), das Ergebnis ist auf einfarbigem Grund
identisch. "PDF erstellen" fuehrt, "Storno" ist rot.
- Zebrastreifen in der Leistungsliste: bei 17 Zeilen verrutscht man sonst.
- Kein expand am offenen Reiter mehr - dadurch sprang beim Umschalten die
ganze Leiste ("das taben macht den aktiven kleiner").
- Das Fenster fuer den gemeinsamen Ordner stand als einziges noch in Weiss da:
es kam nie durch _style_dialog.
Dazu ein Fund, der nichts mit dem Design zu tun hat: die Pruefstaende erbten
"nummer_eingerichtet" aus der config DIESES Rechners. Sobald hier jemand die
Nummer einmal einrichtet, liefen sie in einen anderen Zweig und wurden rot -
ohne dass sich am Programm etwas geaendert haette. Sie legen den Zustand jetzt
selbst fest. Alle 14 gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
parent
0ad03fd1a4
commit
5ebb02b267
17 changed files with 330 additions and 87 deletions
216
theme.py
216
theme.py
|
|
@ -7,11 +7,85 @@ 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 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"):
|
||||
|
|
@ -32,26 +106,42 @@ class ThemeMixin:
|
|||
|
||||
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 = "#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")
|
||||
self.col_bg = "#0d0e10" # Fenster-Hintergrund (tiefstes Grau)
|
||||
self.col_card = "#15171b" # Kachel / Abschnitt
|
||||
self.col_fg = "#e8e8ea" # Schrift
|
||||
self.col_field = "#1d2025" # 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 = "#1b1e24" # jede zweite Zeile der Liste
|
||||
else:
|
||||
self.col_bg = "#f3f3f3"
|
||||
self.col_fg = "#1a1a1a"
|
||||
self.col_bg = "#eef0f3"
|
||||
self.col_card = "#ffffff"
|
||||
self.col_fg = "#14161a"
|
||||
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.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 = "#e9ecf0"
|
||||
|
||||
self.configure(bg=self.col_bg)
|
||||
style = ttk.Style(self)
|
||||
|
|
@ -64,17 +154,89 @@ class ThemeMixin:
|
|||
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("TLabel", background=bg, foreground=fg, font=SCHRIFT)
|
||||
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)])
|
||||
|
||||
# --- 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.
|
||||
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)])
|
||||
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg)
|
||||
style.configure("TEntry", fieldbackground=field, foreground=fg, insertcolor=fg,
|
||||
bordercolor=self.col_line, padding=4)
|
||||
style.map("TEntry", bordercolor=[("focus", accent)])
|
||||
# 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.
|
||||
|
|
@ -101,15 +263,17 @@ class ThemeMixin:
|
|||
# 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))
|
||||
style.configure("TNotebook.Tab", background=field, foreground=self.col_muted,
|
||||
bordercolor=self.col_line, padding=(16, 7),
|
||||
font=("Segoe UI", 10))
|
||||
tabmargins=(2, 4, 2, 0), borderwidth=0)
|
||||
style.configure("TNotebook.Tab", background=self.col_field,
|
||||
foreground=self.col_muted, bordercolor=self.col_line,
|
||||
padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=0)
|
||||
style.map("TNotebook.Tab",
|
||||
background=[("selected", bg), ("active", sel)],
|
||||
background=[("selected", self.col_card), ("active", sel)],
|
||||
foreground=[("selected", accent), ("active", fg)],
|
||||
font=[("selected", ("Segoe UI", 10, "bold"))],
|
||||
expand=[("selected", (1, 1, 1, 0))])
|
||||
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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue