Files

858 lines
30 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
APP_NAME = "Provisions-Manager 95" # modern: ohne „95“
_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"),
"modern": dict(FACE="#f3f3f3", HILITE="#ffffff", LIGHT="#ebebeb", SHADOW="#8a8a8a", DARK="#1b1b1b",
NAVY="#005fb8", TEAL="#e6eef6", WHITE="#ffffff", INFO_BG="#ffffff", SEL_BG="#cce4f7",
SEL_FG="#1b1b1b", RAND="#d1d1d1"),
}
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")
FONT_MONO = ("Courier New" if IS_WIN else "DejaVu Sans Mono", FONT_SIZE + 1)
def design_setzen(name: str) -> None:
"""„modern“ oder „win95“ – setzt Farben und Schriften (Feinschliff der Schrift in apply())."""
global DESIGN, MODERN, FONT_FAMILY, FONT_SIZE, APP_NAME
DESIGN = name if name in _PALETTEN else "modern"
MODERN = DESIGN == "modern"
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"
FONT_SIZE = 9 if (MODERN or not IS_WIN) else 8
_schriften()
def _schriften():
global FONT, FONT_BOLD, FONT_BIG
FONT = (FONT_FAMILY, FONT_SIZE)
FONT_BOLD = (FONT_FAMILY, FONT_SIZE, "bold")
FONT_BIG = (FONT_FAMILY, FONT_SIZE + 4, "bold")
# --------------------------------------------------------------------------- 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
if MODERN and not IS_WIN: # Linux: die erste vorhandene moderne Schrift
from tkinter import font as tkfont
vorhanden = set(tkfont.families(root))
FONT_FAMILY = next((f for f in ("Segoe UI", "Noto Sans", "Cantarell", "Ubuntu", "DejaVu Sans")
if f in vorhanden), FONT_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 und hell: dünne graue Rahmen statt 3D-Kanten, Akzentfarbe beim Fokus."""
o = root.option_add
for klasse in ("Entry", "Text", "Listbox", "Spinbox"):
o(f"*{klasse}.relief", "flat")
o(f"*{klasse}.borderWidth", 3)
o(f"*{klasse}.highlightThickness", 1)
o(f"*{klasse}.highlightBackground", RAND)
o(f"*{klasse}.highlightColor", NAVY)
o("*Button.relief", "flat")
o("*Button.borderWidth", 0)
o("*Button.highlightThickness", 1)
o("*Button.highlightBackground", RAND)
o("*Button.background", WHITE)
o("*Button.activeBackground", SEL_BG)
o("*Button.padY", 3)
o("*Checkbutton.activeBackground", FACE)
o("*Radiobutton.activeBackground", FACE)
o("*Menu.relief", "flat")
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=WHITE,
darkcolor=RAND, focuscolor=NAVY)
st.configure("Treeview", background=WHITE, fieldbackground=WHITE, rowheight=zeile + 4, borderwidth=0, font=FONT)
st.map("Treeview", background=[("selected", SEL_BG)], foreground=[("selected", SEL_FG)])
st.configure("Treeview.Heading", background=WHITE, relief="flat", borderwidth=1, font=FONT, padding=(6, 3),
bordercolor=RAND, lightcolor=WHITE, darkcolor=RAND)
st.map("Treeview.Heading", background=[("active", LIGHT)], relief=[("pressed", "flat")])
st.configure("TScrollbar", background=LIGHT, troughcolor=FACE, arrowcolor=SHADOW, bordercolor=FACE,
lightcolor=LIGHT, darkcolor=LIGHT, relief="flat", arrowsize=12, width=12)
st.map("TScrollbar", background=[("active", RAND)])
st.configure("TCombobox", fieldbackground=WHITE, background=WHITE, arrowcolor=DARK, padding=3,
bordercolor=RAND, lightcolor=WHITE, darkcolor=WHITE)
st.map("TCombobox", fieldbackground=[("readonly", WHITE)], selectbackground=[("readonly", WHITE)],
selectforeground=[("readonly", DARK)], bordercolor=[("focus", NAVY)])
# --------------------------------------------------------------------------- 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, **kw):
if MODERN:
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 icon is not None:
b.configure(image=icon, compound="left", padx=6)
if breite:
b.configure(width=breite)
return b
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 scroll_tree(master, **kw) -> tuple[tk.Frame, ttk.Treeview]:
rahmen = Sunken(master, bg=WHITE)
tree = ttk.Treeview(rahmen, **kw)
vs = ttk.Scrollbar(rahmen, orient="vertical", command=tree.yview)
hs = ttk.Scrollbar(rahmen, orient="horizontal", command=tree.xview)
tree.configure(yscrollcommand=vs.set, xscrollcommand=hs.set)
tree.grid(row=0, column=0, sticky="nsew")
vs.grid(row=0, column=1, sticky="ns")
hs.grid(row=1, column=0, sticky="ew")
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 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)
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)