1005 lines
38 KiB
Python
1005 lines
38 KiB
Python
"""Optik für Tkinter: Farben, Schriften, Pixel-Icons und ein paar Bausteine.
|
||
|
||
Zwei Designs: „modern“ (hell und flach, wie Windows 11 – Standard) und „win95“ (klassisch, Legacy).
|
||
design_setzen() muss VOR dem Aufbau der Oberfläche laufen; alle Module lesen die Farben als w.FACE usw.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import tkinter as tk
|
||
from pathlib import Path
|
||
from tkinter import ttk
|
||
|
||
FACE = "#c0c0c0"
|
||
HILITE = "#ffffff"
|
||
LIGHT = "#dfdfdf"
|
||
SHADOW = "#808080"
|
||
DARK = "#000000"
|
||
NAVY = "#000080"
|
||
TEAL = "#008080"
|
||
WHITE = "#ffffff"
|
||
INFO_BG = "#ffffe1"
|
||
SEL_BG, SEL_FG = NAVY, WHITE # Auswahl/Markierung
|
||
RAND = SHADOW # dünne Rahmenlinien (nur modern)
|
||
DESIGN = "win95"
|
||
MODERN = False # „modern“ oder „dunkel“
|
||
DUNKEL = False
|
||
APP_NAME = "Provisions-Manager 95" # modern: ohne „95“
|
||
|
||
# Farbnamen: FACE = Fläche (Fenster, Karten, Dialoge), BG = Grund hinter den Karten, WHITE = Eingabefelder/Listen,
|
||
# DARK = Text, TEXT2 = Nebentext, SHADOW = leiser Text/Win95-Schatten, NAVY = Akzent, RAND/RAND2 = Linien,
|
||
# PRIMAER/AUF_PRIMAER = Hauptknopf, MANUELL = von Hand geänderte Beträge, *_SOFT = Grund der Status-Pills.
|
||
_PALETTEN = {
|
||
"win95": dict(FACE="#c0c0c0", HILITE="#ffffff", LIGHT="#dfdfdf", SHADOW="#808080", DARK="#000000",
|
||
NAVY="#000080", TEAL="#008080", WHITE="#ffffff", INFO_BG="#ffffe1", SEL_BG="#000080",
|
||
SEL_FG="#ffffff", RAND="#808080", BG="#008080", RAND2="#808080", TEXT2="#000000",
|
||
PRIMAER="#c0c0c0", AUF_PRIMAER="#000000", ZEBRA="#ffffff", MANUELL="#0000c0", ROT="#a00000",
|
||
ROT_SOFT="#c0c0c0", GRUEN="#008000", GRUEN_SOFT="#c0c0c0", BLAU="#0000ff", BLAU_SOFT="#c0c0c0",
|
||
GOLD="#808000", GOLD_SOFT="#c0c0c0"),
|
||
# Hausfarben der Handy-Insel-Webtools (VVL-Tool): Lila, IBM Plex
|
||
"modern": dict(FACE="#ffffff", HILITE="#ffffff", LIGHT="#f1edf8", SHADOW="#6b6386", DARK="#211a38",
|
||
NAVY="#5e35b1", TEAL="#f1eef8", WHITE="#ffffff", INFO_BG="#ffffff", SEL_BG="#ede7fa",
|
||
SEL_FG="#2d1b69", RAND="#e5e0f0", BG="#f1eef8", RAND2="#d6cee8", TEXT2="#5b5372",
|
||
PRIMAER="#2d1b69", AUF_PRIMAER="#ffffff", ZEBRA="#faf8fd", MANUELL="#5e35b1", ROT="#b8223f",
|
||
ROT_SOFT="#fbe4e9", GRUEN="#11734d", GRUEN_SOFT="#def3ea", BLAU="#2a59c4", BLAU_SOFT="#e4ebfb",
|
||
GOLD="#8a6212", GOLD_SOFT="#f7eed8"),
|
||
"dunkel": dict(FACE="#1c1436", HILITE="#1c1436", LIGHT="#2a2049", SHADOW="#948daa", DARK="#ece8f6",
|
||
NAVY="#a88beb", TEAL="#120c22", WHITE="#221940", INFO_BG="#2a2049", SEL_BG="#3a2d66",
|
||
SEL_FG="#ece8f6", RAND="#312551", BG="#120c22", RAND2="#3f3167", TEXT2="#ada5c6",
|
||
PRIMAER="#c3adee", AUF_PRIMAER="#1c1436", ZEBRA="#201838", MANUELL="#c3adee", ROT="#f26982",
|
||
ROT_SOFT="#3a1e28", GRUEN="#4fc796", GRUEN_SOFT="#163025", BLAU="#7da0f0", BLAU_SOFT="#1e2743",
|
||
GOLD="#d8b058", GOLD_SOFT="#332a17"),
|
||
}
|
||
globals().update(_PALETTEN["win95"])
|
||
|
||
IS_WIN = sys.platform.startswith("win")
|
||
FONT_FAMILY = "MS Sans Serif" if IS_WIN else "DejaVu Sans"
|
||
FONT_SIZE = 8 if IS_WIN else 9
|
||
FONT = (FONT_FAMILY, FONT_SIZE)
|
||
FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold")
|
||
FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold")
|
||
MONO_FAMILY = "Courier New" if IS_WIN else "DejaVu Sans Mono"
|
||
FONT_MONO = (MONO_FAMILY, FONT_SIZE + 1)
|
||
FONT_KLEIN_FETT = FONT_TITEL = FONT_ZAHL = FONT_BOLD
|
||
|
||
|
||
def design_setzen(name: str) -> None:
|
||
"""„modern“ (hell), „dunkel“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply())."""
|
||
global DESIGN, MODERN, DUNKEL, FONT_FAMILY, FONT_SIZE, APP_NAME, MONO_FAMILY
|
||
DESIGN = name if name in _PALETTEN else "modern"
|
||
MODERN = DESIGN in ("modern", "dunkel")
|
||
DUNKEL = DESIGN == "dunkel"
|
||
APP_NAME = "Provisions-Manager" if MODERN else "Provisions-Manager 95"
|
||
globals().update(_PALETTEN[DESIGN])
|
||
FONT_FAMILY = ("Segoe UI" if MODERN else "MS Sans Serif") if IS_WIN else "DejaVu Sans"
|
||
MONO_FAMILY = ("Consolas" if MODERN else "Courier New") if IS_WIN else "DejaVu Sans Mono"
|
||
FONT_SIZE = 10 if MODERN else (8 if IS_WIN else 9)
|
||
_schriften()
|
||
|
||
|
||
def _schriften():
|
||
global FONT, FONT_BOLD, FONT_BIG, FONT_MONO, FONT_KLEIN_FETT, FONT_TITEL, FONT_ZAHL
|
||
FONT = (FONT_FAMILY, FONT_SIZE)
|
||
FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold")
|
||
FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold")
|
||
FONT_MONO = (MONO_FAMILY, FONT_SIZE + (0 if MODERN else 1))
|
||
FONT_KLEIN_FETT = (FONT_FAMILY, FONT_SIZE - 2, "bold") # Abschnitts-Überschriften (ABRECHNUNGEN …)
|
||
FONT_TITEL = (FONT_FAMILY, FONT_SIZE + 8, "bold") # Name der geöffneten Abrechnung
|
||
FONT_ZAHL = (MONO_FAMILY, FONT_SIZE + 6) if MODERN else (FONT_FAMILY, FONT_SIZE + 4, "bold") # Kennzahlen
|
||
|
||
|
||
SCHRIFTEN = Path(__file__).resolve().parent / "schriften"
|
||
|
||
|
||
def _schriften_laden() -> None:
|
||
"""IBM Plex (Hausschrift der Handy-Insel-Webtools) nur für dieses Programm laden – ohne Installation."""
|
||
import ctypes
|
||
import ctypes.util
|
||
dateien = sorted(SCHRIFTEN.glob("*.ttf"))
|
||
try:
|
||
if IS_WIN:
|
||
for f in dateien:
|
||
ctypes.windll.gdi32.AddFontResourceExW(str(f), 0x10, 0) # FR_PRIVATE
|
||
return
|
||
lib = ctypes.util.find_library("fontconfig")
|
||
if lib:
|
||
fc = ctypes.CDLL(lib)
|
||
fc.FcConfigAppFontAddFile.argtypes = [ctypes.c_void_p, ctypes.c_char_p]
|
||
for f in dateien:
|
||
fc.FcConfigAppFontAddFile(None, str(f).encode())
|
||
except Exception:
|
||
pass # dann eben Segoe UI / Systemschrift
|
||
|
||
# --------------------------------------------------------------------------- Pixel-Icons (16x16)
|
||
PALETTE = {
|
||
"k": "#000000", "w": "#ffffff", "g": "#c0c0c0", "d": "#808080", "y": "#ffff00", "Y": "#808000",
|
||
"b": "#000080", "B": "#0000ff", "r": "#ff0000", "R": "#800000", "G": "#008000", "L": "#00ff00",
|
||
"t": "#008080", "c": "#00ffff", "s": "#ffcc99", "o": "#ff8000",
|
||
}
|
||
|
||
ICONS = {
|
||
"neu": """
|
||
..kkkkkkkkk.....
|
||
..kwwwwwwwkk....
|
||
..kwwwwwwwkwk...
|
||
..kwwwwwwwkkkk..
|
||
..kwwwwwwwwwwk..
|
||
..kwkkkkkkkwwk..
|
||
..kwwwwwwwwwwk..
|
||
..kwkkkkkkkkwk..
|
||
..kwwwwwwwwwwk..
|
||
..kwkkkkkkkkwk..
|
||
..kwwwwwwwwwwk..
|
||
..kwkkkkkwwwwk..
|
||
..kwwwwwwwwwwk..
|
||
..kwwwwwwwwwwk..
|
||
..kkkkkkkkkkkk..
|
||
................""",
|
||
"oeffnen": """
|
||
................
|
||
.........kkk....
|
||
........k...k.k.
|
||
.............kk.
|
||
.kkk........kkk.
|
||
kyYyk...........
|
||
kYyYykkkkkkk....
|
||
kyYyYyYyYyYk....
|
||
kYyYykkkkkkkkkk.
|
||
kyYykyyyyyyyyyk.
|
||
kYykyyyyyyyyyk..
|
||
kykyyyyyyyyyk...
|
||
kkyyyyyyyyyk....
|
||
kkkkkkkkkkk.....
|
||
................
|
||
................""",
|
||
"import": """
|
||
......kkkk......
|
||
......kGGk......
|
||
......kGGk......
|
||
......kGGk......
|
||
...kkkkGGkkkk...
|
||
....kGGGGGGk....
|
||
.....kGGGGk.....
|
||
......kGGk......
|
||
.......kk.......
|
||
.kkkkkkkkkkkkkk.
|
||
.kwwwwwwwwwwwdk.
|
||
.kwkkwkkwkkwwdk.
|
||
.kwwwwwwwwwwwdk.
|
||
.kddddddddddddk.
|
||
.kkkkkkkkkkkkkk.
|
||
................""",
|
||
"export": """
|
||
kkkkkkkkkkkkkkk.
|
||
kbbkwwwwwwwkbbk.
|
||
kbbkwwwwwwwkbbk.
|
||
kbbkwwwwwwwkbbk.
|
||
kbbkwwwwwwwkbbk.
|
||
kbbkkkkkkkkkbbk.
|
||
kbbbbbbbbbbbbbk.
|
||
kbbbbbbbbbbbbbk.
|
||
kbbkkkkkkkkkkbk.
|
||
kbbkgggkkgggkbk.
|
||
kbbkgggkkgggkbk.
|
||
kbbkgggkkgggkbk.
|
||
kbbkgggggggggbk.
|
||
.kbkkkkkkkkkkbk.
|
||
..kkkkkkkkkkkkk.
|
||
................""",
|
||
"haendler": """
|
||
................
|
||
....kkk.........
|
||
...kssskk.kkk...
|
||
...ksksk.kssk...
|
||
...kssskksksk...
|
||
....kskkkssk....
|
||
...kbbbkkksk....
|
||
..kbbbbbkRRRk...
|
||
..kbbbbbkRRRRk..
|
||
..kbkbkbRRRRRk..
|
||
..kbkbkbkRkRRk..
|
||
..kbkbkbkRkRRk..
|
||
..kbbbbbkRRRRk..
|
||
...kbkbkkRkRk...
|
||
...kkkkk.kkkk...
|
||
................""",
|
||
"prozent": """
|
||
................
|
||
..kkk......kk...
|
||
.kwwwk....kk....
|
||
.kwkwk...kk.....
|
||
.kwwwk..kk......
|
||
..kkk..kk.......
|
||
......kk........
|
||
.....kk.........
|
||
....kk..........
|
||
...kk...kkk.....
|
||
..kk...kwwwk....
|
||
.kk....kwkwk....
|
||
kk.....kwwwk....
|
||
........kkk.....
|
||
................
|
||
................""",
|
||
"rechner": """
|
||
.kkkkkkkkkkkkk..
|
||
.kddddddddddddk.
|
||
.kdkkkkkkkkkkdk.
|
||
.kdkLLLLLLLLkdk.
|
||
.kdkkkkkkkkkkdk.
|
||
.kddddddddddddk.
|
||
.kdwwdwwdwwdrrk.
|
||
.kdkkdkkdkkdkkk.
|
||
.kddddddddddddk.
|
||
.kdwwdwwdwwdwwk.
|
||
.kdkkdkkdkkdkkk.
|
||
.kddddddddddddk.
|
||
.kdwwdwwdwwdwwk.
|
||
.kdkkdkkdkkdkkk.
|
||
.kkkkkkkkkkkkkk.
|
||
................""",
|
||
"info": """
|
||
.....kkkkkk.....
|
||
...kkBBBBBBkk...
|
||
..kBBBBBBBBBBk..
|
||
.kBBBBBwwBBBBBk.
|
||
.kBBBBBwwBBBBBk.
|
||
kBBBBBBBBBBBBBBk
|
||
kBBBBBwwwBBBBBBk
|
||
kBBBBBBwwBBBBBBk
|
||
kBBBBBBwwBBBBBBk
|
||
kBBBBBBwwBBBBBBk
|
||
.kBBBBBwwBBBBBk.
|
||
.kBBBBwwwwBBBBk.
|
||
..kBBBBBBBBBBk..
|
||
...kkBBBBBBkk...
|
||
.....kkkkkk.....
|
||
................""",
|
||
"warnung": """
|
||
.......kk.......
|
||
......kyyk......
|
||
......kyyk......
|
||
.....kyyyyk.....
|
||
.....kykkyk.....
|
||
....kyykkyyk....
|
||
....kyykkyyk....
|
||
...kyyykkyyyk...
|
||
...kyyykkyyyk...
|
||
..kyyyyyyyyyyk..
|
||
..kyyyykkyyyyk..
|
||
.kyyyyykkyyyyyk.
|
||
.kyyyyyyyyyyyyk.
|
||
kkkkkkkkkkkkkkkk
|
||
................
|
||
................""",
|
||
"fehler": """
|
||
.....kkkkkk.....
|
||
...kkrrrrrrkk...
|
||
..krrrrrrrrrrk..
|
||
.krrwrrrrrrwrrk.
|
||
.krwwwrrrrwwwrk.
|
||
krrrwwwrrwwwrrrk
|
||
krrrrwwwwwwrrrrk
|
||
krrrrrwwwwrrrrrk
|
||
krrrrrwwwwrrrrrk
|
||
krrrrwwwwwwrrrrk
|
||
.krrwwwrrwwwrrk.
|
||
.krwwwrrrrwwwrk.
|
||
..krrrrrrrrrrk..
|
||
...kkrrrrrrkk...
|
||
.....kkkkkk.....
|
||
................""",
|
||
"frage": """
|
||
.....kkkkkk.....
|
||
...kkwwwwwwkk...
|
||
..kwwwwwwwwwwk..
|
||
.kwwwwBBBBwwwwk.
|
||
.kwwwBBwwBBwwwk.
|
||
kwwwwwwwwBBwwwwk
|
||
kwwwwwwwBBwwwwwk
|
||
kwwwwwwBBwwwwwwk
|
||
kwwwwwwBBwwwwwwk
|
||
kwwwwwwwwwwwwwwk
|
||
.kwwwwwBBwwwwwk.
|
||
.kwwwwwBBwwwwwk.
|
||
..kwwwwwwwwwwk..
|
||
...kkwwwwwwkk...
|
||
.....kkkkkkk....
|
||
................""",
|
||
"birne": """
|
||
.....kkkkk......
|
||
....kyyyyyk.....
|
||
...kyywwyyyk....
|
||
..kyywwyyyyyk...
|
||
..kywwyyyyyyk...
|
||
..kyyyyyyyyyk...
|
||
..kyyyyyyyyyk...
|
||
...kyyyyyyyk....
|
||
....kyyyyyk.....
|
||
.....kyyyk......
|
||
.....kdddk......
|
||
.....kgggk......
|
||
.....kdddk......
|
||
.....kgggk......
|
||
......kkk.......
|
||
................""",
|
||
"app": """
|
||
................
|
||
.....kkkkkkk....
|
||
....kdddddddk...
|
||
....kdkkkkkdk...
|
||
....kdkLLLkdk...
|
||
....kdkLLLkdk...
|
||
....kdkkkkkdk...
|
||
....kdwdwdwdk...
|
||
....kdddddddk...
|
||
....kdwdwdwdk...
|
||
....kdddddddk...
|
||
..ttkdwdwdwdktt.
|
||
.tcccckkkkkkccct
|
||
tcccccyyyyyyccct
|
||
.ttttttttttttttt
|
||
................""",
|
||
}
|
||
|
||
|
||
def _icon_rows(name: str) -> list[str]:
|
||
rows = [r.strip() for r in ICONS[name].strip("\n").splitlines() if r.strip()]
|
||
return [r.ljust(16, ".")[:16] for r in rows[:16]] + ["." * 16] * (16 - len(rows))
|
||
|
||
|
||
MODERNE_ICONS = Path(__file__).resolve().parent / "bilder" / "modern"
|
||
|
||
|
||
def make_icon(master, name: str, scale: int = 1) -> tk.PhotoImage:
|
||
datei = MODERNE_ICONS / f"{name}{'_gross' if scale > 1 else ''}.png"
|
||
if MODERN and datei.exists(): # glatte Symbole aus build/make_moderne_icons.py
|
||
return tk.PhotoImage(master=master, file=str(datei))
|
||
img = tk.PhotoImage(master=master, width=16 * scale, height=16 * scale)
|
||
for y, row in enumerate(_icon_rows(name)):
|
||
for x, ch in enumerate(row):
|
||
if ch == ".":
|
||
continue
|
||
color = PALETTE[ch]
|
||
img.put(color, to=(x * scale, y * scale, (x + 1) * scale, (y + 1) * scale))
|
||
return img
|
||
|
||
|
||
class Icons:
|
||
"""Lazy-Cache, damit PhotoImages nicht vom Garbage Collector entsorgt werden."""
|
||
|
||
def __init__(self, master):
|
||
self.master = master
|
||
self._cache: dict[tuple[str, int], tk.PhotoImage] = {}
|
||
|
||
def __call__(self, name: str, scale: int = 1) -> tk.PhotoImage:
|
||
key = (name, scale)
|
||
if key not in self._cache:
|
||
self._cache[key] = make_icon(self.master, name, scale)
|
||
return self._cache[key]
|
||
|
||
|
||
# --------------------------------------------------------------------------- Theme
|
||
|
||
def apply(root: tk.Tk) -> None:
|
||
global FONT_FAMILY, MONO_FAMILY
|
||
if MODERN: # die erste vorhandene moderne Schrift, am liebsten IBM Plex
|
||
from tkinter import font as tkfont
|
||
_schriften_laden()
|
||
vorhanden = set(tkfont.families(root))
|
||
FONT_FAMILY = next((f for f in ("IBM Plex Sans", "Segoe UI", "Noto Sans", "Cantarell", "Ubuntu",
|
||
"DejaVu Sans") if f in vorhanden), FONT_FAMILY)
|
||
MONO_FAMILY = next((f for f in ("IBM Plex Mono", "Consolas", "DejaVu Sans Mono") if f in vorhanden),
|
||
MONO_FAMILY)
|
||
_schriften()
|
||
root.configure(bg=FACE)
|
||
o = root.option_add
|
||
o("*Font", FONT)
|
||
o("*Background", FACE)
|
||
o("*Foreground", DARK)
|
||
o("*activeBackground", FACE)
|
||
# Tk-Eigenheit: „activeForeground“ gehört zur Optionsklasse „Background“ – ohne diese Zeile
|
||
# wäre die Schrift gedrückter Knöpfe grau auf grau (unsichtbar).
|
||
o("*activeForeground", DARK)
|
||
o("*highlightThickness", 0)
|
||
o("*selectBackground", NAVY)
|
||
o("*selectForeground", WHITE)
|
||
o("*Button.relief", "raised")
|
||
o("*Button.borderWidth", 2)
|
||
o("*Button.padX", 8)
|
||
o("*Button.padY", 1)
|
||
o("*Entry.background", WHITE)
|
||
o("*Entry.relief", "sunken")
|
||
o("*Entry.borderWidth", 2)
|
||
o("*Entry.disabledBackground", FACE)
|
||
o("*Text.background", WHITE)
|
||
o("*Text.relief", "sunken")
|
||
o("*Text.borderWidth", 2)
|
||
o("*Listbox.background", WHITE)
|
||
o("*Listbox.relief", "sunken")
|
||
o("*Listbox.borderWidth", 2)
|
||
o("*Checkbutton.selectColor", WHITE)
|
||
o("*Radiobutton.selectColor", WHITE)
|
||
o("*Menu.relief", "raised")
|
||
o("*Menu.activeBackground", NAVY)
|
||
o("*Menu.activeForeground", WHITE)
|
||
o("*Menu.activeBorderWidth", 0)
|
||
o("*TCombobox*Listbox.background", WHITE)
|
||
o("*TCombobox*Listbox.selectBackground", NAVY)
|
||
|
||
from tkinter import font as tkfont
|
||
zeile = max(18, tkfont.Font(root=root, font=FONT).metrics("linespace") + 5)
|
||
wort_loeschen_einrichten(root)
|
||
st = ttk.Style(root)
|
||
st.theme_use("classic" if "classic" in st.theme_names() else "default")
|
||
st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=LIGHT,
|
||
selectbackground=NAVY, selectforeground=WHITE, fieldbackground=WHITE,
|
||
bordercolor=FACE, lightcolor=HILITE, darkcolor=SHADOW, highlightthickness=0)
|
||
st.configure("Treeview", background=WHITE, fieldbackground=WHITE, rowheight=zeile, borderwidth=0, font=FONT)
|
||
st.map("Treeview", background=[("selected", NAVY)], foreground=[("selected", WHITE)])
|
||
st.configure("Treeview.Heading", background=FACE, relief="raised", borderwidth=2, font=FONT, padding=(3, 1))
|
||
st.map("Treeview.Heading", relief=[("pressed", "sunken")], background=[("active", FACE)])
|
||
st.configure("TScrollbar", background=FACE, troughcolor=LIGHT, arrowcolor=DARK, relief="raised", borderwidth=2,
|
||
arrowsize=16, width=16)
|
||
st.map("TScrollbar", background=[("active", FACE)])
|
||
st.configure("TCombobox", fieldbackground=WHITE, background=FACE, arrowsize=14, padding=1)
|
||
st.map("TCombobox", fieldbackground=[("readonly", WHITE)], selectbackground=[("readonly", WHITE)],
|
||
selectforeground=[("readonly", DARK)])
|
||
st.configure("TPanedwindow", background=FACE)
|
||
st.configure("Sash", sashthickness=6, background=FACE)
|
||
if MODERN:
|
||
_modern(root, st, zeile)
|
||
|
||
|
||
def _modern(root: tk.Tk, st: ttk.Style, zeile: int) -> None:
|
||
"""Flach wie die Handy-Insel-Webtools: weiße Flächen, dünne Linien, Lila als Akzent."""
|
||
o = root.option_add
|
||
for klasse in ("Entry", "Text", "Listbox", "Spinbox"):
|
||
o(f"*{klasse}.relief", "flat")
|
||
o(f"*{klasse}.borderWidth", 4)
|
||
o(f"*{klasse}.highlightThickness", 1)
|
||
o(f"*{klasse}.highlightBackground", RAND2)
|
||
o(f"*{klasse}.highlightColor", NAVY)
|
||
o(f"*{klasse}.background", WHITE)
|
||
o(f"*{klasse}.foreground", DARK)
|
||
o(f"*{klasse}.insertBackground", DARK)
|
||
o("*Entry.disabledBackground", LIGHT)
|
||
o("*Button.relief", "flat")
|
||
o("*Button.borderWidth", 0)
|
||
o("*Button.highlightThickness", 1)
|
||
o("*Button.highlightBackground", RAND2)
|
||
o("*Button.background", FACE)
|
||
o("*Button.activeBackground", LIGHT)
|
||
o("*Button.padX", 12)
|
||
o("*Button.padY", 4)
|
||
o("*Button.cursor", "hand2")
|
||
for klasse in ("Checkbutton", "Radiobutton"):
|
||
o(f"*{klasse}.activeBackground", FACE)
|
||
o(f"*{klasse}.selectColor", WHITE)
|
||
o("*selectBackground", SEL_BG)
|
||
o("*selectForeground", SEL_FG)
|
||
o("*Menu.relief", "flat")
|
||
o("*TCombobox*Listbox.background", WHITE)
|
||
o("*TCombobox*Listbox.foreground", DARK)
|
||
o("*TCombobox*Listbox.selectBackground", SEL_BG)
|
||
o("*TCombobox*Listbox.selectForeground", SEL_FG)
|
||
st.theme_use("clam")
|
||
st.configure(".", background=FACE, foreground=DARK, font=FONT, troughcolor=FACE, selectbackground=SEL_BG,
|
||
selectforeground=SEL_FG, fieldbackground=WHITE, bordercolor=RAND, lightcolor=FACE,
|
||
darkcolor=RAND, focuscolor=NAVY, insertcolor=DARK)
|
||
st.configure("Treeview", background=WHITE, fieldbackground=WHITE, foreground=DARK, rowheight=zeile + 10,
|
||
borderwidth=0, font=FONT)
|
||
st.map("Treeview", background=[("selected", SEL_BG)], foreground=[("selected", SEL_FG)])
|
||
st.configure("Treeview.Heading", background=FACE, foreground=SHADOW, relief="flat", borderwidth=1,
|
||
font=FONT_KLEIN_FETT, padding=(10, 7), bordercolor=RAND, lightcolor=FACE, darkcolor=RAND)
|
||
st.map("Treeview.Heading", background=[("active", LIGHT)], relief=[("pressed", "flat")])
|
||
# Seitenleiste: ohne Kopf, hohe Zeilen, Auswahl lila wie im VVL-Tool
|
||
st.configure("Seite.Treeview", background=FACE, fieldbackground=FACE, rowheight=zeile + 16, font=FONT,
|
||
borderwidth=0)
|
||
st.layout("Seite.Treeview", [("Treeview.treearea", {"sticky": "nswe"})]) # ohne Rahmen
|
||
st.map("Seite.Treeview", background=[("selected", SEL_BG)], foreground=[("selected", NAVY)])
|
||
st.configure("TScrollbar", background=RAND2, troughcolor=FACE, arrowcolor=SHADOW, bordercolor=FACE,
|
||
lightcolor=RAND2, darkcolor=RAND2, relief="flat", arrowsize=10, width=10, gripcount=0)
|
||
st.map("TScrollbar", background=[("active", SHADOW)])
|
||
st.configure("TCombobox", fieldbackground=WHITE, background=FACE, foreground=DARK, arrowcolor=DARK, padding=4,
|
||
bordercolor=RAND2, lightcolor=WHITE, darkcolor=WHITE)
|
||
st.map("TCombobox", fieldbackground=[("readonly", WHITE)], selectbackground=[("readonly", WHITE)],
|
||
selectforeground=[("readonly", DARK)], bordercolor=[("focus", NAVY)])
|
||
st.configure("TPanedwindow", background=BG)
|
||
st.configure("Sash", sashthickness=6, background=BG)
|
||
|
||
|
||
# --------------------------------------------------------------------------- Wort löschen
|
||
|
||
def _wortgrenze(text: str, pos: int, richtung: int) -> int:
|
||
"""Position der nächsten Wortgrenze von pos aus (richtung -1 = links, +1 = rechts),
|
||
wie Strg+Rücktaste / Strg+Entf unter Windows: erst Leerzeichen, dann das Wort."""
|
||
if richtung < 0:
|
||
i = pos
|
||
while i > 0 and text[i - 1].isspace():
|
||
i -= 1
|
||
if i > 0 and text[i - 1].isalnum():
|
||
while i > 0 and text[i - 1].isalnum():
|
||
i -= 1
|
||
elif i > 0:
|
||
i -= 1 # Satzzeichen einzeln
|
||
return i
|
||
i = pos
|
||
if i < len(text) and text[i].isalnum():
|
||
while i < len(text) and text[i].isalnum():
|
||
i += 1
|
||
elif i < len(text):
|
||
i += 1
|
||
while i < len(text) and text[i].isspace():
|
||
i += 1
|
||
return i
|
||
|
||
|
||
def _wort_weg(event, richtung: int):
|
||
e = event.widget
|
||
try:
|
||
if str(e.cget("state")) in ("disabled", "readonly"):
|
||
return "break"
|
||
except tk.TclError:
|
||
pass
|
||
try:
|
||
if e.selection_present():
|
||
e.delete("sel.first", "sel.last")
|
||
return "break"
|
||
except (tk.TclError, AttributeError):
|
||
pass
|
||
text, pos = e.get(), e.index("insert")
|
||
grenze = _wortgrenze(text, pos, richtung)
|
||
if richtung < 0 and grenze < pos:
|
||
e.delete(grenze, pos)
|
||
elif richtung > 0 and grenze > pos:
|
||
e.delete(pos, grenze)
|
||
return "break"
|
||
|
||
|
||
def _text_wort_weg(event, richtung: int):
|
||
t = event.widget
|
||
if str(t.cget("state")) == "disabled":
|
||
return "break"
|
||
if t.tag_ranges("sel"):
|
||
t.delete("sel.first", "sel.last")
|
||
return "break"
|
||
if richtung < 0:
|
||
t.delete("insert -1c wordstart" if t.get("insert -1c").isspace() else "insert wordstart", "insert")
|
||
else:
|
||
t.delete("insert", "insert wordend")
|
||
return "break"
|
||
|
||
|
||
def wort_loeschen_einrichten(root: tk.Misc):
|
||
"""Strg+Rücktaste / Strg+Entf löschen das ganze Wort – in allen Eingabefeldern (Tk kann das
|
||
unter Windows von Haus aus nicht, dort wird nur ein Zeichen gelöscht)."""
|
||
for klasse in ("Entry", "TEntry", "TCombobox", "Spinbox", "TSpinbox"):
|
||
root.bind_class(klasse, "<Control-BackSpace>", lambda e: _wort_weg(e, -1))
|
||
root.bind_class(klasse, "<Control-Delete>", lambda e: _wort_weg(e, 1))
|
||
root.bind_class("Text", "<Control-BackSpace>", lambda e: _text_wort_weg(e, -1))
|
||
root.bind_class("Text", "<Control-Delete>", lambda e: _text_wort_weg(e, 1))
|
||
|
||
|
||
# --------------------------------------------------------------------------- Bausteine
|
||
|
||
class Sunken(tk.Frame):
|
||
"""Eingelassener Rahmen (Doppellinie wie bei Win95-Listen)."""
|
||
|
||
def __init__(self, master, **kw):
|
||
if MODERN:
|
||
kw.update(bd=0, relief="flat", highlightthickness=1, highlightbackground=RAND)
|
||
kw.setdefault("bd", 2)
|
||
kw.setdefault("relief", "sunken")
|
||
super().__init__(master, **kw)
|
||
|
||
|
||
class Statusleiste(tk.Frame):
|
||
def __init__(self, master, felder: list[tuple[str, int]]):
|
||
super().__init__(master, bg=FACE)
|
||
self.vars: dict[str, tk.StringVar] = {}
|
||
for i, (key, breite) in enumerate(felder):
|
||
v = tk.StringVar()
|
||
lbl = tk.Label(self, textvariable=v, bd=1, relief="flat" if MODERN else "sunken", anchor="w", padx=4)
|
||
if breite:
|
||
lbl.configure(width=breite)
|
||
lbl.pack(side="left", padx=(0, 2), pady=2)
|
||
else:
|
||
lbl.pack(side="left", fill="x", expand=True, padx=(0, 2), pady=2)
|
||
self.vars[key] = v
|
||
|
||
def set(self, key: str, text: str):
|
||
self.vars[key].set(text)
|
||
|
||
|
||
def knopf(master, text: str, command=None, icon: tk.PhotoImage | None = None, breite: int | None = None,
|
||
primaer: bool = False, **kw):
|
||
"""primaer: der eine Hauptknopf einer Fläche (modern dunkel-lila gefüllt, Win95 mit dickem Rahmen)."""
|
||
if MODERN:
|
||
if primaer:
|
||
kw = dict(bg=PRIMAER, fg=AUF_PRIMAER, activebackground=NAVY, activeforeground=AUF_PRIMAER,
|
||
highlightbackground=PRIMAER, font=FONT_BOLD) | kw
|
||
b = tk.Button(master, text=text, command=command, **kw) # Aussehen aus apply()/_modern()
|
||
else:
|
||
b = tk.Button(master, text=text, command=command, bd=2, relief="raised", bg=FACE,
|
||
activebackground=FACE, **kw)
|
||
if primaer:
|
||
b.configure(default="active")
|
||
if icon is not None:
|
||
b.configure(image=icon, compound="left", padx=6)
|
||
if breite:
|
||
b.configure(width=breite)
|
||
return b
|
||
|
||
|
||
class Karte(tk.Frame):
|
||
"""Fläche, die Inhalte zusammenfasst (Kennzahl, Tabelle): modern weiß mit dünner Linie."""
|
||
|
||
def __init__(self, master, **kw):
|
||
if MODERN:
|
||
kw = dict(bg=FACE, bd=0, highlightthickness=1, highlightbackground=RAND, highlightcolor=RAND) | kw
|
||
else:
|
||
kw = dict(bg=FACE, bd=2, relief="raised") | kw
|
||
super().__init__(master, **kw)
|
||
|
||
|
||
def titelleiste(master, text: str = "") -> tuple[tk.Frame, tk.Label]:
|
||
"""Win95: blaue Titelleiste oben in einer Karte („Fenster im Fenster“); rechts passen kleine Knöpfe hinein."""
|
||
f = tk.Frame(master, bg=NAVY)
|
||
f.pack(fill="x", padx=1, pady=1)
|
||
lbl = tk.Label(f, text=text, bg=NAVY, fg=WHITE, font=FONT_BOLD, anchor="w", padx=3)
|
||
lbl.pack(side="left", pady=1)
|
||
return f, lbl
|
||
|
||
|
||
def abschnitt(master, text: str, **kw) -> tk.Label:
|
||
"""Kleine Überschrift (modern in Großbuchstaben und grau, wie die Tabellenköpfe der Webtools)."""
|
||
if MODERN:
|
||
return tk.Label(master, text=text.upper(), fg=SHADOW, font=FONT_KLEIN_FETT, anchor="w", **kw)
|
||
return tk.Label(master, text=text, font=FONT_BOLD, anchor="w", **kw)
|
||
|
||
|
||
def _rund(c: tk.Canvas, x0, y0, x1, y1, r, **kw):
|
||
"""Abgerundetes Rechteck auf einem Canvas."""
|
||
r = min(r, (x1 - x0) / 2, (y1 - y0) / 2)
|
||
pkt = [x0 + r, y0, x1 - r, y0, x1, y0, x1, y0 + r, x1, y1 - r, x1, y1, x1 - r, y1, x0 + r, y1, x0, y1,
|
||
x0, y1 - r, x0, y0 + r, x0, y0]
|
||
return c.create_polygon(pkt, smooth=True, **kw)
|
||
|
||
|
||
class Pill(tk.Canvas):
|
||
"""Umschalter für Filter („Alle“, „Von Hand geändert“ …): modern abgerundet, Win95 als Knopf,
|
||
der eingedrückt bleibt. command bekommt nichts übergeben; aktiv wird von außen gesetzt."""
|
||
|
||
def __init__(self, master, text: str, command=None, aktiv: bool = False, **kw):
|
||
from tkinter import font as tkfont
|
||
f = tkfont.Font(root=master, font=FONT)
|
||
breite, hoehe = f.measure(text) + (24 if MODERN else 16), f.metrics("linespace") + (10 if MODERN else 8)
|
||
bg = kw.pop("bg", master.cget("bg"))
|
||
super().__init__(master, width=breite, height=hoehe, bg=bg, highlightthickness=0, bd=0, takefocus=1,
|
||
cursor="hand2" if MODERN else "", **kw)
|
||
self.text, self.command, self.aktiv = text, command, aktiv
|
||
self._zeichnen()
|
||
self.bind("<Button-1>", lambda e: self._ausloesen())
|
||
self.bind("<space>", lambda e: self._ausloesen())
|
||
self.bind("<Return>", lambda e: self._ausloesen())
|
||
|
||
def _ausloesen(self):
|
||
self.focus_set()
|
||
if self.command:
|
||
self.command()
|
||
|
||
def setzen(self, aktiv: bool):
|
||
if aktiv != self.aktiv:
|
||
self.aktiv = aktiv
|
||
self._zeichnen()
|
||
|
||
def _zeichnen(self):
|
||
self.delete("all")
|
||
b, h = int(self["width"]), int(self["height"])
|
||
if MODERN:
|
||
fuell, linie, fg = (PRIMAER, PRIMAER, AUF_PRIMAER) if self.aktiv else (FACE, RAND2, TEXT2)
|
||
_rund(self, 1, 1, b - 2, h - 2, h, fill=fuell, outline=linie)
|
||
else:
|
||
oben, unten = (SHADOW, HILITE) if self.aktiv else (HILITE, SHADOW)
|
||
self.create_rectangle(0, 0, b - 1, h - 1, fill=LIGHT if self.aktiv else FACE, outline=DARK)
|
||
self.create_line(1, h - 2, 1, 1, b - 2, 1, fill=oben)
|
||
self.create_line(1, h - 2, b - 2, h - 2, b - 2, 1, fill=unten)
|
||
fg = DARK
|
||
self.create_text(b // 2 + (1 if self.aktiv and not MODERN else 0), h // 2, text=self.text, fill=fg, font=FONT)
|
||
|
||
|
||
def punkt(master, farbe: str, d: int = 9) -> tk.PhotoImage:
|
||
"""Farbiger Punkt (Status in Listen); Ecken bleiben durchsichtig."""
|
||
img = tk.PhotoImage(master=master, width=d + 4, height=d)
|
||
r = d / 2
|
||
# Win95: grauer Ring, damit der Punkt auf weißem Grund und auf der blauen Auswahl sichtbar ist
|
||
for f, rr in ((farbe, r),) if MODERN else ((SHADOW, r), (farbe, r - 1)):
|
||
for y in range(d):
|
||
xs = [x for x in range(d) if (x + .5 - r) ** 2 + (y + .5 - r) ** 2 <= rr * rr]
|
||
if xs:
|
||
img.put(f, to=(xs[0], y, xs[-1] + 1, y + 1))
|
||
return img
|
||
|
||
|
||
def logo(master, groesse: int = 34) -> tk.Canvas:
|
||
"""Logo-Kachel wie in den Webtools: Lila-Verlauf, abgerundet, weißes %."""
|
||
c = tk.Canvas(master, width=groesse, height=groesse, bg=master.cget("bg"), highlightthickness=0, bd=0)
|
||
img = tk.PhotoImage(master=master, width=groesse, height=groesse)
|
||
a, e = (0x2d, 0x1b, 0x69), (0x5e, 0x35, 0xb1)
|
||
r = groesse * 0.28
|
||
for y in range(groesse):
|
||
for x in range(groesse):
|
||
dx = max(r - x - .5, x + .5 - (groesse - r), 0)
|
||
dy = max(r - y - .5, y + .5 - (groesse - r), 0)
|
||
if dx * dx + dy * dy <= r * r:
|
||
t = (x + y) / (2 * groesse - 2)
|
||
img.put("#%02x%02x%02x" % tuple(round(a[i] + (e[i] - a[i]) * t) for i in range(3)), (x, y))
|
||
c.create_image(0, 0, image=img, anchor="nw")
|
||
c.bild = img # type: ignore[attr-defined] # Referenz halten
|
||
c.create_text(groesse / 2, groesse / 2, text="%", fill="#ffffff", font=(FONT_FAMILY, int(groesse * .4), "bold"))
|
||
return c
|
||
|
||
|
||
class Tooltip:
|
||
def __init__(self, widget, text: str):
|
||
self.widget, self.text, self.tip, self._after = widget, text, None, None
|
||
widget.bind("<Enter>", self._plan, add="+")
|
||
widget.bind("<Leave>", self._weg, add="+")
|
||
widget.bind("<ButtonPress>", self._weg, add="+")
|
||
|
||
def _plan(self, _e):
|
||
self._after = self.widget.after(600, self._zeigen)
|
||
|
||
def _zeigen(self):
|
||
x = self.widget.winfo_rootx() + 10
|
||
y = self.widget.winfo_rooty() + self.widget.winfo_height() + 4
|
||
self.tip = tw = tk.Toplevel(self.widget)
|
||
tw.wm_overrideredirect(True)
|
||
tw.withdraw()
|
||
tk.Label(tw, text=self.text, bg=INFO_BG, bd=1, relief="solid", padx=4, pady=1).pack()
|
||
tw.update_idletasks()
|
||
# Größe + Position zusammen (reine Position ignoriert Tk unter Windows bei rahmenlosen Fenstern)
|
||
tw.wm_geometry(f"{tw.winfo_reqwidth()}x{tw.winfo_reqheight()}+{x}+{y}")
|
||
tw.deiconify()
|
||
|
||
def _weg(self, _e=None):
|
||
if self._after:
|
||
self.widget.after_cancel(self._after)
|
||
self._after = None
|
||
if self.tip:
|
||
self.tip.destroy()
|
||
self.tip = None
|
||
|
||
|
||
def trenner(master, vertikal: bool = True):
|
||
f = tk.Frame(master, bg=FACE)
|
||
if MODERN:
|
||
tk.Frame(f, width=1, height=1, bg=RAND).pack(side="left", fill="y") if vertikal else \
|
||
tk.Frame(f, height=1, bg=RAND).pack(fill="x")
|
||
return f
|
||
if vertikal:
|
||
tk.Frame(f, width=1, bg=SHADOW).pack(side="left", fill="y")
|
||
tk.Frame(f, width=1, bg=HILITE).pack(side="left", fill="y")
|
||
else:
|
||
tk.Frame(f, height=1, bg=SHADOW).pack(fill="x")
|
||
tk.Frame(f, height=1, bg=HILITE).pack(fill="x")
|
||
return f
|
||
|
||
|
||
def gruppe(master, titel: str, **kw) -> tk.LabelFrame:
|
||
if MODERN:
|
||
return tk.LabelFrame(master, text=titel, bd=0, relief="flat", bg=FACE, padx=4, pady=2, font=FONT_BOLD, **kw)
|
||
return tk.LabelFrame(master, text=titel, bd=2, relief="groove", bg=FACE, padx=6, pady=4, **kw)
|
||
|
||
|
||
def _bei_bedarf(leiste: ttk.Scrollbar):
|
||
"""Bildlaufleiste nur zeigen, wenn nicht alles hineinpasst."""
|
||
def setzen(lo, hi):
|
||
leiste.grid() if float(lo) > 0 or float(hi) < 1 else leiste.grid_remove()
|
||
leiste.set(lo, hi)
|
||
return setzen
|
||
|
||
|
||
def scroll_tree(master, auto: bool = False, **kw) -> tuple[tk.Frame, ttk.Treeview]:
|
||
"""Treeview mit Bildlaufleisten (modern nur bei Bedarf). auto: keine waagrechte Leiste (Seitenleiste)."""
|
||
rahmen = Sunken(master, bg=WHITE if MODERN else FACE) # Win95: graue Ecke zwischen den Leisten
|
||
tree = ttk.Treeview(rahmen, **kw)
|
||
vs = ttk.Scrollbar(rahmen, orient="vertical", command=tree.yview)
|
||
tree.grid(row=0, column=0, sticky="nsew")
|
||
vs.grid(row=0, column=1, sticky="ns")
|
||
if auto and MODERN:
|
||
rahmen.configure(highlightthickness=0, bg=FACE)
|
||
if not auto:
|
||
hs = ttk.Scrollbar(rahmen, orient="horizontal", command=tree.xview)
|
||
hs.grid(row=1, column=0, sticky="ew")
|
||
tree.configure(xscrollcommand=_bei_bedarf(hs) if MODERN else hs.set)
|
||
tree.configure(yscrollcommand=_bei_bedarf(vs) if auto or MODERN else vs.set)
|
||
rahmen.rowconfigure(0, weight=1)
|
||
rahmen.columnconfigure(0, weight=1)
|
||
return rahmen, tree
|
||
|
||
|
||
def auto_breite(tree: ttk.Treeview, flex: dict[str, float], reserve: int = 4):
|
||
"""Feste Spalten behalten ihre Breite, die „flex“-Spalten teilen sich den Rest anteilig.
|
||
So bleiben z. B. die Betragsspalten immer sichtbar, egal wie breit das Fenster ist."""
|
||
|
||
def anpassen(_e=None):
|
||
verfuegbar = tree.winfo_width() - reserve
|
||
if verfuegbar < 50:
|
||
return
|
||
alle = ["#0"] + list(tree["columns"]) if "tree" in str(tree["show"]) else list(tree["columns"])
|
||
fest = sum(int(tree.column(c, "width")) for c in alle if c not in flex)
|
||
rest = max(0, verfuegbar - fest)
|
||
summe = sum(flex.values())
|
||
for c, anteil in flex.items():
|
||
minimum = int(tree.column(c, "minwidth"))
|
||
tree.column(c, width=max(minimum, int(rest * anteil / summe)))
|
||
|
||
tree.bind("<Configure>", anpassen, add="+")
|
||
return anpassen
|
||
|
||
|
||
# --------------------------------------------------------------------------- Dialoge
|
||
|
||
# Klassischer Win95-Rahmen auch für Dialoge (wird von der App gesetzt, abschaltbar in den Einstellungen)
|
||
KLASSISCH = False
|
||
|
||
|
||
class Dialog(tk.Toplevel):
|
||
"""Modaler Dialog im Win95-Stil.
|
||
|
||
Modal heißt hier: Der INHALT des aufrufenden Fensters ist gesperrt (siehe modal.py), dessen
|
||
Titelleiste und Rand bleiben bedienbar. Unter Windows gehört der Dialog dem aufrufenden Fenster
|
||
(bleibt darüber, kommt mit Alt+Tab nach vorn) und hat einen eigenen Taskleisten-Eintrag.
|
||
"""
|
||
|
||
def __init__(self, master, titel: str, resizable: bool = False):
|
||
super().__init__(master, bg=FACE)
|
||
self.withdraw()
|
||
self.title(titel)
|
||
if not KLASSISCH:
|
||
# Rahmenlose Dialoge bekommen ihren Besitzer über win32 – mit transient() würde Tk sie
|
||
# unter Windows als Kindfenster anlegen, die dann hinter anderen Programmen hängen.
|
||
self.transient(master.winfo_toplevel())
|
||
self.resizable(resizable, resizable)
|
||
self.result = None
|
||
self._sperre = None
|
||
self.protocol("WM_DELETE_WINDOW", self.abbrechen)
|
||
self.bind("<Escape>", lambda e: self.abbrechen())
|
||
try:
|
||
self.iconphoto(False, master.winfo_toplevel()._app_icon) # type: ignore[attr-defined]
|
||
except Exception:
|
||
pass
|
||
self.titelleiste = None
|
||
if KLASSISCH:
|
||
from provtool.gui.rahmen import Titelleiste
|
||
self.overrideredirect(True)
|
||
self.configure(bd=2, relief="raised", padx=2, pady=2)
|
||
self.titelleiste = Titelleiste(self, self, titel, None, ("close",), schliessen=self.abbrechen)
|
||
self.titelleiste.pack(side="top", fill="x")
|
||
|
||
def zeigen(self, fokus=None):
|
||
from provtool.gui import modal, win32
|
||
if getattr(self._root(), "_provtool_schliesst", False):
|
||
self.destroy() # Programm wird beendet – keine neuen Dialoge mehr
|
||
return None
|
||
self.update_idletasks()
|
||
m = self.master.winfo_toplevel()
|
||
self._aufrufer = m
|
||
w, h = self.winfo_reqwidth(), self.winfo_reqheight()
|
||
x = m.winfo_rootx() + max(0, (m.winfo_width() - w) // 2)
|
||
y = m.winfo_rooty() + max(0, (m.winfo_height() - h) // 3)
|
||
self.geometry(f"{w}x{h}+{max(0, x)}+{max(0, y)}") # Größe + Position zusammen (siehe Tooltip)
|
||
self.deiconify()
|
||
self.update_idletasks()
|
||
if DUNKEL:
|
||
win32.dunkle_titelleiste(self)
|
||
if KLASSISCH and win32.IS_WIN:
|
||
# eigener Taskleisten-Eintrag; wird erst nach kurzem Aus-/Einblenden wirksam
|
||
if win32.taskleisteneintrag(self):
|
||
self.withdraw()
|
||
self.deiconify()
|
||
self.update_idletasks()
|
||
win32.besitzer_setzen(self, m)
|
||
self._sperre = modal.Sperre(m, self)
|
||
self.lift()
|
||
self.focus_force()
|
||
(fokus or self).focus_set()
|
||
self.wait_window()
|
||
return self.result
|
||
|
||
def nach_vorn(self, blinken: bool = False):
|
||
"""Dialog nach vorn holen (z. B. nach Klick auf das gesperrte Hauptfenster)."""
|
||
from provtool.gui import win32
|
||
try:
|
||
self.lift()
|
||
self.focus_force()
|
||
win32.nach_vorn(self)
|
||
except tk.TclError:
|
||
return
|
||
if blinken:
|
||
self.bell()
|
||
win32.blinken(self)
|
||
if self.titelleiste is not None:
|
||
self.titelleiste.blinken()
|
||
|
||
def destroy(self):
|
||
sperre = getattr(self, "_sperre", None)
|
||
if sperre is not None:
|
||
sperre.aufheben()
|
||
self._sperre = None
|
||
aufrufer = getattr(self, "_aufrufer", None)
|
||
super().destroy()
|
||
try: # Fokus zurück an das aufrufende Fenster
|
||
if aufrufer is not None and aufrufer.winfo_exists():
|
||
aufrufer.focus_force()
|
||
except tk.TclError:
|
||
pass
|
||
|
||
def abbrechen(self):
|
||
self.result = None
|
||
self.destroy()
|
||
|
||
def knopfleiste(self, knoepfe: list[tuple[str, object]], default: int = 0) -> tk.Frame:
|
||
f = tk.Frame(self, bg=FACE)
|
||
for i, (text, cmd) in enumerate(knoepfe):
|
||
b = knopf(f, text, cmd, breite=10, primaer=MODERN and i == default)
|
||
if i == default:
|
||
b.configure(default="active")
|
||
b.pack(side="left", padx=4)
|
||
return f
|
||
|
||
|
||
def _meldung(master, titel: str, text: str, icon: str, knoepfe: list[tuple[str, object]], default=0):
|
||
d = Dialog(master, titel)
|
||
body = tk.Frame(d, bg=FACE)
|
||
body.pack(fill="both", expand=True, padx=12, pady=(14, 8))
|
||
ic = make_icon(d, icon, 2)
|
||
d._ic = ic
|
||
tk.Label(body, image=ic, bg=FACE).pack(side="left", anchor="n", padx=(0, 14))
|
||
tk.Label(body, text=text, bg=FACE, justify="left", wraplength=420, anchor="w").pack(side="left", fill="x")
|
||
|
||
def mk(val):
|
||
def f():
|
||
d.result = val
|
||
d.destroy()
|
||
return f
|
||
|
||
leiste = d.knopfleiste([(t, mk(v)) for t, v in knoepfe], default)
|
||
leiste.pack(pady=(4, 12))
|
||
d.bind("<Return>", lambda e: mk(knoepfe[default][1])())
|
||
return d.zeigen()
|
||
|
||
|
||
def info(master, titel: str, text: str):
|
||
_meldung(master, titel, text, "info", [("OK", True)])
|
||
|
||
|
||
def warnung(master, titel: str, text: str):
|
||
_meldung(master, titel, text, "warnung", [("OK", True)])
|
||
|
||
|
||
def fehler(master, titel: str, text: str):
|
||
_meldung(master, titel, text, "fehler", [("OK", True)])
|
||
|
||
|
||
def frage(master, titel: str, text: str, ja: str = "Ja", nein: str = "Nein", default_ja: bool = True) -> bool:
|
||
return bool(_meldung(master, titel, text, "frage", [(ja, True), (nein, False)], 0 if default_ja else 1))
|
||
|
||
|
||
def eingabe(master, titel: str, text: str, wert: str = "") -> str | None:
|
||
d = Dialog(master, titel)
|
||
tk.Label(d, text=text, bg=FACE, justify="left", anchor="w").pack(fill="x", padx=12, pady=(12, 4))
|
||
var = tk.StringVar(value=wert)
|
||
e = tk.Entry(d, textvariable=var, width=46)
|
||
e.pack(padx=12, pady=4)
|
||
e.select_range(0, "end")
|
||
|
||
def ok():
|
||
d.result = var.get()
|
||
d.destroy()
|
||
|
||
d.bind("<Return>", lambda _e: ok())
|
||
d.knopfleiste([("OK", ok), ("Abbrechen", d.abbrechen)]).pack(pady=(6, 12))
|
||
return d.zeigen(e)
|