diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..e3fff1e --- /dev/null +++ b/.gitattributes @@ -0,0 +1,23 @@ +# Zeilenenden festlegen, damit sie nicht von der Maschine abhaengen. +# +# Neun Dateien liegen in beiden Programmen gleich und werden von Hand +# hinueberkopiert; pruef_gemeinsam_automatisch.py prueft das byteweise. +# Ohne diese Festlegung entscheidet core.autocrlf jeder Maschine einzeln, +# wie eine Datei im Arbeitsverzeichnis aussieht - und dann meldet die +# Pruefung einen Unterschied, den es im Repository gar nicht gibt. +# Genau so stand es am 07.09.2026 bei hinweis.py: gleicher Inhalt, die +# eine Fassung CRLF, die andere LF. +* text=auto +*.py text eol=crlf +*.iss text eol=crlf +*.md text eol=crlf +*.json text eol=crlf + +# Binaeres nie anfassen. +*.png binary +*.ico binary +*.svg text eol=crlf +*.ttf binary +*.exe binary +*.pdf binary +*.sqlite3 binary diff --git a/ablage.py b/ablage.py index cf7937d..d00bc49 100644 --- a/ablage.py +++ b/ablage.py @@ -77,8 +77,13 @@ def firmen_ordner(): Nummernbuch eine Ebene darueber - so nimmt ein Deinstallieren es nicht mit (siehe gemeinsam.standard_ordner). """ - heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~") - return os.path.join(heim, "ravokk") + # EINE Quelle fuer diesen Pfad. Er wurde an drei Stellen einzeln + # ausgerechnet - hier, in gemeinsam.py und in der config.py des + # Rechnungstools. Laeuft eine davon je auseinander, fuehren die beiden + # Programme zwei Nummernbuecher, und das faellt erst auf, wenn eine + # Rechnungsnummer zum zweiten Mal vergeben ist. + import gemeinsam + return gemeinsam.standard_ordner() def _zeiger_datei(): diff --git a/api.py b/api.py index 98eff1e..196de45 100644 --- a/api.py +++ b/api.py @@ -138,6 +138,10 @@ class _Handler(socketserver.StreamRequestHandler): try: self.wfile.write((json.dumps({"antwort": antwort}) + "\n").encode("utf-8")) except OSError: + # Der Rufer hat die Verbindung schon zugemacht - das ist der + # Normalfall bei "schau_nach", wenn das andere Programm beendet + # wurde, waehrend die Antwort unterwegs war. Die Arbeit ist + # getan; nur die Empfangsbestaetigung geht ins Leere. pass diff --git a/app.py b/app.py index b0abadb..a99c4c2 100644 --- a/app.py +++ b/app.py @@ -196,39 +196,11 @@ class App(theme.ThemeMixin, tk.Tk): niemand mehr findet. Neu bauen kann nichts vergessen. Die Werte haengen an Tk-Variablen (Jahr, Suchfeld) und am Journal - - beides ueberlebt. Gesichert werden sie trotzdem, denn die - _build-Methoden legen ihre Variablen selbst an: nach dem Neubau - stuenden sonst neue, leere da. Genau das ist im Rechnungstool beim - ersten Versuch passiert. + beides ueberlebt. Gesichert werden sie trotzdem; warum, steht bei + variablen_sichern() im ThemeMixin. """ - gesichert = {} - for name in dir(self): - if name.startswith("__"): - continue - try: - wert = getattr(self, name) - except Exception: # noqa: BLE001 - Eigenschaften koennen werfen - continue - if isinstance(wert, (tk.StringVar, tk.IntVar, tk.DoubleVar, - tk.BooleanVar)): - try: - gesichert[name] = wert.get() - except tk.TclError: - pass - - # Fremde Fenster stehen lassen! Ein tk.Toplevel IST ein Kind des - # Hauptfensters - der offene Darstellungsdialog wurde hier bisher - # mitgeloescht, und die Zeile danach fasste eine Leiche an - # ("invalid command name .!toplevel"). Gefunden am 07.09.2026 von der - # Pruefkette, mit dem echten Bedienweg gemessen: Klick auf "Dunkel" - # -> Farbe wechselt, Fenster ist weg, Traceback im Log. - # - # Der Pruefstand hat es nicht gefunden, weil er cfg["darstellung"] von - # Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser - # dem Weg, den ein Mensch geht. - for kind in self.winfo_children(): - if not isinstance(kind, tk.Toplevel): - kind.destroy() + gesichert = self.variablen_sichern() + self.kinder_loeschen() self._apply_theme() self._menue() @@ -236,59 +208,19 @@ class App(theme.ThemeMixin, tk.Tk): self._blaetter() self._statusleiste() - for name, wert in gesichert.items(): - neu = getattr(self, name, None) - if isinstance(neu, (tk.StringVar, tk.IntVar, tk.DoubleVar, - tk.BooleanVar)): - try: - neu.set(wert) - except tk.TclError: - pass + self.variablen_zuruecksetzen(gesichert) self.aktualisieren() - def dlg_darstellung(self): - """Hell, dunkel oder wie Windows - und sofort sichtbar.""" - d = tk.Toplevel(self) - d.title("Darstellung") - d.transient(self) - d.resizable(False, False) - self._style_dialog(d) - rahmen = ttk.Frame(d, padding=18) - rahmen.pack(fill="both", expand=True) - ttk.Label(rahmen, text="Darstellung", - font=(theme.SCHRIFT_NAME, 11, "bold"), - foreground=self.col_accent).pack(anchor="w") - ttk.Label(rahmen, justify="left", wraplength=340, - foreground=self.col_muted, - text="Gilt für das Programmfenster. Das Menü bleibt in der " - "Farbe von Windows – ein natives Menü lässt sich dort " - "nicht einfärben.").pack(anchor="w", pady=(6, 12)) - - gewaehlt = tk.StringVar(value=theme.modus(self.cfg)) - - def uebernehmen(): - neu = gewaehlt.get() - if neu == theme.modus(self.cfg): - return - self.cfg["darstellung"] = neu - self._oberflaeche_neu() - self._style_dialog(d) - - for wert in theme.MODI: - ttk.Radiobutton(rahmen, text=theme.MODUS_NAMEN[wert], value=wert, - variable=gewaehlt, command=uebernehmen).pack( - anchor="w", pady=3) - - ttk.Separator(rahmen, orient="horizontal").pack(fill="x", pady=(12, 10)) - ttk.Button(rahmen, text="Schließen", width=14, - command=d.destroy).pack(anchor="e") - theme.mittig(d, self) - return d + # Der Dialog selbst steht im ThemeMixin (theme.py). Gespeichert werden + # muss hier nichts: self.cfg ist die Ablage aus _einstellungen(), die + # jeden Schreibzugriff sofort in die Journaltabelle durchreicht. + DARSTELLUNG_HINWEIS = ( + "Gilt für das Programmfenster. Das Menü bleibt in der Farbe von " + "Windows – ein natives Menü lässt sich dort nicht einfärben.") def _fenstersymbol(self): """Das Programmsymbol auf das Fenster legen.""" - return wasserzeichen.symbol_setzen(self, melden=log.info, - dunkel=getattr(self, "_dark_mode", False)) + return wasserzeichen.symbol_setzen(self, melden=log.info) def _wasserzeichen_setzen(self, groesse=220): """Das Programmlogo blass in die untere rechte Ecke.""" diff --git a/einzelinstanz.py b/einzelinstanz.py index c26fcd1..1e66418 100644 --- a/einzelinstanz.py +++ b/einzelinstanz.py @@ -61,6 +61,10 @@ def eigene_pid_merken(): with open(pid_datei(), "w", encoding="ascii") as f: f.write(str(os.getpid())) except OSError: + # Der Merkzettel ist eine Bequemlichkeit, keine Bedingung: ohne ihn + # findet eine zweite Instanz das laufende Fenster nicht und meldet + # nur, dass das Programm schon laeuft. Daran darf der Start nicht + # scheitern - der Ordner kann schreibgeschuetzt sein. pass def laufende_pid_lesen(): """PID der laufenden Instanz aus dem Merkzettel; None wenn unlesbar.""" diff --git a/menueleiste.py b/menueleiste.py index 6eb4ce6..7b0f578 100644 --- a/menueleiste.py +++ b/menueleiste.py @@ -32,29 +32,32 @@ Beschriftung ergibt eine Trennlinie. Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie hinueber. """ +import theme import tkinter as tk from tkinter import ttk class Menueleiste(tk.Frame): - def __init__(self, master, menues, farben=None, mit_trennlinie=True): + def __init__(self, master, menues, farben=None): self.f = farben if farben is not None else master super().__init__(master, bg=self.f.col_bg) self.menues = menues self._offen = None + self._anker = None # welcher Titel gerade offen ist + self._titel = {} # Label -> Eintraege, fuer den Wechsel im Klick self.pack(side="top", fill="x") for titel, eintraege in menues.items(): lbl = tk.Label(self, text=titel, bg=self.f.col_bg, fg=self.f.col_fg, padx=8, pady=3) lbl.pack(side="left") + self._titel[lbl] = eintraege lbl.bind("", lambda e, w=lbl: w.configure(bg=self.f.col_sel)) lbl.bind("", lambda e, w=lbl: w.configure(bg=self.f.col_bg)) lbl.bind("", lambda e, b=lbl, it=eintraege: self.aufklappen(b, it)) - if mit_trennlinie: - ttk.Separator(master, orient="horizontal").pack(side="top", fill="x") + ttk.Separator(master, orient="horizontal").pack(side="top", fill="x") # ---------------------------------------------------------------- Klappen def aufklappen(self, anker, eintraege): @@ -83,9 +86,15 @@ class Menueleiste(tk.Frame): eintrag.bind("", lambda e, c=befehl: self._ausfuehren(c)) pop.update_idletasks() - pop.geometry(f"+{anker.winfo_rootx()}" - f"+{anker.winfo_rooty() + anker.winfo_height()}") + # Auf den sichtbaren Bereich begrenzen, wie jede andere Position im + # Programm auch. Steht das Fenster am unteren oder rechten Rand, lief + # die Liste sonst hinaus und war nicht mehr zu treffen. + x, y = theme.auf_bildschirm( + anker.winfo_rootx(), anker.winfo_rooty() + anker.winfo_height(), + pop.winfo_reqwidth(), pop.winfo_reqheight()) + pop.geometry(f"+{x}+{y}") self._offen = pop + self._anker = anker pop.grab_set() pop.bind("", self._klick_ausserhalb, add="+") pop.bind("", lambda e: self.zuklappen()) @@ -94,14 +103,53 @@ class Menueleiste(tk.Frame): faerben(pop) def _klick_ausserhalb(self, ereignis): + """Klick neben die Liste: schliessen - und auf einem anderen Titel + gleich dessen Liste aufklappen. + + WARUM DAS HIER STEHT UND NICHT IM LABEL (07.09.2026) + ---------------------------------------------------- + Solange eine Liste offen ist, haelt ihr `grab_set` alle Mausklicks der + Anwendung fest. Die ``-Bindung des Menuetitels feuert dann + gar nicht - der Klick landet hier. Vorher endete er damit beim blossen + Zuklappen, und man musste ein zweites Mal klicken, um von "Datei" zu + "Hilfe" zu kommen. Das native tk.Menu, das diese Leiste ersetzt hat, + wechselte schon beim blossen Ueberfahren; zwei Klicks waren also eine + Verschlechterung gegenueber dem, was vorher da war. + + Ein Klick auf DENSELBEN Titel schliesst weiterhin nur - sonst koennte + man eine Liste nie ueber ihren Titel wieder loswerden. + """ pop = self._offen if not pop: return - if (ereignis.x_root < pop.winfo_rootx() - or ereignis.x_root > pop.winfo_rootx() + pop.winfo_width() - or ereignis.y_root < pop.winfo_rooty() - or ereignis.y_root > pop.winfo_rooty() + pop.winfo_height()): - self.zuklappen() + if (pop.winfo_rootx() <= ereignis.x_root + <= pop.winfo_rootx() + pop.winfo_width() + and pop.winfo_rooty() <= ereignis.y_root + <= pop.winfo_rooty() + pop.winfo_height()): + return # der Klick lag in der Liste + vorher = self._anker + self.zuklappen() + getroffen = self._titel_bei(ereignis.x_root, ereignis.y_root) + if getroffen is not None and getroffen is not vorher: + self.aufklappen(getroffen, self._titel[getroffen]) + + def _titel_bei(self, x_root, y_root): + """Welcher Menuetitel an dieser Bildschirmstelle liegt - oder None. + + Nachgerechnet statt gefragt: `winfo_containing()` waere kuerzer, gibt + aber nichts zurueck, sobald das Fenster einer ANDEREN Anwendung ueber + dem Punkt liegt. Beim Bedienen ist das nie der Fall - beim Pruefen + schon, und dort meldete es dann einen Fehler, den es nicht gibt + (07.09.2026, zwei Pruefstaende liefen gleichzeitig auf demselben + Bildschirmplatz). Wo die eigenen Titel liegen, weiss diese Leiste + selbst; dafuer muss sie das Fenstersystem nicht fragen. + """ + for lbl in self._titel: + x, y = lbl.winfo_rootx(), lbl.winfo_rooty() + if (x <= x_root <= x + lbl.winfo_width() + and y <= y_root <= y + lbl.winfo_height()): + return lbl + return None def _ausfuehren(self, befehl): # Erst zuklappen, dann ausfuehren: sonst haelt der grab_set der Liste @@ -118,3 +166,4 @@ class Menueleiste(tk.Frame): except tk.TclError: pass self._offen = None + self._anker = None diff --git a/ordnerwahl.py b/ordnerwahl.py index ddf6fe0..fa9b5a9 100644 --- a/ordnerwahl.py +++ b/ordnerwahl.py @@ -118,8 +118,6 @@ class OrdnerWahl(tk.Toplevel): # zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar. self.update_idletasks() hoehe = max(self.winfo_reqheight(), 300) - x = master.winfo_rootx() + max(0, (master.winfo_width() - BREITE) // 2) - y = master.winfo_rooty() + 90 self.geometry(f"{BREITE}x{hoehe}") theme.mittig(self, master) self.minsize(BREITE, hoehe) diff --git a/pruef_berichtigung.py b/pruef_berichtigung.py index e4a190f..13a59c0 100644 --- a/pruef_berichtigung.py +++ b/pruef_berichtigung.py @@ -24,16 +24,6 @@ HIER = r"C:\claude\beherbergungssteuer" TOOL = r"C:\claude\rechnungstool" sys.path.insert(0, HIER) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - # --- Reissleine: ein Pruefstand darf nie haengen --- import threading as _th @@ -59,6 +49,7 @@ shutil.copy(os.path.join(TOOL, "config.json"), os.path.join(tmp, "config.json")) tool_config.CONFIG_PATH = os.path.join(tmp, "config.json") import berechnung as tb # noqa: E402 import pdf_renderer # noqa: E402 +from pruefhelfer import fehler, pruefe cfg_tool = tool_config.load_config() diff --git a/pruef_darstellung.py b/pruef_darstellung.py index 742afb2..71ee44f 100644 --- a/pruef_darstellung.py +++ b/pruef_darstellung.py @@ -34,16 +34,6 @@ import tkinter as tk QUELLE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, QUELLE) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) - os.chdir(QUELLE) os.environ["CAMPINGHOF_TEST"] = "1" @@ -72,6 +62,7 @@ os.chdir(prog) import app as A # noqa: E402 import theme # noqa: E402 +from pruefhelfer import fehler, pruefe f = A.App() diff --git a/pruef_doppelte.py b/pruef_doppelte.py index f31ee8e..9486d39 100644 --- a/pruef_doppelte.py +++ b/pruef_doppelte.py @@ -24,16 +24,6 @@ from datetime import date HIER = r"C:\claude\beherbergungssteuer" sys.path.insert(0, HIER) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") @@ -74,6 +64,7 @@ messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: None from modell import Buchung, doppelte_nummern, summiere # noqa: E402 +from pruefhelfer import fehler, pruefe pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})") diff --git a/pruef_einzelinstanz.py b/pruef_einzelinstanz.py index ac7dd45..e3dba71 100644 --- a/pruef_einzelinstanz.py +++ b/pruef_einzelinstanz.py @@ -24,16 +24,6 @@ import time HIER = r"C:\claude\beherbergungssteuer" sys.path.insert(0, HIER) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - laeufer = [] @@ -54,6 +44,7 @@ _wecker.daemon = True _wecker.start() from einzelinstanz import pid_datei # noqa: E402 +from pruefhelfer import fehler, pruefe if os.path.exists(pid_datei()): print(f" (Merkzettel: {pid_datei()})") diff --git a/pruef_gemeinsam_automatisch.py b/pruef_gemeinsam_automatisch.py index 4c9b936..eab6c13 100644 --- a/pruef_gemeinsam_automatisch.py +++ b/pruef_gemeinsam_automatisch.py @@ -22,6 +22,12 @@ GEPRUEFT WIRD 2. Der Ordner laesst sich anlegen, und darin entsteht EIN Nummernbuch. 3. Was das eine eintraegt, findet das andere - ohne dass jemand einen Pfad eingegeben hat. + 4. Die Dateien, die von sich sagen, sie laegen in beiden Programmen gleich, + sind auch wirklich gleich. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber - und sie prueft sich unter Punkt 4 selbst mit, denn sie traegt +denselben Satz wie die anderen. Angefasst wird dabei nichts Echtes: der Standardordner wird ueber die Umgebungsvariable LOCALAPPDATA auf einen Temp-Ordner umgelenkt. @@ -31,16 +37,6 @@ import shutil import sys import tempfile -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) - RECHNUNG = r"C:\claude\rechnungstool" STEUER = r"C:\claude\beherbergungssteuer" @@ -49,6 +45,7 @@ STEUER = r"C:\claude\beherbergungssteuer" # Geladen wird jede Fassung EINZELN, damit wirklich die Datei des jeweiligen # Programms befragt wird und nicht die zufaellig zuerst importierte. import importlib.util # noqa: E402 +from pruefhelfer import fehler, pruefe def modul_aus(ordner, name): @@ -95,9 +92,54 @@ buch_s.eintragen("2026-778", jahr=2026, nachname="Gegenprobe", quelle="journal") zurueck = [n for n in buch_r.nummern(2026) if n == "2026-778"] pruefe(bool(zurueck), "und umgekehrt genauso") -pruefe(len(os.listdir(angelegt)) >= 1 - and any(n.endswith(".sqlite3") for n in os.listdir(angelegt)), - f"es liegt genau ein Buch dort ({os.listdir(angelegt)})") +# GENAU eins - nicht "mindestens eins". Vorher stand hier `>= 1` mit einem +# any(); im Text darueber stand "genau ein Buch". Eine Pruefung, die nicht rot +# werden kann, prueft nichts: laege dort ein zweites Buch, weil eine der +# Fassungen einen anderen Dateinamen bildet, waere sie gruen geblieben - und +# das ist genau der Fall, den dieser Pruefstand abfangen soll. +# +# Die Begleitdateien von SQLite (-wal, -shm) zaehlen nicht mit; sie gehoeren +# zum selben Buch. +buecher = [n for n in os.listdir(angelegt) if n.endswith(".sqlite3")] +pruefe(len(buecher) == 1, + f"es liegt genau EIN Buch dort ({os.listdir(angelegt)})") +pruefe(buecher == [g_rechnung.DATEINAME], + f"und es traegt den Namen, den beide erwarten ({buecher})") + +# ---- 4. Was "in beiden gleich" heisst, ist auch gleich ------------------ +# Neun Dateien tragen im Kopf den Satz, sie laegen in beiden Programmen +# gleich. Das ist eine Zusage an den Naechsten, der eine davon anfasst - und +# geprueft hat sie bis zum 07.09.2026 niemand. Bei hinweis.py stimmte sie da +# schon nicht mehr: gleicher Inhalt, andere Zeilenenden. +# +# Die Liste steht bewusst NICHT hier, sondern wird aus dem Satz selbst +# gelesen. Eine Liste, die jemand von Hand nachfuehren muss, veraltet still +# (und still veraltete Pruefstaende fuehren in die Irre). +MARKE = "Diese Datei liegt in BEIDEN Programmen gleich" + +gemeinsame = sorted(n for n in os.listdir(RECHNUNG) if n.endswith(".py") + and MARKE in open(os.path.join(RECHNUNG, n), + encoding="utf-8").read()) +pruefe(len(gemeinsame) >= 5, + f"{len(gemeinsame)} Dateien sagen von sich, sie seien in beiden gleich") + +for name in gemeinsame: + hier = os.path.join(RECHNUNG, name) + dort = os.path.join(STEUER, name) + if not os.path.isfile(dort): + pruefe(False, f"{name} fehlt im Steuerrechnungstool") + continue + roh_h, roh_d = open(hier, "rb").read(), open(dort, "rb").read() + if roh_h == roh_d: + pruefe(True, f"{name} ist byteweise gleich") + continue + # Nicht gleich - dann wenigstens sagen, WORAN es liegt. Das ist der + # Unterschied zwischen einer Meldung und einer Suche. + if roh_h.replace(b"\r\n", b"\n") == roh_d.replace(b"\r\n", b"\n"): + grund = "nur die Zeilenenden weichen ab" + else: + grund = "der Inhalt weicht ab" + pruefe(False, f"{name}: {grund}") shutil.rmtree(tmp, ignore_errors=True) diff --git a/pruef_installation.py b/pruef_installation.py index f789ebc..55d0d1c 100644 --- a/pruef_installation.py +++ b/pruef_installation.py @@ -54,22 +54,13 @@ HIER = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HIER) import version # noqa: E402 +from pruefhelfer import fehler, pruefe LOKAL = os.environ.get("LOCALAPPDATA", "") RAVOKK = os.path.join(LOKAL, "ravokk") NAME = os.path.splitext(version.EXE_NAME)[0] PROGRAMMORDNER = os.path.join(RAVOKK, NAME) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) - def setup_datei(): ordner = os.path.join(HIER, "release") diff --git a/pruef_menueleiste.py b/pruef_menueleiste.py new file mode 100644 index 0000000..7013965 --- /dev/null +++ b/pruef_menueleiste.py @@ -0,0 +1,184 @@ +# -*- coding: utf-8 -*- +"""Pruefstand: die eigene Menueleiste - Wechsel mit EINEM Klick, und am Rand. + +WARUM ES DIESE LEISTE GIBT +-------------------------- +`tk.Menu` ist unter Windows ein Menue des Betriebssystems. Es nimmt Farben +entgegen und zeichnet trotzdem im Systemgrau - ein weisser Balken ueber einem +dunklen Fenster. Sein Befund am 07.09.2026: *"nur die einstellunden hilfe +leiste die ist noch hell"*. Deshalb ist die Leiste aus gewoehnlichen Labels +gebaut. + +WAS HIER GEPRUEFT WIRD + +1. Ein Klick auf einen Titel klappt seine Liste auf, ein zweiter auf denselben + Titel klappt sie wieder zu. + +2. Steht eine Liste offen und man klickt auf einen ANDEREN Titel, klappt + dessen Liste auf - mit EINEM Klick. + + Das ging bis zum 07.09.2026 nicht. Solange eine Liste offen ist, haelt ihr + `grab_set` alle Mausklicks der Anwendung fest; die ``-Bindung des + Menuetitels feuert dann gar nicht. Der Klick landete beim Zuklappen und war + damit verbraucht - man musste ein zweites Mal klicken, um von "Datei" zu + "Hilfe" zu kommen. Das native tk.Menu wechselte schon beim blossen + Ueberfahren; zwei Klicks waren also eine Verschlechterung gegenueber dem, + was ersetzt wurde. + + Geprueft wird das ueber ein echtes Klickereignis auf der offenen Liste, mit + den Bildschirmkoordinaten des anderen Titels - also genau der Weg, den der + grab_set erzwingt. Ein direkter Aufruf von aufklappen() wuerde am Problem + vorbeipruefen. + +3. Die Liste bleibt auf dem Bildschirm. Steht das Fenster am unteren Rand, + lief sie vorher darueber hinaus und war nicht mehr zu treffen: die Position + wurde ungeprueft gesetzt, waehrend jede andere neue Position im Programm + ueber theme.auf_bildschirm() laeuft. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber - sonst steht in zwei Programmen zweierlei. +""" +import os +import sys +import time +import tkinter as tk + +QUELLE = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, QUELLE) + +import menueleiste # noqa: E402 +import theme # noqa: E402 +from pruefhelfer import fehler, pruefe # noqa: E402 + + +class Fenster(theme.ThemeMixin, tk.Tk): + """Ein nacktes Fenster mit Farben - mehr braucht die Leiste nicht.""" + + def __init__(self): + super().__init__() + self.cfg = {} # theme.modus() will etwas mit .get() + self._apply_theme() + self.title("Pruefstand Menueleiste") + self.geometry("560x320" + theme.pruefplatz()) + + +f = Fenster() +gerufen = [] +f.menue = menueleiste.Menueleiste(f, { + "Datei": [("Neu", lambda: gerufen.append("neu")), ("-", None), + ("Beenden", lambda: gerufen.append("beenden"))], + "Bearbeiten": [("Rueckgaengig", lambda: gerufen.append("rueck"))], + "Hilfe": [("Info", lambda: gerufen.append("info"))], +}) + + +def pumpen(sekunden=0.4): + ende = time.time() + sekunden + while time.time() < ende: + f.update() + time.sleep(0.02) + + +pumpen(1.0) + +titel = {str(w.cget("text")): w for w in f.menue.winfo_children() + if isinstance(w, tk.Label)} +pruefe(set(titel) == {"Datei", "Bearbeiten", "Hilfe"}, + f"alle drei Titel stehen in der Leiste ({sorted(titel)})") + + +def klick_auf_titel(name): + """Auf einen Menuetitel klicken - so, wie es die Maus tut. + + Steht schon eine Liste offen, MUSS das Ereignis an sie gehen: ihr grab_set + faengt jeden Klick der Anwendung ab. Genau das ist der Fall, um den es + unter Punkt 2 geht. + """ + w = titel[name] + x = w.winfo_rootx() + w.winfo_width() // 2 + y = w.winfo_rooty() + w.winfo_height() // 2 + ziel = f.menue._offen or w + if ziel is w: + # Achtung: Tk nennt die Bildschirmkoordinaten beim Erzeugen -rootx + # und -rooty; im Ereignis heissen sie dann x_root und y_root. + w.event_generate("", x=w.winfo_width() // 2, + y=w.winfo_height() // 2, rootx=x, rooty=y) + else: + ziel.event_generate("", x=-1, y=-1, rootx=x, rooty=y) + pumpen(0.4) + + +def offener_titel(): + """Welcher Titel gerade offen ist - oder None.""" + anker = f.menue._anker + if anker is None or not f.menue._offen: + return None + return next((t for t, w in titel.items() if w is anker), "?") + + +# ---- 1. Aufklappen und wieder zuklappen --------------------------------- +klick_auf_titel("Datei") +pruefe(offener_titel() == "Datei", f"'Datei' klappt auf ({offener_titel()})") + +klick_auf_titel("Datei") +pruefe(offener_titel() is None, + f"und derselbe Titel klappt wieder zu ({offener_titel()})") + +# ---- 2. Der Wechsel: EIN Klick ------------------------------------------ +klick_auf_titel("Datei") +pruefe(offener_titel() == "Datei", "'Datei' steht offen") + +klick_auf_titel("Hilfe") +pruefe(offener_titel() == "Hilfe", + f"ein Klick auf 'Hilfe' wechselt hinueber ({offener_titel()})") + +klick_auf_titel("Bearbeiten") +pruefe(offener_titel() == "Bearbeiten", + f"und weiter auf 'Bearbeiten' ({offener_titel()})") + +# Ein Klick irgendwo neben die Leiste schliesst - und oeffnet nichts. +pop = f.menue._offen +pop.event_generate("", x=-1, y=-1, + rootx=f.winfo_rootx() + 300, + rooty=f.winfo_rooty() + 250) +pumpen(0.4) +pruefe(offener_titel() is None, + f"ein Klick daneben schliesst und oeffnet nichts ({offener_titel()})") + +# ---- 3. Ein Eintrag fuehrt seinen Befehl aus ---------------------------- +klick_auf_titel("Hilfe") +eintrag = None +for kind in f.menue._offen.winfo_children(): + for enkel in kind.winfo_children(): + if isinstance(enkel, tk.Label) and str(enkel.cget("text")) == "Info": + eintrag = enkel +eintrag.event_generate("", x=2, y=2) +pumpen(0.4) +pruefe(gerufen == ["info"], f"der Eintrag 'Info' fuehrt seinen Befehl aus ({gerufen})") +pruefe(offener_titel() is None, "und die Liste ist danach zu") + +# ---- 4. Am unteren Bildschirmrand bleibt die Liste sichtbar ------------- +# Das Fenster so weit nach unten schieben, dass unter der Leiste kein Platz +# mehr fuer die Klappliste ist. +links, oben = theme.auf_bildschirm(f.winfo_rootx(), f.winfo_rooty()) +unten = oben + f.winfo_screenheight() +f.geometry(f"560x320+{links}+{unten - 120}") +pumpen(0.6) + +klick_auf_titel("Datei") +pop = f.menue._offen +pruefe(pop is not None, "die Liste geht auch am unteren Rand auf") +if pop is not None: + x, y = pop.winfo_rootx(), pop.winfo_rooty() + hoehe, breite = pop.winfo_height(), pop.winfo_width() + grenze_x, grenze_y = theme.auf_bildschirm(x, y, breite, hoehe) + pruefe((x, y) == (grenze_x, grenze_y), + f"und steht vollstaendig auf dem Bildschirm " + f"(Liste {x},{y} bis {x + breite},{y + hoehe})") + +f.destroy() + +if fehler: + print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") + sys.exit(1) +print("GRUEN | Die Menueleiste wechselt mit einem Klick und bleibt im Bild") diff --git a/pruef_release.py b/pruef_release.py index 6722570..e8e13a4 100644 --- a/pruef_release.py +++ b/pruef_release.py @@ -39,16 +39,7 @@ sys.path.insert(0, HIER) import updater # noqa: E402 import version # noqa: E402 - -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) +from pruefhelfer import fehler, pruefe try: diff --git a/pruef_start_exe.py b/pruef_start_exe.py index adccc6b..1dafd4f 100644 --- a/pruef_start_exe.py +++ b/pruef_start_exe.py @@ -36,16 +36,7 @@ HIER = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HIER) import version # noqa: E402 - -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) +from pruefhelfer import fehler, pruefe stamm, endung = os.path.splitext(version.EXE_NAME) diff --git a/pruef_storno_journal.py b/pruef_storno_journal.py index e0a58a8..4a01e90 100644 --- a/pruef_storno_journal.py +++ b/pruef_storno_journal.py @@ -24,16 +24,6 @@ TOOL = r"C:\claude\rechnungstool" JOURNAL = r"C:\claude\beherbergungssteuer" sys.path.insert(0, TOOL) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") @@ -62,6 +52,7 @@ os.environ["CAMPINGHOF_TEST"] = "1" import app as A # noqa: E402 import bestand as BE # noqa: E402 from tkinter import messagebox # noqa: E402 +from pruefhelfer import fehler, pruefe # Erprobungshinweis und Freigabe vor dem Beleg werden hier bejaht - geprueft # werden sie eigens in pruef_hinweis.py und pruef_freigabe.py des Rechnungstools. diff --git a/pruef_storno_verrechnet.py b/pruef_storno_verrechnet.py index 88394d6..8bde5d3 100644 --- a/pruef_storno_verrechnet.py +++ b/pruef_storno_verrechnet.py @@ -30,17 +30,6 @@ from datetime import date HIER = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HIER) -fehler = 0 - - -def pruefe(bedingung, text): - global fehler - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler += 1 - def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") @@ -84,6 +73,7 @@ messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: None from gui_monat import SPALTEN # noqa: E402 +from pruefhelfer import fehler, pruefe pruefe(app_mod.BASE_DIR == tmp, f"Pruefstand laeuft im Testordner ({app_mod.BASE_DIR})") @@ -216,5 +206,5 @@ fenster.destroy() _wecker.cancel() print("---") -print("alles gruen" if not fehler else f"{fehler} FEHLER") +print("alles gruen" if not fehler else f"{len(fehler)} FEHLER") sys.exit(1 if fehler else 0) diff --git a/pruef_temp_merkzettel.py b/pruef_temp_merkzettel.py index 4ded7fe..e12e6e5 100644 --- a/pruef_temp_merkzettel.py +++ b/pruef_temp_merkzettel.py @@ -38,16 +38,6 @@ import tempfile HIER = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HIER) -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kod = sys.stdout.encoding or "utf-8" - print(zeile.encode(kod, "replace").decode(kod)) - if not bedingung: - fehler.append(text) - echtes_lad = os.environ.get("LOCALAPPDATA", "") spielwiese = os.path.join(os.path.expanduser("~"), ".pruef_merkzettel") @@ -61,6 +51,7 @@ sys.frozen = True sys.argv = [os.path.join(spielwiese, "Steuerrechnungstool.exe")] import ablage # noqa: E402 +from pruefhelfer import fehler, pruefe importlib.reload(ablage) # ---- 1. Die Funktion erkennt einen Temp-Pfad --------------------------- diff --git a/pruef_wache.py b/pruef_wache.py index 33755fd..8558f12 100644 --- a/pruef_wache.py +++ b/pruef_wache.py @@ -25,16 +25,6 @@ import time JOURNAL = r"C:\claude\beherbergungssteuer" TOOL = r"C:\claude\rechnungstool" -fehler = [] - - -def pruefe(bedingung, text): - zeile = (" OK | " if bedingung else "FEHLER | ") + text - kodierung = sys.stdout.encoding or "utf-8" - print(zeile.encode(kodierung, "replace").decode(kodierung)) - if not bedingung: - fehler.append(text) - def _reissleine(): print("ABBRUCH | Pruefstand haengt (Zeitgrenze)") @@ -66,6 +56,7 @@ del sys.modules["app"] sys.path.remove(JOURNAL) from tkinter import messagebox # noqa: E402 +from pruefhelfer import fehler, pruefe messagebox.showinfo = lambda *a, **k: None messagebox.showwarning = lambda *a, **k: None diff --git a/pruefhelfer.py b/pruefhelfer.py new file mode 100644 index 0000000..f3e9155 --- /dev/null +++ b/pruefhelfer.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +"""Das Gemeinsame aller Pruefstaende: eine Zeile schreiben und sie merken. + +WARUM ES DIESE DATEI GIBT (07.09.2026) +-------------------------------------- +Die Funktion pruefe() stand 35-mal im Quelltext, in sechs leicht verschiedenen +Fassungen. Keine davon war falsch - aber sie sahen unterschiedlich aus, und +wer 37 Laeufe hintereinander durchsieht, liest dann dreierlei Formen fuer +dieselbe Aussage. Wird an der Ausgabe je etwas geaendert, sind es 35 Stellen. + +BENUTZUNG + + from pruefhelfer import fehler, pruefe + + pruefe(a == b, "beide nennen denselben Ordner") + ... + if fehler: + print(f"ROT | {len(fehler)} Punkt(e) nicht erfuellt") + sys.exit(1) + print("GRUEN | ...") + +`fehler` ist eine Liste und wird beim Importieren geteilt - jeder Pruefstand +laeuft in einem eigenen Prozess, sie gehoert also ihm allein. + +Den Abschluss schreibt jeder Pruefstand selbst. Er sieht nur aehnlich aus: +manche raeumen dort noch einen Temp-Ordner ab, manche zaehlen die +fehlgeschlagenen Punkte einzeln auf. Das ist Inhalt, keine Wiederholung. + +Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie +hinueber - sonst steht in zwei Programmen zweierlei. +""" +import sys + +# Alles, was nicht erfuellt war - der Text, nicht nur die Anzahl. Wer am Ende +# nur eine Zahl hat, muss den Lauf noch einmal lesen, um zu wissen, was fehlte. +fehler = [] + + +def pruefe(bedingung, text): + """Eine Zusage pruefen, das Ergebnis melden und Fehlschlaege merken.""" + zeile = (" OK | " if bedingung else "FEHLER | ") + text + # Die Windows-Konsole ist cp1252 - ein Sonderzeichen im Text darf den Lauf + # nicht mit einem UnicodeEncodeError abbrechen. + kodierung = sys.stdout.encoding or "utf-8" + print(zeile.encode(kodierung, "replace").decode(kodierung)) + if not bedingung: + fehler.append(text) diff --git a/theme.py b/theme.py index 309f261..213e2d3 100644 --- a/theme.py +++ b/theme.py @@ -46,7 +46,6 @@ def titelleiste_faerben(fenster, dunkel): if not sys.platform.startswith("win"): return False try: - import ctypes fenster.update_idletasks() hwnd = ctypes.windll.user32.GetParent(fenster.winfo_id()) or int(fenster.frame(), 16) wert = ctypes.c_int(1 if dunkel else 0) @@ -84,16 +83,13 @@ def pruefplatz(): if not sys.platform.startswith("win"): return "" try: - import ctypes - from ctypes import wintypes - class RECT(ctypes.Structure): _fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long), ("right", ctypes.c_long), ("bottom", ctypes.c_long)] class MONITORINFO(ctypes.Structure): - _fields_ = [("cbSize", wintypes.DWORD), ("rcMonitor", RECT), - ("rcWork", RECT), ("dwFlags", wintypes.DWORD)] + _fields_ = [("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", RECT), + ("rcWork", RECT), ("dwFlags", ctypes.wintypes.DWORD)] gefunden = [] @@ -106,9 +102,9 @@ def pruefplatz(): r.left, r.top, r.right - r.left)) return 1 - RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HMONITOR, - wintypes.HDC, ctypes.POINTER(RECT), - wintypes.LPARAM) + RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HMONITOR, + ctypes.wintypes.HDC, ctypes.POINTER(RECT), + ctypes.wintypes.LPARAM) ctypes.windll.user32.EnumDisplayMonitors(0, 0, RUECKRUF(je_monitor), 0) # Der breiteste Nebenmonitor - bei mehreren der linkeste. neben = sorted((m for m in gefunden if not m[0]), @@ -137,7 +133,6 @@ def auf_bildschirm(x, y, breite=0, hoehe=0): if not sys.platform.startswith("win"): return int(x), int(y) try: - import ctypes hole = ctypes.windll.user32.GetSystemMetrics links, oben = hole(76), hole(77) rechts, unten = links + hole(78), oben + hole(79) @@ -207,7 +202,6 @@ def schrift_vorbereiten(): _schrift_geladen = True if not sys.platform.startswith("win"): return SCHRIFT_NAME - import ctypes FR_PRIVATE = 0x10 geladen = 0 for basis in _ordner(): @@ -615,6 +609,120 @@ class ThemeMixin: """ titelleiste_faerben(win, bool(getattr(self, "_dark_mode", False))) + # ------------------------------------------------ Hell / dunkel umstellen + # Der Hinweistext gehoert dem einzelnen Programm - er sagt etwas ueber + # DIESES Programm. Hier steht nur, was dasteht, wenn ihn niemand setzt. + DARSTELLUNG_HINWEIS = "Gilt nur für das Programmfenster." + + def _cfg_speichern(self): + """Die Einstellungen wegschreiben - falls das noetig ist. + + Im Rechnungstool ist die Konfiguration ein gewoehnliches Woerterbuch + und muss ausdruecklich gespeichert werden. Im Steuerrechnungstool ist + sie eine Ablage, die jeden Schreibzugriff sofort in die + Journaltabelle durchreicht - dort ist hier nichts zu tun. + """ + return + + def variablen_sichern(self): + """Alle Tk-Variablen des Fensters als {Name: Wert}. + + WARUM ALLE UND NICHT EINE AUSWAHL + --------------------------------- + Die _build-Methoden legen ihre Variablen selbst an + (`self.k_name = tk.StringVar()`). Nach dem Neubau stehen also neue, + leere da. Beim ersten Versuch waren Kundenname und Rechnungsnummer + weg, waehrend die Mengen blieben - die hatte ich einzeln gesichert. + + Durchgegangen wird deshalb, was das Objekt an Tk-Variablen traegt. + Wer spaeter ein Feld hinzufuegt, muss nichts eintragen; eine Liste zum + Pflegen waere genau die Sorte, die man beim naechsten Feld vergisst. + """ + gesichert = {} + for name in dir(self): + if name.startswith("__"): + continue + try: + wert = getattr(self, name) + except Exception: # noqa: BLE001 - Eigenschaften koennen werfen + continue + if isinstance(wert, (tk.StringVar, tk.IntVar, tk.DoubleVar, + tk.BooleanVar)): + try: + gesichert[name] = wert.get() + except tk.TclError: + pass + return gesichert + + def variablen_zuruecksetzen(self, gesichert): + """Die gesicherten Werte in die neu gebauten Variablen zurueckgeben.""" + for name, wert in gesichert.items(): + neu = getattr(self, name, None) + if isinstance(neu, (tk.StringVar, tk.IntVar, tk.DoubleVar, + tk.BooleanVar)): + try: + neu.set(wert) + except tk.TclError: + pass + + def kinder_loeschen(self): + """Die Oberflaeche abraeumen - aber keine fremden Fenster. + + Ein tk.Toplevel IST ein Kind des Hauptfensters. Der offene + Darstellungsdialog wurde hier bisher mitgeloescht, und die Zeile + danach fasste eine Leiche an ("invalid command name .!toplevel"). + Gefunden am 07.09.2026 von der Pruefkette, mit dem echten Bedienweg + gemessen: Klick auf "Dunkel" -> Farbe wechselt, Fenster ist weg, + Traceback im Log. + + Der Pruefstand hatte es nicht gefunden, weil er cfg["darstellung"] von + Hand setzte und _oberflaeche_neu() direkt rief - also alles ausser dem + Weg, den ein Mensch geht. Er klickt jetzt. + """ + for kind in self.winfo_children(): + if not isinstance(kind, tk.Toplevel): + kind.destroy() + + def dlg_darstellung(self): + """Hell, dunkel oder wie Windows - und sofort sichtbar.""" + d = tk.Toplevel(self) + d.title("Darstellung") + d.transient(self) + d.resizable(False, False) + self._style_dialog(d) + rahmen = ttk.Frame(d, padding=18) + rahmen.pack(fill="both", expand=True) + ttk.Label(rahmen, text="Darstellung", + font=(SCHRIFT_NAME, 11, "bold"), + foreground=self.col_accent).pack(anchor="w") + ttk.Label(rahmen, justify="left", wraplength=340, + foreground=self.col_muted, + text=self.DARSTELLUNG_HINWEIS).pack(anchor="w", pady=(6, 12)) + + gewaehlt = tk.StringVar(value=modus(self.cfg)) + + def uebernehmen(): + neu = gewaehlt.get() + if neu == modus(self.cfg): + return + self.cfg["darstellung"] = neu + self._cfg_speichern() + self._oberflaeche_neu() + # Der Dialog gehoert noch zur alten Darstellung - er wird + # mitgefaerbt, sonst steht ein helles Fenster vor einem dunklen. + self._style_dialog(d) + + for wert in MODI: + ttk.Radiobutton(rahmen, text=MODUS_NAMEN[wert], value=wert, + variable=gewaehlt, command=uebernehmen).pack( + anchor="w", pady=3) + + ttk.Separator(rahmen, orient="horizontal").pack(fill="x", pady=(12, 10)) + ttk.Button(rahmen, text="Schließen", width=14, + command=d.destroy).pack(anchor="e") + mittig(d, self) + return d + class MeldungMixin: """Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms. diff --git a/updater.py b/updater.py index 5db2447..836c448 100644 --- a/updater.py +++ b/updater.py @@ -245,6 +245,11 @@ def aufraeumen(ordner, exe_name, behalten=2): os.remove(pfad) weg.append(os.path.basename(pfad)) except OSError: + # Laeuft die alte Fassung noch oder haelt ein Virenscanner die + # Datei, sperrt Windows sie. Das ist kein Grund zum Abbruch: die + # Datei stoert nur, sie schadet nicht, und beim naechsten Mal + # klappt es. Was WEG ist, steht in der Rueckgabe - was nicht weg + # ist, taucht dort nicht auf. pass return weg diff --git a/wasserzeichen.py b/wasserzeichen.py index 7cf0b51..9a73658 100644 --- a/wasserzeichen.py +++ b/wasserzeichen.py @@ -45,7 +45,7 @@ def pfad(name): return "" -def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22): +def kachel(bild, kante, rand=0.16): """Das Logo auf eine helle Kachel mit runden Ecken setzen. Seine Ansage am 07.09.2026: *"bei den windows icons kannst du einen @@ -57,9 +57,9 @@ def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22): das Programm nicht. Eine helle Kachel ist auf jedem sichtbar; so machen es die meisten Programme, und deshalb sieht es auch nicht fremd aus. - `rand` ist der Abstand zum Kachelrand, `radius` die Eckenrundung - beides - als Anteil der Kantenlaenge, damit es in jeder Groesse gleich aussieht. - Die 22 % entsprechen etwa dem, was Windows fuer Kacheln verwendet. + `rand` ist der Abstand zum Kachelrand, als Anteil der Kantenlaenge - so + sieht es in jeder Groesse gleich aus. Die Rundung liegt fest bei 22 %, + etwa dem, was Windows fuer Kacheln verwendet. """ from PIL import Image, ImageDraw kante = int(kante) @@ -68,8 +68,8 @@ def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22): fein = kante * 4 maske = Image.new("L", (fein, fein), 0) ImageDraw.Draw(maske).rounded_rectangle( - (0, 0, fein - 1, fein - 1), radius=int(fein * radius), fill=255) - flaeche = Image.new("RGBA", (fein, fein), tuple(grund) + (255,)) + (0, 0, fein - 1, fein - 1), radius=int(fein * 0.22), fill=255) + flaeche = Image.new("RGBA", (fein, fein), (0xFF, 0xFF, 0xFF, 255)) flaeche.putalpha(maske) innen = max(1, int(fein * (1 - 2 * rand))) @@ -126,17 +126,16 @@ def symbol_bauen(ziel): return "" -def symbol_setzen(fenster, melden=print, dunkel=False): +def symbol_setzen(fenster, melden=print): """Das Programmsymbol auf ein Fenster legen. True bei Erfolg. Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes hat. - `dunkel` wird nicht mehr gebraucht und nur noch entgegengenommen, damit - bestehende Aufrufe weiterlaufen: seit das Symbol eine helle Kachel ist, ist - es in jeder Darstellung sichtbar. Vorher gab es dafuer eine zweite, - aufgehellte Datei - die ist mit der Kachel ueberfluessig geworden. + Frueher gab es dafuer eine zweite, aufgehellte ICO und einen Schalter + `dunkel`. Seit das Symbol eine helle Kachel ist, ist es in jeder + Darstellung sichtbar - beides ist damit weggefallen. """ p = pfad("programm.ico") if not p: