Win95-Titelleiste und -Menüleiste, Einrichtungs-Assistent, Rundgang, Tipp des Tages
Windows-Build / windows-exe (push) Successful in 1m43s

- selbst gezeichneter Fensterrahmen (verschieben, maximieren, minimieren, Größe ändern), abschaltbar
- Dialoge mit Win95-Titelleiste; vorherige Modalsperre wird nach Rückfragen wiederhergestellt
- Assistent: Speicherort, Unterhändler (Nummern aus Abrechnung vorschlagen), erste Abrechnung
- Rundgang mit Hinweis-Sprechblasen, Tipp des Tages

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 5.5 committed 2026-11-02 10:14:19 +01:00
1 parent 6f860746ca
commit 6b520bce35
7 files changed
+1128 -43

No files matched your search

+372
View File
@@ -0,0 +1,372 @@
"""Selbst gezeichneter Windows-95-Fensterrahmen.
Windows erlaubt Tk nicht, die echte Titelleiste umzufärben. Deshalb wird sie ausgeblendet
(overrideredirect) und hier nachgebaut: blaue Titelleiste mit Icon und _ □ x, Verschieben,
Doppelklick = Maximieren, Größenänderung über den Rand und den Anfasser unten rechts,
graue Menüleiste. Unter Windows sorgen ein paar Win32-Aufrufe dafür, dass das Fenster
trotzdem in der Taskleiste erscheint, per Alt+Tab erreichbar ist und minimiert werden kann.
"""
from __future__ import annotations
import sys
import tkinter as tk
from provtool.gui import win95 as w
IS_WIN = sys.platform.startswith("win")
# --------------------------------------------------------------------------- Glyphen
GLYPHEN = {
"min": """
........
........
........
........
........
........
kkkkkk..
kkkkkk..""",
"max": """
kkkkkkkkk
kkkkkkkkk
k.......k
k.......k
k.......k
k.......k
k.......k
kkkkkkkkk""",
"restore": """
..kkkkkk
..kkkkkk
..k....k
kkkkkk.k
kkkkkk.k
k....kkk
k....k..
kkkkkk..""",
"close": """
kk....kk
.kk..kk.
..kkkk..
...kk...
..kkkk..
.kk..kk.
kk....kk""",
}
def glyph(master, name: str) -> tk.PhotoImage:
rows = [r.strip() for r in GLYPHEN[name].strip("\n").splitlines() if r.strip()]
breite = max(len(r) for r in rows)
img = tk.PhotoImage(master=master, width=breite, height=len(rows))
for y, row in enumerate(rows):
for x, ch in enumerate(row):
if ch != ".":
img.put(w.PALETTE[ch], (x, y))
return img
def griff_bild(master, groesse: int = 12) -> tk.PhotoImage:
"""Der typische schraffierte Größen-Anfasser unten rechts."""
img = tk.PhotoImage(master=master, width=groesse, height=groesse)
for y in range(groesse):
for x in range(groesse):
d = x + y - (groesse - 1)
if d >= 0:
farbe = (w.HILITE, w.SHADOW, w.SHADOW, None)[d % 4]
if farbe:
img.put(farbe, (x, y))
return img
# --------------------------------------------------------------------------- Win32-Hilfen
def _hwnd(fenster: tk.Misc) -> int:
import ctypes
return ctypes.windll.user32.GetParent(fenster.winfo_id())
def taskleiste_anzeigen(fenster: tk.Tk) -> None:
"""Ein overrideredirect-Fenster erscheint unter Windows sonst nicht in Taskleiste/Alt+Tab."""
if not IS_WIN:
return
try:
import ctypes
GWL_EXSTYLE, WS_EX_APPWINDOW, WS_EX_TOOLWINDOW = -20, 0x00040000, 0x00000080
hwnd = _hwnd(fenster)
stil = ctypes.windll.user32.GetWindowLongW(hwnd, GWL_EXSTYLE)
stil = (stil & ~WS_EX_TOOLWINDOW) | WS_EX_APPWINDOW
ctypes.windll.user32.SetWindowLongW(hwnd, GWL_EXSTYLE, stil)
fenster.wm_withdraw()
fenster.after(10, lambda: (fenster.wm_deiconify(), fenster.focus_force()))
except Exception:
pass
def arbeitsbereich(fenster: tk.Misc) -> tuple[int, int, int, int]:
"""Bildschirmbereich ohne Taskleiste (des Monitors, auf dem das Fenster steht): x, y, breite, höhe."""
if IS_WIN:
try:
import ctypes
from ctypes import wintypes
class MONITORINFO(ctypes.Structure):
_fields_ = [("cbSize", wintypes.DWORD), ("rcMonitor", wintypes.RECT),
("rcWork", wintypes.RECT), ("dwFlags", wintypes.DWORD)]
mon = ctypes.windll.user32.MonitorFromWindow(_hwnd(fenster), 2) # MONITOR_DEFAULTTONEAREST
mi = MONITORINFO()
mi.cbSize = ctypes.sizeof(MONITORINFO)
if ctypes.windll.user32.GetMonitorInfoW(mon, ctypes.byref(mi)):
r = mi.rcWork
return r.left, r.top, r.right - r.left, r.bottom - r.top
except Exception:
pass
return 0, 0, fenster.winfo_screenwidth(), fenster.winfo_screenheight()
# --------------------------------------------------------------------------- Bausteine
class TitelKnopf(tk.Label):
def __init__(self, master, bild: tk.PhotoImage, command):
super().__init__(master, image=bild, bg=w.FACE, bd=2, relief="raised", width=14, height=12)
self.command = command
self.bind("<ButtonPress-1>", lambda e: self.configure(relief="sunken"))
self.bind("<ButtonRelease-1>", self._los)
def _los(self, e):
self.configure(relief="raised")
if 0 <= e.x < self.winfo_width() and 0 <= e.y < self.winfo_height():
self.command()
class Titelleiste(tk.Frame):
"""Blaue Win95-Titelleiste. knoepfe: Teilmenge von ("min", "max", "close")."""
def __init__(self, master, fenster: tk.Misc, titel: str, icon: tk.PhotoImage | None, knoepfe=("close",),
schliessen=None, minimieren=None, maximieren=None):
super().__init__(master, bg=w.NAVY, height=20)
self.fenster = fenster
self._bilder = {n: glyph(self, n) for n in ("min", "max", "restore", "close")}
if icon is not None:
tk.Label(self, image=icon, bg=w.NAVY).pack(side="left", padx=(2, 0))
self.titel_var = tk.StringVar(value=titel)
self.titel_lbl = tk.Label(self, textvariable=self.titel_var, bg=w.NAVY, fg=w.WHITE, font=w.FONT_BOLD,
anchor="w")
self.titel_lbl.pack(side="left", fill="x", expand=True, padx=3)
self.knoepfe: dict[str, TitelKnopf] = {}
if "close" in knoepfe:
self.knoepfe["close"] = TitelKnopf(self, self._bilder["close"], schliessen)
self.knoepfe["close"].pack(side="right", padx=(2, 2), pady=2)
if "max" in knoepfe:
self.knoepfe["max"] = TitelKnopf(self, self._bilder["max"], maximieren)
self.knoepfe["max"].pack(side="right", pady=2)
if "min" in knoepfe:
self.knoepfe["min"] = TitelKnopf(self, self._bilder["min"], minimieren)
self.knoepfe["min"].pack(side="right", pady=2)
self._maximieren = maximieren
for widget in (self, self.titel_lbl):
widget.bind("<ButtonPress-1>", self._start)
widget.bind("<B1-Motion>", self._ziehen)
if maximieren:
widget.bind("<Double-Button-1>", lambda e: maximieren())
self._ab = (0, 0)
self.ziehen_erlaubt = lambda: True
def set_titel(self, t: str):
self.titel_var.set(t)
def max_symbol(self, maximiert: bool):
if "max" in self.knoepfe:
self.knoepfe["max"].configure(image=self._bilder["restore" if maximiert else "max"])
def _start(self, e):
self._ab = (e.x_root - self.fenster.winfo_rootx(), e.y_root - self.fenster.winfo_rooty())
def _ziehen(self, e):
if not self.ziehen_erlaubt():
return
self.fenster.geometry(f"+{e.x_root - self._ab[0]}+{e.y_root - self._ab[1]}")
class Menueleiste(tk.Frame):
"""Graue Menüleiste (die native von Windows 10/11 ist weiß und lässt sich nicht umfärben)."""
def __init__(self, master, root: tk.Misc):
super().__init__(master, bg=w.FACE)
self.root = root
self.knoepfe: dict[str, tk.Menubutton] = {}
def menue(self, label: str, underline: int | None = 0) -> tk.Menu:
mb = tk.Menubutton(self, text=label, bg=w.FACE, relief="flat", bd=1, padx=6, pady=1,
activebackground=w.NAVY, activeforeground=w.WHITE,
underline=-1 if underline is None else underline)
menu = tk.Menu(mb, tearoff=0)
mb.configure(menu=menu)
mb.pack(side="left")
self.knoepfe[label] = mb
if underline is not None:
taste = label[underline].lower()
self.root.bind_all(f"<Alt-KeyPress-{taste}>", lambda e, m=mb: self.oeffnen(m))
return menu
def oeffnen(self, mb: tk.Menubutton):
menu = mb.nametowidget(mb.cget("menu"))
menu.tk_popup(mb.winfo_rootx(), mb.winfo_rooty() + mb.winfo_height())
return "break"
# --------------------------------------------------------------------------- Hauptfenster
class Hauptrahmen:
"""Umgibt das Hauptfenster. Im klassischen Modus mit eigener Titelleiste, sonst nur ein Container."""
RAND = 5 # Pixel am Rand, an denen man die Größe ziehen kann
def __init__(self, root: tk.Tk, titel: str, icon: tk.PhotoImage, schliessen, klassisch: bool = True,
minbreite: int = 900, minhoehe: int = 520):
self.root, self.klassisch = root, klassisch
self.minbreite, self.minhoehe = minbreite, minhoehe
self.maximiert = False
self._normal_geo: str | None = None
self._schliessen = schliessen
root.minsize(minbreite, minhoehe)
if not klassisch:
self.aussen = tk.Frame(root, bg=w.FACE)
self.aussen.pack(fill="both", expand=True)
self.inhalt = self.aussen
self.titelleiste = None
return
root.overrideredirect(True)
self.aussen = tk.Frame(root, bg=w.FACE, bd=2, relief="raised", padx=2, pady=2)
self.aussen.pack(fill="both", expand=True)
self.titelleiste = Titelleiste(self.aussen, root, titel, icon, ("min", "max", "close"),
schliessen=schliessen, minimieren=self.minimieren,
maximieren=self.umschalten)
self.titelleiste.pack(fill="x")
self.titelleiste.ziehen_erlaubt = lambda: not self.maximiert
self.inhalt = tk.Frame(self.aussen, bg=w.FACE)
self.inhalt.pack(fill="both", expand=True)
self._rand: str = ""
self.aussen.bind("<Motion>", self._rand_cursor)
self.aussen.bind("<Leave>", lambda e: self.aussen.configure(cursor=""))
self.aussen.bind("<ButtonPress-1>", self._resize_start)
self.aussen.bind("<B1-Motion>", self._resize)
root.bind_all("<Alt-F4>", lambda e: schliessen())
root.after(50, lambda: taskleiste_anzeigen(root))
# ---- öffentliche Helfer
def set_titel(self, text: str):
self.root.title(text)
if self.titelleiste:
self.titelleiste.set_titel(text)
def griff(self, master) -> tk.Label | None:
"""Anfasser für die Statusleiste (nur im klassischen Modus)."""
if not self.klassisch:
return None
self._griff_bild = griff_bild(master)
g = tk.Label(master, image=self._griff_bild, bg=w.FACE, cursor="sizing" if not IS_WIN else "size_nw_se")
g.bind("<ButtonPress-1>", lambda e: self._resize_start(e, "se"))
g.bind("<B1-Motion>", self._resize)
return g
def geometrie_setzen(self, geo: str | None, maximiert: bool = False):
self.root.update_idletasks()
if not geo or "+" not in geo:
breite, hoehe = 1200, 720
if geo and "x" in geo:
try:
breite, hoehe = (int(v) for v in geo.split("+")[0].split("x"))
except ValueError:
pass
x0, y0, bw, bh = arbeitsbereich(self.root)
breite, hoehe = min(breite, bw), min(hoehe, bh)
geo = f"{breite}x{hoehe}+{x0 + (bw - breite) // 2}+{y0 + (bh - hoehe) // 2}"
self.root.geometry(geo)
if maximiert and self.klassisch:
self.root.after(100, self.maximieren)
def normale_geometrie(self) -> str:
return self._normal_geo if self.maximiert and self._normal_geo else self.root.geometry()
# ---- Fensterfunktionen
def minimieren(self):
# Windows minimiert keine overrideredirect-Fenster → kurz normalen Rahmen aktivieren
geo = self.root.geometry()
self.root.overrideredirect(False)
self.root.iconify()
def wieder_da(e):
if e.widget is self.root and self.root.state() == "normal":
self.root.unbind("<Map>")
self.root.overrideredirect(True)
self.root.geometry(geo)
taskleiste_anzeigen(self.root)
self.root.bind("<Map>", wieder_da)
def maximieren(self):
if self.maximiert:
return
self._normal_geo = self.root.geometry()
x, y, bw, bh = arbeitsbereich(self.root)
self.root.geometry(f"{bw}x{bh}+{x}+{y}")
self.maximiert = True
self.titelleiste.max_symbol(True)
def wiederherstellen(self):
if not self.maximiert:
return
self.maximiert = False
if self._normal_geo:
self.root.geometry(self._normal_geo)
self.titelleiste.max_symbol(False)
def umschalten(self):
self.wiederherstellen() if self.maximiert else self.maximieren()
# ---- Größe ändern
def _welcher_rand(self, x, y) -> str:
bw, bh, r = self.aussen.winfo_width(), self.aussen.winfo_height(), self.RAND
rand = ("n" if y < r else "s" if y >= bh - r else "") + ("w" if x < r else "e" if x >= bw - r else "")
return rand
CURSOR_WIN = {"n": "size_ns", "s": "size_ns", "e": "size_we", "w": "size_we",
"nw": "size_nw_se", "se": "size_nw_se", "ne": "size_ne_sw", "sw": "size_ne_sw"}
CURSOR_X11 = {"n": "top_side", "s": "bottom_side", "e": "right_side", "w": "left_side",
"nw": "top_left_corner", "se": "bottom_right_corner", "ne": "top_right_corner",
"sw": "bottom_left_corner"}
def _rand_cursor(self, e):
if self.maximiert:
return
rand = self._welcher_rand(e.x, e.y)
try:
self.aussen.configure(cursor=(self.CURSOR_WIN if IS_WIN else self.CURSOR_X11).get(rand, ""))
except tk.TclError:
pass
def _resize_start(self, e, rand: str | None = None):
self._rand = rand or self._welcher_rand(e.x, e.y)
self._start = (e.x_root, e.y_root, self.root.winfo_x(), self.root.winfo_y(),
self.root.winfo_width(), self.root.winfo_height())
def _resize(self, e):
if not self._rand or self.maximiert:
return
sx, sy, x, y, bw, bh = self._start
dx, dy = e.x_root - sx, e.y_root - sy
if "e" in self._rand:
bw = max(self.minbreite, bw + dx)
if "s" in self._rand:
bh = max(self.minhoehe, bh + dy)
if "w" in self._rand:
neu = max(self.minbreite, bw - dx)
x, bw = x + (bw - neu), neu
if "n" in self._rand:
neu = max(self.minhoehe, bh - dy)
y, bh = y + (bh - neu), neu
self.root.geometry(f"{bw}x{bh}+{x}+{y}")