Aufraeumen nach der Pruefkette: doppelter Code an eine Stelle
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_bilder.py schrieb ein eigenes Format, pruef_storno_verrechnet.py zaehlte in einer Zahl und konnte am Ende nicht sagen, WAS fehlschlug. * 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. * Sechs stumme "except OSError" sagen jetzt, warum sie schweigen duerfen, und einer meldet statt zu schweigen: schlaegt die Uebernahme einer alten config.json fehl, faengt das Programm ohne Firmendaten, Katalog und Zaehler neu an - und der Erfolgsfall schrieb eine Zeile, der Fehlerfall nicht. Die 24 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
This commit is contained in:
parent
5babfc3e2e
commit
2b09d15cd0
36 changed files with 563 additions and 364 deletions
23
.gitattributes
vendored
Normal file
23
.gitattributes
vendored
Normal file
|
|
@ -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
|
||||||
4
api.py
4
api.py
|
|
@ -138,6 +138,10 @@ class _Handler(socketserver.StreamRequestHandler):
|
||||||
try:
|
try:
|
||||||
self.wfile.write((json.dumps({"antwort": antwort}) + "\n").encode("utf-8"))
|
self.wfile.write((json.dumps({"antwort": antwort}) + "\n").encode("utf-8"))
|
||||||
except OSError:
|
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
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
109
app.py
109
app.py
|
|
@ -476,34 +476,15 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
WAS DABEI ERHALTEN BLEIBT
|
WAS DABEI ERHALTEN BLEIBT
|
||||||
-------------------------
|
-------------------------
|
||||||
Alles Eingetippte. Ein Themenwechsel darf keine halb getippte Rechnung
|
Alles Eingetippte. Ein Themenwechsel darf keine halb getippte Rechnung
|
||||||
wegwerfen.
|
wegwerfen. Um die Tk-Variablen kuemmert sich variablen_sichern() im
|
||||||
|
ThemeMixin - dort steht auch, warum ALLE gesichert werden und nicht
|
||||||
|
eine gepflegte Auswahl.
|
||||||
|
|
||||||
Dass die Werte an StringVars haengen, reicht dafuer NICHT: die
|
Hier stehen nur die Dinge, die es sonst nirgends gibt: die Mengen und
|
||||||
_build_-Methoden legen ihre Vars selbst an (`self.k_name =
|
Personenzahlen der Leistungszeilen (sie haengen an Woerterbuechern,
|
||||||
tk.StringVar()`), beim Neubau stehen also neue, leere Vars da. Beim
|
nicht am Fenster) und das Blatt, das gerade vorn war.
|
||||||
ersten Versuch waren Kundenname und Rechnungsnummer weg, waehrend die
|
|
||||||
Mengen blieben - die hatte ich einzeln gesichert.
|
|
||||||
|
|
||||||
Deshalb werden hier ALLE Variablen des Fensters gesichert, nicht eine
|
|
||||||
Auswahl: durchgegangen wird, was das Objekt an Tk-Variablen traegt.
|
|
||||||
Wer spaeter ein Feld hinzufuegt, muss nichts eintragen - es ist
|
|
||||||
automatisch dabei. Eine Liste zum Pflegen waere genau die Sorte, die
|
|
||||||
man beim naechsten Feld vergisst.
|
|
||||||
"""
|
"""
|
||||||
gesichert = {}
|
gesichert = self.variablen_sichern()
|
||||||
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
|
|
||||||
mengen = {nr: v.get() for nr, v in self.anzahl_vars.items()}
|
mengen = {nr: v.get() for nr, v in self.anzahl_vars.items()}
|
||||||
personen = {nr: v.get() for nr, v in self.personen_vars.items()}
|
personen = {nr: v.get() for nr, v in self.personen_vars.items()}
|
||||||
blatt = None
|
blatt = None
|
||||||
|
|
@ -512,19 +493,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
except Exception: # noqa: BLE001 - dann eben das erste Blatt
|
except Exception: # noqa: BLE001 - dann eben das erste Blatt
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Fremde Fenster stehen lassen! Ein tk.Toplevel IST ein Kind des
|
self.kinder_loeschen()
|
||||||
# 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()
|
|
||||||
self.flaeche = None
|
self.flaeche = None
|
||||||
self._text_zellen = []
|
self._text_zellen = []
|
||||||
|
|
||||||
|
|
@ -536,14 +505,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
self._build_positionen()
|
self._build_positionen()
|
||||||
self.flaeche.anordnen()
|
self.flaeche.anordnen()
|
||||||
|
|
||||||
for name, wert in gesichert.items():
|
self.variablen_zuruecksetzen(gesichert)
|
||||||
neu = getattr(self, name, None)
|
|
||||||
if isinstance(neu, (tk.StringVar, tk.IntVar, tk.DoubleVar,
|
|
||||||
tk.BooleanVar)):
|
|
||||||
try:
|
|
||||||
neu.set(wert)
|
|
||||||
except tk.TclError:
|
|
||||||
pass
|
|
||||||
for nr, wert in mengen.items():
|
for nr, wert in mengen.items():
|
||||||
if nr in self.anzahl_vars:
|
if nr in self.anzahl_vars:
|
||||||
self.anzahl_vars[nr].set(wert)
|
self.anzahl_vars[nr].set(wert)
|
||||||
|
|
@ -558,46 +520,17 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
self._buch_anzeigen()
|
self._buch_anzeigen()
|
||||||
self.recalc()
|
self.recalc()
|
||||||
|
|
||||||
def dlg_darstellung(self):
|
# Der Dialog selbst steht im ThemeMixin (theme.py) - er sah in beiden
|
||||||
"""Hell, dunkel oder wie Windows - und sofort sichtbar."""
|
# Programmen gleich aus. Verschieden ist nur der Hinweis darin und die
|
||||||
d = tk.Toplevel(self)
|
# Frage, ob die Einstellungen ausdruecklich gespeichert werden muessen.
|
||||||
d.title("Darstellung")
|
DARSTELLUNG_HINWEIS = (
|
||||||
d.transient(self)
|
"Gilt nur für das Programmfenster. Die Farbe der Rechnung steht unter "
|
||||||
d.resizable(False, False)
|
"Einstellungen → Firmendaten – das Blatt gehört dem Kunden, nicht dem "
|
||||||
self._style_dialog(d)
|
"Programm.")
|
||||||
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 nur für das Programmfenster. Die Farbe der Rechnung "
|
|
||||||
"steht unter Einstellungen → Firmendaten – das Blatt "
|
|
||||||
"gehört dem Kunden, nicht dem Programm.").pack(
|
|
||||||
anchor="w", pady=(6, 12))
|
|
||||||
|
|
||||||
gewaehlt = tk.StringVar(value=theme.modus(self.cfg))
|
def _cfg_speichern(self):
|
||||||
|
"""Hier ist die Konfiguration eine config.json und will geschrieben sein."""
|
||||||
def uebernehmen():
|
config.save_config(self.cfg)
|
||||||
neu = gewaehlt.get()
|
|
||||||
if neu == theme.modus(self.cfg):
|
|
||||||
return
|
|
||||||
self.cfg["darstellung"] = neu
|
|
||||||
config.save_config(self.cfg)
|
|
||||||
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 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
|
|
||||||
|
|
||||||
def _umbruch_nachziehen(self, runde=0, vorige_breite=None):
|
def _umbruch_nachziehen(self, runde=0, vorige_breite=None):
|
||||||
"""Den Textumbruch der Leistungsspalte auf ihre echte Breite setzen.
|
"""Den Textumbruch der Leistungsspalte auf ihre echte Breite setzen.
|
||||||
|
|
@ -856,7 +789,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
|
|
||||||
def _fenstersymbol(self):
|
def _fenstersymbol(self):
|
||||||
"""Das Programmsymbol auf das Fenster legen."""
|
"""Das Programmsymbol auf das Fenster legen."""
|
||||||
return wasserzeichen.symbol_setzen(self, dunkel=getattr(self, "_dark_mode", False))
|
return wasserzeichen.symbol_setzen(self)
|
||||||
|
|
||||||
def _hinweis_wenn_sichtbar(self, versuche=0):
|
def _hinweis_wenn_sichtbar(self, versuche=0):
|
||||||
"""Wartet, bis das Fenster gezeichnet ist, und fragt dann.
|
"""Wartet, bis das Fenster gezeichnet ist, und fragt dann.
|
||||||
|
|
@ -1108,7 +1041,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
||||||
d.bind("<Escape>", lambda _e: d.destroy())
|
d.bind("<Escape>", lambda _e: d.destroy())
|
||||||
d.update_idletasks()
|
d.update_idletasks()
|
||||||
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
|
|
||||||
theme.mittig(d, self)
|
theme.mittig(d, self)
|
||||||
d.grab_set()
|
d.grab_set()
|
||||||
self.wait_window(d)
|
self.wait_window(d)
|
||||||
|
|
@ -1458,7 +1390,6 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin,
|
||||||
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
||||||
d.bind("<Escape>", lambda _e: d.destroy())
|
d.bind("<Escape>", lambda _e: d.destroy())
|
||||||
d.update_idletasks()
|
d.update_idletasks()
|
||||||
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
|
|
||||||
theme.mittig(d, self)
|
theme.mittig(d, self)
|
||||||
d.grab_set()
|
d.grab_set()
|
||||||
self.wait_window(d)
|
self.wait_window(d)
|
||||||
|
|
|
||||||
14
config.py
14
config.py
|
|
@ -58,6 +58,7 @@ def _daten_ordner():
|
||||||
"""
|
"""
|
||||||
if not getattr(sys, "frozen", False):
|
if not getattr(sys, "frozen", False):
|
||||||
return EXE_DIR # aus der Quelle heraus bleibt alles, wo es ist
|
return EXE_DIR # aus der Quelle heraus bleibt alles, wo es ist
|
||||||
|
import gemeinsam
|
||||||
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
heim = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
|
||||||
# NEBEN dem Programm, nicht darin: das Programm liegt in
|
# NEBEN dem Programm, nicht darin: das Programm liegt in
|
||||||
# ravokk/Rechnungstool, die Daten eine Ebene darueber in ravokk. Genau so
|
# ravokk/Rechnungstool, die Daten eine Ebene darueber in ravokk. Genau so
|
||||||
|
|
@ -65,7 +66,8 @@ def _daten_ordner():
|
||||||
# es den ordner rechnungstool und steuertool". Nur dadurch kann ein
|
# es den ordner rechnungstool und steuertool". Nur dadurch kann ein
|
||||||
# Deinstallieren den Programmordner restlos leeren, ohne die Belege
|
# Deinstallieren den Programmordner restlos leeren, ohne die Belege
|
||||||
# anzufassen (§ 147 AO).
|
# anzufassen (§ 147 AO).
|
||||||
kandidat = os.path.join(heim, "ravokk")
|
# Gerechnet wird der Pfad nur an EINER Stelle - siehe dort.
|
||||||
|
kandidat = gemeinsam.standard_ordner()
|
||||||
if not _beschreibbar(kandidat):
|
if not _beschreibbar(kandidat):
|
||||||
kandidat = os.path.join(EXE_DIR, "Daten")
|
kandidat = os.path.join(EXE_DIR, "Daten")
|
||||||
if not _beschreibbar(kandidat):
|
if not _beschreibbar(kandidat):
|
||||||
|
|
@ -84,8 +86,14 @@ def _daten_ordner():
|
||||||
os.replace(alt, neu_)
|
os.replace(alt, neu_)
|
||||||
print(f"[config] {name} aus {alt} übernommen")
|
print(f"[config] {name} aus {alt} übernommen")
|
||||||
break
|
break
|
||||||
except OSError:
|
except OSError as e:
|
||||||
pass
|
# Nicht abbrechen - der naechste Ort wird noch probiert,
|
||||||
|
# und zur Not faengt das Programm eben neu an. Aber sagen,
|
||||||
|
# dass es schiefging: der Erfolgsfall schreibt eine Zeile,
|
||||||
|
# und wer sie sucht und nicht findet, wuesste sonst nicht,
|
||||||
|
# ob nichts da war oder ob die Uebernahme misslang. Es geht
|
||||||
|
# um Firmendaten, Leistungskatalog und Rechnungszaehler.
|
||||||
|
print(f"[config] {name} aus {alt} NICHT übernommen: {e}")
|
||||||
return kandidat
|
return kandidat
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,10 @@ def eigene_pid_merken():
|
||||||
with open(PID_DATEI, "w", encoding="ascii") as f:
|
with open(PID_DATEI, "w", encoding="ascii") as f:
|
||||||
f.write(str(os.getpid()))
|
f.write(str(os.getpid()))
|
||||||
except OSError:
|
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
|
pass
|
||||||
def laufende_pid_lesen():
|
def laufende_pid_lesen():
|
||||||
"""PID der laufenden Instanz aus dem Merkzettel; None wenn unlesbar."""
|
"""PID der laufenden Instanz aus dem Merkzettel; None wenn unlesbar."""
|
||||||
|
|
|
||||||
|
|
@ -512,7 +512,6 @@ class KorrekturMixin:
|
||||||
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
d.protocol("WM_DELETE_WINDOW", d.destroy)
|
||||||
d.bind("<Escape>", lambda _e: d.destroy())
|
d.bind("<Escape>", lambda _e: d.destroy())
|
||||||
d.update_idletasks()
|
d.update_idletasks()
|
||||||
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
|
|
||||||
theme.mittig(d, self)
|
theme.mittig(d, self)
|
||||||
d.grab_set()
|
d.grab_set()
|
||||||
self.wait_window(d)
|
self.wait_window(d)
|
||||||
|
|
|
||||||
|
|
@ -32,29 +32,32 @@ Beschriftung ergibt eine Trennlinie.
|
||||||
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
|
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
|
||||||
hinueber.
|
hinueber.
|
||||||
"""
|
"""
|
||||||
|
import theme
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import ttk
|
from tkinter import ttk
|
||||||
|
|
||||||
|
|
||||||
class Menueleiste(tk.Frame):
|
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
|
self.f = farben if farben is not None else master
|
||||||
super().__init__(master, bg=self.f.col_bg)
|
super().__init__(master, bg=self.f.col_bg)
|
||||||
self.menues = menues
|
self.menues = menues
|
||||||
self._offen = None
|
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")
|
self.pack(side="top", fill="x")
|
||||||
|
|
||||||
for titel, eintraege in menues.items():
|
for titel, eintraege in menues.items():
|
||||||
lbl = tk.Label(self, text=titel, bg=self.f.col_bg, fg=self.f.col_fg,
|
lbl = tk.Label(self, text=titel, bg=self.f.col_bg, fg=self.f.col_fg,
|
||||||
padx=8, pady=3)
|
padx=8, pady=3)
|
||||||
lbl.pack(side="left")
|
lbl.pack(side="left")
|
||||||
|
self._titel[lbl] = eintraege
|
||||||
lbl.bind("<Enter>", lambda e, w=lbl: w.configure(bg=self.f.col_sel))
|
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("<Leave>", lambda e, w=lbl: w.configure(bg=self.f.col_bg))
|
||||||
lbl.bind("<Button-1>",
|
lbl.bind("<Button-1>",
|
||||||
lambda e, b=lbl, it=eintraege: self.aufklappen(b, it))
|
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
|
# ---------------------------------------------------------------- Klappen
|
||||||
def aufklappen(self, anker, eintraege):
|
def aufklappen(self, anker, eintraege):
|
||||||
|
|
@ -83,9 +86,15 @@ class Menueleiste(tk.Frame):
|
||||||
eintrag.bind("<Button-1>", lambda e, c=befehl: self._ausfuehren(c))
|
eintrag.bind("<Button-1>", lambda e, c=befehl: self._ausfuehren(c))
|
||||||
|
|
||||||
pop.update_idletasks()
|
pop.update_idletasks()
|
||||||
pop.geometry(f"+{anker.winfo_rootx()}"
|
# Auf den sichtbaren Bereich begrenzen, wie jede andere Position im
|
||||||
f"+{anker.winfo_rooty() + anker.winfo_height()}")
|
# 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._offen = pop
|
||||||
|
self._anker = anker
|
||||||
pop.grab_set()
|
pop.grab_set()
|
||||||
pop.bind("<Button-1>", self._klick_ausserhalb, add="+")
|
pop.bind("<Button-1>", self._klick_ausserhalb, add="+")
|
||||||
pop.bind("<Escape>", lambda e: self.zuklappen())
|
pop.bind("<Escape>", lambda e: self.zuklappen())
|
||||||
|
|
@ -94,14 +103,53 @@ class Menueleiste(tk.Frame):
|
||||||
faerben(pop)
|
faerben(pop)
|
||||||
|
|
||||||
def _klick_ausserhalb(self, ereignis):
|
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
|
pop = self._offen
|
||||||
if not pop:
|
if not pop:
|
||||||
return
|
return
|
||||||
if (ereignis.x_root < pop.winfo_rootx()
|
if (pop.winfo_rootx() <= ereignis.x_root
|
||||||
or ereignis.x_root > pop.winfo_rootx() + pop.winfo_width()
|
<= pop.winfo_rootx() + pop.winfo_width()
|
||||||
or ereignis.y_root < pop.winfo_rooty()
|
and pop.winfo_rooty() <= ereignis.y_root
|
||||||
or ereignis.y_root > pop.winfo_rooty() + pop.winfo_height()):
|
<= pop.winfo_rooty() + pop.winfo_height()):
|
||||||
self.zuklappen()
|
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):
|
def _ausfuehren(self, befehl):
|
||||||
# Erst zuklappen, dann ausfuehren: sonst haelt der grab_set der Liste
|
# Erst zuklappen, dann ausfuehren: sonst haelt der grab_set der Liste
|
||||||
|
|
@ -118,3 +166,4 @@ class Menueleiste(tk.Frame):
|
||||||
except tk.TclError:
|
except tk.TclError:
|
||||||
pass
|
pass
|
||||||
self._offen = None
|
self._offen = None
|
||||||
|
self._anker = None
|
||||||
|
|
|
||||||
|
|
@ -118,8 +118,6 @@ class OrdnerWahl(tk.Toplevel):
|
||||||
# zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar.
|
# zweiten Spalte ab und der Auswahl-Knopf ist nicht erreichbar.
|
||||||
self.update_idletasks()
|
self.update_idletasks()
|
||||||
hoehe = max(self.winfo_reqheight(), 300)
|
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}")
|
self.geometry(f"{BREITE}x{hoehe}")
|
||||||
theme.mittig(self, master)
|
theme.mittig(self, master)
|
||||||
self.minsize(BREITE, hoehe)
|
self.minsize(BREITE, hoehe)
|
||||||
|
|
|
||||||
|
|
@ -32,16 +32,6 @@ QUELLE = r"C:\claude\rechnungstool"
|
||||||
JOURNAL = r"C:\claude\beherbergungssteuer"
|
JOURNAL = r"C:\claude\beherbergungssteuer"
|
||||||
sys.path.insert(0, QUELLE)
|
sys.path.insert(0, QUELLE)
|
||||||
|
|
||||||
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():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
||||||
|
|
@ -73,6 +63,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import bestand as BE # noqa: E402
|
import bestand as BE # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
antworten = {"Fertig": False, "Hausnummer": True}
|
antworten = {"Fertig": False, "Hausnummer": True}
|
||||||
|
|
||||||
|
|
|
||||||
11
pruef_api.py
11
pruef_api.py
|
|
@ -52,6 +52,7 @@ import api # noqa: E402
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import gemeinsam # noqa: E402
|
import gemeinsam # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -63,16 +64,6 @@ _wecker = threading.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
|
||||||
k = sys.stdout.encoding or "utf-8"
|
|
||||||
print(zeile.encode(k, "replace").decode(k))
|
|
||||||
if not bedingung:
|
|
||||||
fehler.append(text)
|
|
||||||
|
|
||||||
|
|
||||||
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
||||||
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
||||||
|
|
|
||||||
|
|
@ -30,14 +30,7 @@ shutil.copy(os.path.join(QUELLE, "config.json"), config.CONFIG_PATH)
|
||||||
import bilder # noqa: E402
|
import bilder # noqa: E402
|
||||||
import pdf_renderer # noqa: E402
|
import pdf_renderer # noqa: E402
|
||||||
from berechnung import berechne # noqa: E402
|
from berechnung import berechne # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
print(("OK | " if bedingung else "FEHL | ") + text)
|
|
||||||
if not bedingung:
|
|
||||||
fehler.append(text)
|
|
||||||
|
|
||||||
|
|
||||||
def testbild(pfad, farbe):
|
def testbild(pfad, farbe):
|
||||||
|
|
|
||||||
|
|
@ -35,16 +35,6 @@ import tkinter as tk
|
||||||
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, QUELLE)
|
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.chdir(QUELLE)
|
||||||
os.environ["CAMPINGHOF_TEST"] = "1"
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
|
|
@ -69,6 +59,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"),
|
||||||
|
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import theme # noqa: E402
|
import theme # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
f = A.RechnungsApp()
|
f = A.RechnungsApp()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,16 +26,6 @@ QUELLE = r"C:\claude\rechnungstool"
|
||||||
JOURNAL = r"C:\claude\beherbergungssteuer"
|
JOURNAL = r"C:\claude\beherbergungssteuer"
|
||||||
sys.path.insert(0, QUELLE)
|
sys.path.insert(0, QUELLE)
|
||||||
|
|
||||||
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():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
||||||
|
|
@ -64,6 +54,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import bestand as BE # noqa: E402
|
import bestand as BE # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
antworten = {"Fertig": False, "Hausnummer": True, "Storno erstellt": False,
|
antworten = {"Fertig": False, "Hausnummer": True, "Storno erstellt": False,
|
||||||
"Angaben zum Kunden": True, "Weicht ab": True}
|
"Angaben zum Kunden": True, "Weicht ab": True}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import flaeche as FL # noqa: E402
|
import flaeche as FL # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -55,16 +56,6 @@ _wecker = threading.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
|
||||||
k = sys.stdout.encoding or "utf-8"
|
|
||||||
print(zeile.encode(k, "replace").decode(k))
|
|
||||||
if not bedingung:
|
|
||||||
fehler.append(text)
|
|
||||||
|
|
||||||
|
|
||||||
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
||||||
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
||||||
|
|
|
||||||
|
|
@ -28,16 +28,6 @@ import threading
|
||||||
QUELLE = r"C:\claude\rechnungstool"
|
QUELLE = r"C:\claude\rechnungstool"
|
||||||
sys.path.insert(0, QUELLE)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
||||||
|
|
@ -70,6 +60,7 @@ os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import gemeinsam # noqa: E402
|
import gemeinsam # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
antworten = {"Fertig": False, "Hausnummer": True}
|
antworten = {"Fertig": False, "Hausnummer": True}
|
||||||
gefragt = []
|
gefragt = []
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,12 @@ GEPRUEFT WIRD
|
||||||
2. Der Ordner laesst sich anlegen, und darin entsteht EIN Nummernbuch.
|
2. Der Ordner laesst sich anlegen, und darin entsteht EIN Nummernbuch.
|
||||||
3. Was das eine eintraegt, findet das andere - ohne dass jemand einen Pfad
|
3. Was das eine eintraegt, findet das andere - ohne dass jemand einen Pfad
|
||||||
eingegeben hat.
|
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
|
Angefasst wird dabei nichts Echtes: der Standardordner wird ueber die
|
||||||
Umgebungsvariable LOCALAPPDATA auf einen Temp-Ordner umgelenkt.
|
Umgebungsvariable LOCALAPPDATA auf einen Temp-Ordner umgelenkt.
|
||||||
|
|
@ -31,16 +37,6 @@ import shutil
|
||||||
import sys
|
import sys
|
||||||
import tempfile
|
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"
|
RECHNUNG = r"C:\claude\rechnungstool"
|
||||||
STEUER = r"C:\claude\beherbergungssteuer"
|
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
|
# Geladen wird jede Fassung EINZELN, damit wirklich die Datei des jeweiligen
|
||||||
# Programms befragt wird und nicht die zufaellig zuerst importierte.
|
# Programms befragt wird und nicht die zufaellig zuerst importierte.
|
||||||
import importlib.util # noqa: E402
|
import importlib.util # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def modul_aus(ordner, name):
|
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"]
|
zurueck = [n for n in buch_r.nummern(2026) if n == "2026-778"]
|
||||||
pruefe(bool(zurueck), "und umgekehrt genauso")
|
pruefe(bool(zurueck), "und umgekehrt genauso")
|
||||||
|
|
||||||
pruefe(len(os.listdir(angelegt)) >= 1
|
# GENAU eins - nicht "mindestens eins". Vorher stand hier `>= 1` mit einem
|
||||||
and any(n.endswith(".sqlite3") for n in os.listdir(angelegt)),
|
# any(); im Text darueber stand "genau ein Buch". Eine Pruefung, die nicht rot
|
||||||
f"es liegt genau ein Buch dort ({os.listdir(angelegt)})")
|
# 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)
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,6 @@ import threading
|
||||||
QUELLE = r"C:\claude\rechnungstool"
|
QUELLE = r"C:\claude\rechnungstool"
|
||||||
sys.path.insert(0, QUELLE)
|
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)
|
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
||||||
|
|
@ -71,6 +61,7 @@ os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import hinweis as H # noqa: E402
|
import hinweis as H # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
antworten = {"Fertig": False, "Hausnummer": True}
|
antworten = {"Fertig": False, "Hausnummer": True}
|
||||||
gesehen = []
|
gesehen = []
|
||||||
|
|
|
||||||
|
|
@ -54,22 +54,13 @@ HIER = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, HIER)
|
sys.path.insert(0, HIER)
|
||||||
|
|
||||||
import version # noqa: E402
|
import version # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
LOKAL = os.environ.get("LOCALAPPDATA", "")
|
LOKAL = os.environ.get("LOCALAPPDATA", "")
|
||||||
RAVOKK = os.path.join(LOKAL, "ravokk")
|
RAVOKK = os.path.join(LOKAL, "ravokk")
|
||||||
NAME = os.path.splitext(version.EXE_NAME)[0]
|
NAME = os.path.splitext(version.EXE_NAME)[0]
|
||||||
PROGRAMMORDNER = os.path.join(RAVOKK, NAME)
|
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():
|
def setup_datei():
|
||||||
ordner = os.path.join(HIER, "release")
|
ordner = os.path.join(HIER, "release")
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ from tkinter import messagebox # noqa: E402
|
||||||
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
||||||
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
||||||
import threading as _th
|
import threading as _th
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -66,18 +67,6 @@ _wecker = _th.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
# Die Windows-Konsole ist cp1252 - ein Sonderzeichen im Text darf den Lauf
|
|
||||||
# nicht mit einem UnicodeEncodeError abbrechen.
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# ---- Dialoge: jede Frage muss angemeldet sein --------------------------------
|
# ---- Dialoge: jede Frage muss angemeldet sein --------------------------------
|
||||||
antworten = {}
|
antworten = {}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ from tkinter import messagebox # noqa: E402
|
||||||
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
||||||
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
||||||
import threading as _th
|
import threading as _th
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -65,18 +66,6 @@ _wecker = _th.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
# Die Windows-Konsole ist cp1252 - ein Sonderzeichen im Text darf den Lauf
|
|
||||||
# nicht mit einem UnicodeEncodeError abbrechen.
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
antworten = {"Fertig": False, "Storno wirklich": True,
|
antworten = {"Fertig": False, "Storno wirklich": True,
|
||||||
"endgültig erstellen": True,
|
"endgültig erstellen": True,
|
||||||
|
|
|
||||||
184
pruef_menueleiste.py
Normal file
184
pruef_menueleiste.py
Normal file
|
|
@ -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 `<Button-1>`-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("<Button-1>", x=w.winfo_width() // 2,
|
||||||
|
y=w.winfo_height() // 2, rootx=x, rooty=y)
|
||||||
|
else:
|
||||||
|
ziel.event_generate("<Button-1>", 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("<Button-1>", 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("<Button-1>", 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")
|
||||||
|
|
@ -49,6 +49,7 @@ os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import bestand as BE # noqa: E402
|
import bestand as BE # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -61,16 +62,6 @@ _wecker = threading.Timer(150, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Dialoge automatisch beantworten; jede Frage wird protokolliert.
|
# Dialoge automatisch beantworten; jede Frage wird protokolliert.
|
||||||
antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True}
|
antworten = {"Fertig": False, "endgültig erstellen": True, "Hausnummer": True}
|
||||||
|
|
|
||||||
|
|
@ -23,16 +23,6 @@ import threading
|
||||||
TOOL = r"C:\claude\rechnungstool"
|
TOOL = r"C:\claude\rechnungstool"
|
||||||
JOURNAL = r"C:\claude\beherbergungssteuer"
|
JOURNAL = r"C:\claude\beherbergungssteuer"
|
||||||
|
|
||||||
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():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
||||||
|
|
@ -66,6 +56,7 @@ sys.path.remove(JOURNAL)
|
||||||
|
|
||||||
from datetime import date # noqa: E402
|
from datetime import date # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
messagebox.showinfo = lambda *a, **k: None
|
messagebox.showinfo = lambda *a, **k: None
|
||||||
messagebox.showwarning = lambda *a, **k: None
|
messagebox.showwarning = lambda *a, **k: None
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,6 @@ import threading
|
||||||
QUELLE = r"C:\claude\rechnungstool"
|
QUELLE = r"C:\claude\rechnungstool"
|
||||||
sys.path.insert(0, QUELLE)
|
sys.path.insert(0, QUELLE)
|
||||||
|
|
||||||
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():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze)")
|
||||||
|
|
@ -65,6 +55,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import gemeinsam # noqa: E402
|
import gemeinsam # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
warnungen = []
|
warnungen = []
|
||||||
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
||||||
|
|
|
||||||
|
|
@ -31,16 +31,6 @@ QUELLE = r"C:\claude\rechnungstool"
|
||||||
JOURNAL = r"C:\claude\beherbergungssteuer"
|
JOURNAL = r"C:\claude\beherbergungssteuer"
|
||||||
sys.path.insert(0, QUELLE)
|
sys.path.insert(0, QUELLE)
|
||||||
|
|
||||||
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():
|
def _reissleine():
|
||||||
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
print("ABBRUCH | Pruefstand haengt (Zeitgrenze) - vermutlich ein Dialog")
|
||||||
|
|
@ -69,6 +59,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
import bestand as BE # noqa: E402
|
import bestand as BE # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
||||||
# Geprueft wird er eigens in pruef_hinweis.py.
|
# Geprueft wird er eigens in pruef_hinweis.py.
|
||||||
|
|
|
||||||
|
|
@ -39,16 +39,7 @@ sys.path.insert(0, HIER)
|
||||||
|
|
||||||
import updater # noqa: E402
|
import updater # noqa: E402
|
||||||
import version # noqa: E402
|
import version # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,10 @@ import ast
|
||||||
import io
|
import io
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
ORDNER = [r"C:\claude\rechnungstool", r"C:\claude\beherbergungssteuer"]
|
ORDNER = [r"C:\claude\rechnungstool", r"C:\claude\beherbergungssteuer"]
|
||||||
|
|
||||||
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 fundstellen(quelltext):
|
def fundstellen(quelltext):
|
||||||
"""Alle Zeilen, in denen `self` steht, ohne dass es eines gibt."""
|
"""Alle Zeilen, in denen `self` steht, ohne dass es eines gibt."""
|
||||||
|
|
|
||||||
|
|
@ -41,16 +41,6 @@ import time
|
||||||
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
QUELLE = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, QUELLE)
|
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.chdir(QUELLE)
|
||||||
os.environ["CAMPINGHOF_TEST"] = "1"
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
|
|
@ -72,6 +62,7 @@ for _n in ("showinfo", "showwarning", "showerror"):
|
||||||
setattr(messagebox, _n, lambda *a, **k: None)
|
setattr(messagebox, _n, lambda *a, **k: None)
|
||||||
|
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
f = A.RechnungsApp()
|
f = A.RechnungsApp()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,16 +36,7 @@ HIER = os.path.dirname(os.path.abspath(__file__))
|
||||||
sys.path.insert(0, HIER)
|
sys.path.insert(0, HIER)
|
||||||
|
|
||||||
import version # noqa: E402
|
import version # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
stamm, endung = os.path.splitext(version.EXE_NAME)
|
stamm, endung = os.path.splitext(version.EXE_NAME)
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,7 @@ from tkinter import messagebox # noqa: E402
|
||||||
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
# eine Aenderung eine neue Frage eingefuehrt hat), bricht er nach zwei Minuten
|
||||||
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
# von selbst ab, statt ein Fenster offen zu lassen, auf das niemand klickt.
|
||||||
import threading as _th
|
import threading as _th
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -61,18 +62,6 @@ _wecker = _th.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
# Die Windows-Konsole ist cp1252 - ein Sonderzeichen im Text darf den Lauf
|
|
||||||
# nicht mit einem UnicodeEncodeError abbrechen.
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
# Dialoge automatisch beantworten; jede Frage wird protokolliert.
|
# Dialoge automatisch beantworten; jede Frage wird protokolliert.
|
||||||
antworten = {}
|
antworten = {}
|
||||||
|
|
|
||||||
|
|
@ -25,16 +25,7 @@ QUELLE = r"C:\claude\rechnungstool"
|
||||||
sys.path.insert(0, QUELLE)
|
sys.path.insert(0, QUELLE)
|
||||||
|
|
||||||
import updater as U # noqa: E402
|
import updater as U # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
EXE = "Rechnungstool.exe"
|
EXE = "Rechnungstool.exe"
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ json.dump(roh, open(config.CONFIG_PATH, "w", encoding="utf-8"), ensure_ascii=Fal
|
||||||
os.environ["CAMPINGHOF_TEST"] = "1"
|
os.environ["CAMPINGHOF_TEST"] = "1"
|
||||||
import app as A # noqa: E402
|
import app as A # noqa: E402
|
||||||
from tkinter import messagebox # noqa: E402
|
from tkinter import messagebox # noqa: E402
|
||||||
|
from pruefhelfer import fehler, pruefe
|
||||||
|
|
||||||
|
|
||||||
def _reissleine():
|
def _reissleine():
|
||||||
|
|
@ -51,16 +52,6 @@ _wecker = _th.Timer(120, _reissleine)
|
||||||
_wecker.daemon = True
|
_wecker.daemon = True
|
||||||
_wecker.start()
|
_wecker.start()
|
||||||
|
|
||||||
fehler = []
|
|
||||||
|
|
||||||
|
|
||||||
def pruefe(bedingung, text):
|
|
||||||
zeile = (" OK | " if bedingung else "FEHLER | ") + text
|
|
||||||
k = sys.stdout.encoding or "utf-8"
|
|
||||||
print(zeile.encode(k, "replace").decode(k))
|
|
||||||
if not bedingung:
|
|
||||||
fehler.append(text)
|
|
||||||
|
|
||||||
|
|
||||||
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
messagebox.showinfo = messagebox.showwarning = messagebox.showerror = lambda *a, **k: None
|
||||||
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
# Der Erprobungshinweis wird von der pauschalen Antwort unten mit erfasst.
|
||||||
|
|
|
||||||
47
pruefhelfer.py
Normal file
47
pruefhelfer.py
Normal file
|
|
@ -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)
|
||||||
130
theme.py
130
theme.py
|
|
@ -46,7 +46,6 @@ def titelleiste_faerben(fenster, dunkel):
|
||||||
if not sys.platform.startswith("win"):
|
if not sys.platform.startswith("win"):
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
import ctypes
|
|
||||||
fenster.update_idletasks()
|
fenster.update_idletasks()
|
||||||
hwnd = ctypes.windll.user32.GetParent(fenster.winfo_id()) or int(fenster.frame(), 16)
|
hwnd = ctypes.windll.user32.GetParent(fenster.winfo_id()) or int(fenster.frame(), 16)
|
||||||
wert = ctypes.c_int(1 if dunkel else 0)
|
wert = ctypes.c_int(1 if dunkel else 0)
|
||||||
|
|
@ -84,16 +83,13 @@ def pruefplatz():
|
||||||
if not sys.platform.startswith("win"):
|
if not sys.platform.startswith("win"):
|
||||||
return ""
|
return ""
|
||||||
try:
|
try:
|
||||||
import ctypes
|
|
||||||
from ctypes import wintypes
|
|
||||||
|
|
||||||
class RECT(ctypes.Structure):
|
class RECT(ctypes.Structure):
|
||||||
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
|
_fields_ = [("left", ctypes.c_long), ("top", ctypes.c_long),
|
||||||
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
|
("right", ctypes.c_long), ("bottom", ctypes.c_long)]
|
||||||
|
|
||||||
class MONITORINFO(ctypes.Structure):
|
class MONITORINFO(ctypes.Structure):
|
||||||
_fields_ = [("cbSize", wintypes.DWORD), ("rcMonitor", RECT),
|
_fields_ = [("cbSize", ctypes.wintypes.DWORD), ("rcMonitor", RECT),
|
||||||
("rcWork", RECT), ("dwFlags", wintypes.DWORD)]
|
("rcWork", RECT), ("dwFlags", ctypes.wintypes.DWORD)]
|
||||||
|
|
||||||
gefunden = []
|
gefunden = []
|
||||||
|
|
||||||
|
|
@ -106,9 +102,9 @@ def pruefplatz():
|
||||||
r.left, r.top, r.right - r.left))
|
r.left, r.top, r.right - r.left))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, wintypes.HMONITOR,
|
RUECKRUF = ctypes.WINFUNCTYPE(ctypes.c_int, ctypes.wintypes.HMONITOR,
|
||||||
wintypes.HDC, ctypes.POINTER(RECT),
|
ctypes.wintypes.HDC, ctypes.POINTER(RECT),
|
||||||
wintypes.LPARAM)
|
ctypes.wintypes.LPARAM)
|
||||||
ctypes.windll.user32.EnumDisplayMonitors(0, 0, RUECKRUF(je_monitor), 0)
|
ctypes.windll.user32.EnumDisplayMonitors(0, 0, RUECKRUF(je_monitor), 0)
|
||||||
# Der breiteste Nebenmonitor - bei mehreren der linkeste.
|
# Der breiteste Nebenmonitor - bei mehreren der linkeste.
|
||||||
neben = sorted((m for m in gefunden if not m[0]),
|
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"):
|
if not sys.platform.startswith("win"):
|
||||||
return int(x), int(y)
|
return int(x), int(y)
|
||||||
try:
|
try:
|
||||||
import ctypes
|
|
||||||
hole = ctypes.windll.user32.GetSystemMetrics
|
hole = ctypes.windll.user32.GetSystemMetrics
|
||||||
links, oben = hole(76), hole(77)
|
links, oben = hole(76), hole(77)
|
||||||
rechts, unten = links + hole(78), oben + hole(79)
|
rechts, unten = links + hole(78), oben + hole(79)
|
||||||
|
|
@ -207,7 +202,6 @@ def schrift_vorbereiten():
|
||||||
_schrift_geladen = True
|
_schrift_geladen = True
|
||||||
if not sys.platform.startswith("win"):
|
if not sys.platform.startswith("win"):
|
||||||
return SCHRIFT_NAME
|
return SCHRIFT_NAME
|
||||||
import ctypes
|
|
||||||
FR_PRIVATE = 0x10
|
FR_PRIVATE = 0x10
|
||||||
geladen = 0
|
geladen = 0
|
||||||
for basis in _ordner():
|
for basis in _ordner():
|
||||||
|
|
@ -615,6 +609,120 @@ class ThemeMixin:
|
||||||
"""
|
"""
|
||||||
titelleiste_faerben(win, bool(getattr(self, "_dark_mode", False)))
|
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:
|
class MeldungMixin:
|
||||||
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.
|
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.
|
||||||
|
|
|
||||||
|
|
@ -245,6 +245,11 @@ def aufraeumen(ordner, exe_name, behalten=2):
|
||||||
os.remove(pfad)
|
os.remove(pfad)
|
||||||
weg.append(os.path.basename(pfad))
|
weg.append(os.path.basename(pfad))
|
||||||
except OSError:
|
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
|
pass
|
||||||
return weg
|
return weg
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ def pfad(name):
|
||||||
return ""
|
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.
|
"""Das Logo auf eine helle Kachel mit runden Ecken setzen.
|
||||||
|
|
||||||
Seine Ansage am 07.09.2026: *"bei den windows icons kannst du einen
|
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
|
das Programm nicht. Eine helle Kachel ist auf jedem sichtbar; so machen es
|
||||||
die meisten Programme, und deshalb sieht es auch nicht fremd aus.
|
die meisten Programme, und deshalb sieht es auch nicht fremd aus.
|
||||||
|
|
||||||
`rand` ist der Abstand zum Kachelrand, `radius` die Eckenrundung - beides
|
`rand` ist der Abstand zum Kachelrand, als Anteil der Kantenlaenge - so
|
||||||
als Anteil der Kantenlaenge, damit es in jeder Groesse gleich aussieht.
|
sieht es in jeder Groesse gleich aus. Die Rundung liegt fest bei 22 %,
|
||||||
Die 22 % entsprechen etwa dem, was Windows fuer Kacheln verwendet.
|
etwa dem, was Windows fuer Kacheln verwendet.
|
||||||
"""
|
"""
|
||||||
from PIL import Image, ImageDraw
|
from PIL import Image, ImageDraw
|
||||||
kante = int(kante)
|
kante = int(kante)
|
||||||
|
|
@ -68,8 +68,8 @@ def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22):
|
||||||
fein = kante * 4
|
fein = kante * 4
|
||||||
maske = Image.new("L", (fein, fein), 0)
|
maske = Image.new("L", (fein, fein), 0)
|
||||||
ImageDraw.Draw(maske).rounded_rectangle(
|
ImageDraw.Draw(maske).rounded_rectangle(
|
||||||
(0, 0, fein - 1, fein - 1), radius=int(fein * radius), fill=255)
|
(0, 0, fein - 1, fein - 1), radius=int(fein * 0.22), fill=255)
|
||||||
flaeche = Image.new("RGBA", (fein, fein), tuple(grund) + (255,))
|
flaeche = Image.new("RGBA", (fein, fein), (0xFF, 0xFF, 0xFF, 255))
|
||||||
flaeche.putalpha(maske)
|
flaeche.putalpha(maske)
|
||||||
|
|
||||||
innen = max(1, int(fein * (1 - 2 * rand)))
|
innen = max(1, int(fein * (1 - 2 * rand)))
|
||||||
|
|
@ -126,17 +126,16 @@ def symbol_bauen(ziel):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def symbol_setzen(fenster, melden=print, dunkel=False):
|
def symbol_setzen(fenster, melden=print):
|
||||||
"""Das Programmsymbol auf ein Fenster legen. True bei Erfolg.
|
"""Das Programmsymbol auf ein Fenster legen. True bei Erfolg.
|
||||||
|
|
||||||
Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das
|
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
|
steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes
|
||||||
hat.
|
hat.
|
||||||
|
|
||||||
`dunkel` wird nicht mehr gebraucht und nur noch entgegengenommen, damit
|
Frueher gab es dafuer eine zweite, aufgehellte ICO und einen Schalter
|
||||||
bestehende Aufrufe weiterlaufen: seit das Symbol eine helle Kachel ist, ist
|
`dunkel`. Seit das Symbol eine helle Kachel ist, ist es in jeder
|
||||||
es in jeder Darstellung sichtbar. Vorher gab es dafuer eine zweite,
|
Darstellung sichtbar - beides ist damit weggefallen.
|
||||||
aufgehellte Datei - die ist mit der Kachel ueberfluessig geworden.
|
|
||||||
"""
|
"""
|
||||||
p = pfad("programm.ico")
|
p = pfad("programm.ico")
|
||||||
if not p:
|
if not p:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue