"""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 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 _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 DESIGN = name if name in _PALETTEN else "modern" MODERN = DESIGN == "modern" 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)) def make_icon(master, name: str, scale: int = 1) -> tk.PhotoImage: 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, "", lambda e: _wort_weg(e, -1)) root.bind_class(klasse, "", lambda e: _wort_weg(e, 1)) root.bind_class("Text", "", lambda e: _text_wort_weg(e, -1)) root.bind_class("Text", "", 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("", lambda e: self.configure(bg=LIGHT)) self.bind("", lambda e: self.configure(bg=FACE)) self.bind("", lambda e: self.configure(bg=SEL_BG)) self.bind("", self._release) if tooltip: Tooltip(self, tooltip) return self.bind("", lambda e: self.configure(relief="raised")) self.bind("", lambda e: self.configure(relief="flat")) self.bind("", lambda e: self.configure(relief="sunken")) self.bind("", 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("", self._plan, add="+") widget.bind("", self._weg, add="+") widget.bind("", 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("", 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("", 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("", 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("", lambda _e: ok()) d.knopfleiste([("OK", ok), ("Abbrechen", d.abbrechen)]).pack(pady=(6, 12)) return d.zeigen(e)