"""Windows-95-Optik für Tkinter: Farben, Schriften, Pixel-Icons und ein paar Bausteine.""" 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" 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) # --------------------------------------------------------------------------- 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: root.configure(bg=FACE) o = root.option_add o("*Font", FONT) o("*Background", FACE) o("*Foreground", DARK) o("*activeBackground", FACE) 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) 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) # --------------------------------------------------------------------------- Bausteine class Sunken(tk.Frame): """Eingelassener Rahmen (Doppellinie wie bei Win95-Listen).""" def __init__(self, master, **kw): 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): 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="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): 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 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(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.wm_geometry(f"+{x}+{y}") tk.Label(tw, text=self.text, bg=INFO_BG, bd=1, relief="solid", padx=4, pady=1).pack() 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 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: 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.""" def __init__(self, master, titel: str, resizable: bool = False): super().__init__(master, bg=FACE) self.withdraw() self.title(titel) self.transient(master.winfo_toplevel()) self.resizable(resizable, resizable) self.result = 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): 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() 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"+{max(0, x)}+{max(0, y)}") self.deiconify() self.lift() vorher = self.grab_current() # z. B. der Assistent, wenn hier eine Rückfrage aufgeht try: self.grab_set() except tk.TclError: pass bind_id = None if KLASSISCH: # Ein rahmenloser Dialog darf nicht hinter dem Hauptfenster verschwinden bind_id = m.bind("", lambda e: self.winfo_exists() and self.lift(), add="+") self.focus_force() (fokus or self).focus_set() self.wait_window() if bind_id: try: m.unbind("", bind_id) except tk.TclError: pass # Tk gibt beim Schließen die Sperre frei, stellt die vorige aber nicht wieder her try: if vorher is not None and vorher.winfo_exists(): vorher.grab_set() vorher.focus_force() except tk.TclError: pass return self.result 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)