Files
provisionsmanager95/provtool/gui/win95.py
T

1062 lines
41 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 Fenster(tk.Frame):
"""Ein „Fenster im Fenster“ mit blauer Titelleiste – reine Optik, gliedert die Oberfläche."""
def __init__(self, master, titel: str, icon: tk.PhotoImage | None = None, **kw):
if MODERN: # schlichte Überschrift statt blauer Titelleiste
super().__init__(master, bd=0, bg=FACE, **kw)
self.titel_var = tk.StringVar(value=titel)
self.titelleiste = tk.Label(self, textvariable=self.titel_var, bg=FACE, fg=DARK, font=FONT_BOLD,
anchor="w")
self.titelleiste.pack(fill="x", pady=(0, 4))
self.inhalt = tk.Frame(self, bg=FACE)
self.inhalt.pack(fill="both", expand=True)
return
super().__init__(master, bd=2, relief="raised", bg=FACE, **kw)
self.titelleiste = tk.Frame(self, bg=NAVY, height=18)
self.titelleiste.pack(fill="x", padx=1, pady=1)
if icon is not None:
tk.Label(self.titelleiste, image=icon, bg=NAVY).pack(side="left", padx=(2, 0))
self.titel_var = tk.StringVar(value=titel)
tk.Label(self.titelleiste, textvariable=self.titel_var, bg=NAVY, fg=WHITE, font=FONT_BOLD,
anchor="w").pack(side="left", fill="x", padx=3)
self.inhalt = tk.Frame(self, bg=FACE)
self.inhalt.pack(fill="both", expand=True, padx=2, pady=2)
def set_titel(self, t: str):
self.titel_var.set(t)
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 (wie bei 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 ToolbarButton(tk.Label):
"""Flacher Werkzeugleisten-Knopf (Office-95-Stil): hebt sich beim Überfahren, senkt sich beim Klick."""
def __init__(self, master, text: str, icon: tk.PhotoImage, command, tooltip: str = ""):
super().__init__(master, text=text, image=icon, compound="top", bd=1, relief="flat", bg=FACE,
padx=6, pady=2, font=FONT)
self.command = command
if MODERN: # Hervorhebung durch Farbe statt 3D-Kante
self.configure(bd=0, padx=8, pady=4)
self.bind("<Enter>", lambda e: self.configure(bg=LIGHT))
self.bind("<Leave>", lambda e: self.configure(bg=FACE))
self.bind("<ButtonPress-1>", lambda e: self.configure(bg=SEL_BG))
self.bind("<ButtonRelease-1>", self._release)
if tooltip:
Tooltip(self, tooltip)
return
self.bind("<Enter>", lambda e: self.configure(relief="raised"))
self.bind("<Leave>", lambda e: self.configure(relief="flat"))
self.bind("<ButtonPress-1>", lambda e: self.configure(relief="sunken"))
self.bind("<ButtonRelease-1>", self._release)
if tooltip:
Tooltip(self, tooltip)
def _release(self, e):
self.configure(bg=LIGHT) if MODERN else self.configure(relief="raised")
if 0 <= e.x < self.winfo_width() and 0 <= e.y < self.winfo_height() and self.command:
self.command()
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)