Die Runde vom 07.09.2026 hat neben den zwei echten Fehlern eine Reihe Wiederholungen und stumme Stellen gemeldet. Abgearbeitet in Etappen, jede fuer sich pruefbar - nach jeder liefen beide Pruefketten gruen. * Menueleiste: der Wechsel von einem Titel zum anderen brauchte ZWEI Klicks. Der grab_set der offenen Liste schluckt den ersten, die <Button-1>-Bindung des Titels feuert gar nicht. 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. Die Klappliste wird ausserdem jetzt wie jede andere Position ueber theme.auf_bildschirm() begrenzt; am unteren Fensterrand lief sie vorher hinaus. Neu: pruef_menueleiste.py misst beides ueber echte Klickereignisse. * %LOCALAPPDATA%/ravokk wurde an drei Stellen einzeln ausgerechnet. Jetzt fragen alle gemeinsam.standard_ordner(). Laufen die je auseinander, fuehren die beiden Programme zwei Nummernbuecher - und das faellt erst auf, wenn eine Rechnungsnummer zum zweiten Mal vergeben ist (§ 14 Abs. 4 Nr. 4 UStG). * pruefe() stand 35-mal im Quelltext, in sechs Fassungen -> pruefhelfer.py. Die zwei Ausreisser sind mitgezogen: pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug, pruef_bilder.py im Rechnungstool schrieb ein eigenes Format. * dlg_darstellung() und die drei Handgriffe des Umschaltens standen in beiden app.py fast wortgleich -> ThemeMixin in theme.py. Beide trugen inzwischen denselben langen Kommentar zu demselben Fehler; das war der Beweis, dass es eine Sache ist. Programmspezifisch bleiben der Hinweistext im Dialog (R31) und die Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen. * pruef_gemeinsam_automatisch.py sagte im Text "genau ein Buch" und prueft ">= 1" mit einem any() - eine Pruefung, die nicht rot werden kann. Jetzt == 1, und der Dateiname wird mitgeprueft. Dazu neu: die neun Dateien, die im Kopf zusagen, sie laegen in beiden Programmen gleich, werden byteweise verglichen. Bei hinweis.py stimmte die Zusage seit dem 06.09. nicht mehr - gleicher Inhalt, CRLF gegen LF. * .gitattributes, damit die Zeilenenden nicht von der Maschine abhaengen. Ohne das meldet genau diese Pruefung nach einem frischen Checkout einen Unterschied, den es im Repository gar nicht gibt. * Die stummen "except OSError" in api.py, updater.py und einzelinstanz.py sagen jetzt, warum sie schweigen duerfen. Die stummen tk.TclError bleiben: dort wird ein Widget angefasst, das gerade zerstoert wurde, und eine Meldung waere Rauschen. * Tote Parameter und Variablen aus dem Umbau des Vortags: symbol_setzen(dunkel=), kachel(grund=, radius=), mit_trennlinie, sechs x/y-Berechnungen, die von theme.mittig() sofort ueberschrieben wurden, vier lokale "import ctypes". 39 Pruefstaende, 0 rot. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
169 lines
7.2 KiB
Python
169 lines
7.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Eine Menueleiste, die sich einfaerben laesst.
|
|
|
|
WARUM NICHT DAS MENUE VON TK
|
|
----------------------------
|
|
`tk.Menu` ist unter Windows kein Tk-Widget, sondern ein Menue des
|
|
Betriebssystems. Es nimmt Farben zwar entgegen, zeichnet aber trotzdem im
|
|
Systemgrau - ein weisser Balken ueber einem dunklen Fenster. Sein Befund am
|
|
07.09.2026 zum Steuerrechnungstool: *"nur die einstellunden hilfe leiste die
|
|
ist noch hell"*.
|
|
|
|
Diese Leiste ist deshalb aus gewoehnlichen Labels gebaut: eine Zeile mit den
|
|
Titeln, und beim Klick geht ein randloses Toplevel als Klappliste auf. Beides
|
|
traegt die Farben des Programms.
|
|
|
|
WAS SIE KANN UND WAS NICHT
|
|
--------------------------
|
|
Sie kann, was diese beiden Programme brauchen: Titel, Eintraege, Trennlinien.
|
|
Sie kann bewusst NICHT: Untermenues, Tastenkuerzel neben dem Eintrag,
|
|
Ankreuzfelder. Wer das braucht, baut es hier ein - nicht daneben.
|
|
|
|
BENUTZUNG
|
|
|
|
self.menue = menueleiste.Menueleiste(self, farben=self, menues={
|
|
"Datei": [("Beenden", self._beenden)],
|
|
"Hilfe": [("Info", self._info)],
|
|
})
|
|
|
|
`farben` ist das Fenster mit den col_*-Feldern (das ThemeMixin). Ein "-" als
|
|
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):
|
|
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("<Enter>", lambda e, w=lbl: w.configure(bg=self.f.col_sel))
|
|
lbl.bind("<Leave>", lambda e, w=lbl: w.configure(bg=self.f.col_bg))
|
|
lbl.bind("<Button-1>",
|
|
lambda e, b=lbl, it=eintraege: self.aufklappen(b, it))
|
|
|
|
ttk.Separator(master, orient="horizontal").pack(side="top", fill="x")
|
|
|
|
# ---------------------------------------------------------------- Klappen
|
|
def aufklappen(self, anker, eintraege):
|
|
if self._offen: # zweiter Klick schliesst wieder
|
|
self.zuklappen()
|
|
return
|
|
pop = tk.Toplevel(self)
|
|
pop.overrideredirect(True)
|
|
# Der Rahmen ist der Hintergrund des Toplevels, einen Pixel breit -
|
|
# sonst zeichnet Windows einen weissen Rand um die Liste.
|
|
pop.configure(bg=self.f.col_line)
|
|
innen = tk.Frame(pop, bg=self.f.col_field)
|
|
innen.pack(padx=1, pady=1)
|
|
|
|
for beschriftung, befehl in eintraege:
|
|
if beschriftung == "-":
|
|
tk.Frame(innen, bg=self.f.col_line, height=1).pack(fill="x", pady=3)
|
|
continue
|
|
eintrag = tk.Label(innen, text=beschriftung, bg=self.f.col_field,
|
|
fg=self.f.col_fg, anchor="w", padx=16, pady=5)
|
|
eintrag.pack(fill="x")
|
|
eintrag.bind("<Enter>",
|
|
lambda e, w=eintrag: w.configure(bg=self.f.col_sel))
|
|
eintrag.bind("<Leave>",
|
|
lambda e, w=eintrag: w.configure(bg=self.f.col_field))
|
|
eintrag.bind("<Button-1>", lambda e, c=befehl: self._ausfuehren(c))
|
|
|
|
pop.update_idletasks()
|
|
# 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("<Button-1>", self._klick_ausserhalb, add="+")
|
|
pop.bind("<Escape>", lambda e: self.zuklappen())
|
|
faerben = getattr(self.f, "_dark_titlebar", None)
|
|
if faerben:
|
|
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 `<Button-1>`-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 (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
|
|
# einen Dialog fest, der aus dem Eintrag heraus aufgeht.
|
|
self.zuklappen()
|
|
if befehl:
|
|
befehl()
|
|
|
|
def zuklappen(self):
|
|
if self._offen:
|
|
try:
|
|
self._offen.grab_release()
|
|
self._offen.destroy()
|
|
except tk.TclError:
|
|
pass
|
|
self._offen = None
|
|
self._anker = None
|