Eigene Meldungsfenster, aktiver Reiter hervorgehoben, Pruefschalter als Funktion

Sein Fund: "der ohne gemeinsamen ordner popup ist noch hell" und "er hebt den
aktiven tab nicht hervor, der inaktive ist der hervorgehobene".

- Meldungen und Ja/Nein-Fragen kommen jetzt aus eigenen Fenstern im Theme
  (MeldungMixin). messagebox sind Fenster von WINDOWS - sie bleiben hell, egal
  was das Programm einstellt. Dieselbe Erfahrung steckt schon in der eigenen
  Menueleiste. 49 Aufrufe umgestellt; das Ordner-Fenster meldet ueber das
  Hauptfenster mit.
  ⚠️ Im Pruflauf laeuft beides weiter ueber messagebox - dort haengen die
  Pruefstaende ihre Antworten ein. Sonst warten sie auf einen Klick.
- Reiter: der OFFENE ist jetzt der hellere (Kachelfarbe), die ruhenden liegen
  auf dem dunklen Grund. Vorher war es andersherum.
- config.TESTLAUF ist eine FUNKTION geworden: Pruefstaende importieren config
  oft, bevor sie CAMPINGHOF_TEST setzen - der einmal berechnete Wert stand dann
  den ganzen Lauf auf False, und das Programm oeffnete echte Dialoge, auf die
  niemand klickt. Genau daran hing pruef_api.
- Der Erststart-Dialog hat einen eigenen Schalter (_erststart_zeigen), statt am
  ganzen Pruefschalter zu haengen: ein Pruefstand kann IHN zeigen, ohne dass
  alle anderen Meldungen wieder echte Fenster aufmachen.
- Pruefstand-Fenster stehen nicht mehr in der Taskleiste (-toolwindow): sie
  liegen neben dem Bildschirm, und ein Eintrag, den man anklickt, ohne dass
  etwas erscheint, ist schlimmer als gar keiner.
- pruef_nummernsperre drueckt jetzt den Radioknopf "Meine Zaehlung
  weiterfuehren", statt nur ins Zahlenfeld zu schreiben - das Feld ist ohne
  diesen Klick gesperrt, wie beim Benutzer auch.

Alle 14 Pruefstaende gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
This commit is contained in:
TheMockTv 2026-09-05 19:17:13 +02:00
parent 5ebb02b267
commit 2e3a191f19
8 changed files with 229 additions and 74 deletions

106
theme.py
View file

@ -8,6 +8,7 @@ baut das Programm Menueleiste und Dropdowns selbst; hier stehen nur die Farben.
import sys
import tkinter as tk
from tkinter import font as tkfont
from tkinter import messagebox
from tkinter import ttk
import config
@ -264,11 +265,15 @@ class ThemeMixin:
# Akzentfarbe, die ruhenden bleiben zurueckgenommen.
style.configure("TNotebook", background=bg, bordercolor=self.col_line,
tabmargins=(2, 4, 2, 0), borderwidth=0)
style.configure("TNotebook.Tab", background=self.col_field,
# Der OFFENE Reiter gehoert zur Kachel darunter und ist deshalb heller
# als die ruhenden. Vorher war es andersherum: die ruhenden lagen auf der
# helleren Flaeche und sahen aus, als waeren sie ausgewaehlt.
style.configure("TNotebook.Tab", background=bg,
foreground=self.col_muted, bordercolor=self.col_line,
padding=(20, 9), font=(SCHRIFT_NAME, 10), borderwidth=0)
style.map("TNotebook.Tab",
background=[("selected", self.col_card), ("active", sel)],
background=[("selected", self.col_card),
("active", mischen(accent, bg, 0.12))],
foreground=[("selected", accent), ("active", fg)],
font=[("selected", (SCHRIFT_NAME, 10, "bold"))])
# Kein expand beim ausgewaehlten Reiter: er wuchs dadurch um ein paar
@ -286,12 +291,16 @@ class ThemeMixin:
und Tastatur fest, waehrend nebenan jemand schreibt.
"""
d.configure(bg=self.col_bg)
if config.TESTLAUF:
if config.testlauf():
# Wie das Hauptfenster: neben den Bildschirm, und ohne grab_set -
# ein Dialog, der Maus und Tastatur festhaelt, blockiert den
# Menschen, der nebenher schreibt.
d.geometry(f"+{d.winfo_screenwidth() + 20}+40")
d.grab_set = lambda *a, **k: None
try:
d.attributes("-toolwindow", True) # nicht in die Taskleiste
except tk.TclError:
pass
# erst nach dem Anzeigen setzen, sonst greift DWM noch nicht
d.after(20, lambda: self._dark_titlebar(d))
@ -312,3 +321,94 @@ class ThemeMixin:
ctypes.windll.user32.SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP)
except Exception as e: # noqa: BLE001
print("[theme] Titelleiste:", e)
class MeldungMixin:
"""Eigene Meldungs- und Ja/Nein-Fenster im Theme des Programms.
Warum nicht `messagebox`: das sind Fenster von Windows selbst. Sie sind
immer hell, egal was das Programm einstellt - mitten in einer dunklen
Oberflaeche blendet das und sieht nach Fremdkoerper aus. Dieselbe Erfahrung
steckt schon in der eigenen Menueleiste weiter oben.
⚠️ Im PRUEFLAUF geht beides weiter ueber `messagebox`. Die Pruefstaende
haengen ihre Antworten dort ein ("auf diese Frage antworte Ja") und lesen
dort mit, was das Programm gemeldet hat. Wuerde hier ein eigenes Fenster
aufgehen, warteten sie auf einen Klick, den niemand macht. Geprueft wird
damit derselbe Weg - nur die Anzeige unterscheidet sich.
"""
# Die drei Sorten Meldung. `parent=`, `icon=` und `default=` kommen aus den
# frueheren messagebox-Aufrufen mit - sie werden geschluckt, damit an den
# Aufrufstellen nichts umgeschrieben werden muss.
def melden_info(self, titel, text="", **_kw):
return self.melden(titel, text, "info")
def melden_warnung(self, titel, text="", **_kw):
return self.melden(titel, text, "warnung")
def melden_fehler(self, titel, text="", **_kw):
return self.melden(titel, text, "fehler")
def melden(self, titel, text, art="warnung"):
"""Eine Meldung mit einem OK-Knopf."""
if config.testlauf():
{"info": messagebox.showinfo,
"warnung": messagebox.showwarning,
"fehler": messagebox.showerror}.get(art, messagebox.showwarning)(titel, text)
return None
return self._meldungsfenster(titel, text, art, knoepfe=None)
def frage(self, titel, text="", ja="Ja", nein="Nein", vorgabe=True, **_kw):
"""Ja/Nein. Gibt True oder False zurueck - wie askyesno."""
if config.testlauf():
return bool(messagebox.askyesno(titel, text))
return bool(self._meldungsfenster(titel, text, "frage",
knoepfe=(ja, nein), vorgabe=vorgabe))
def _meldungsfenster(self, titel, text, art, knoepfe=None, vorgabe=True):
farbe = {"fehler": self.col_gefahr, "warnung": self.col_gefahr,
"frage": self.col_accent}.get(art, self.col_accent)
d = tk.Toplevel(self)
d.title(titel)
d.transient(self)
d.resizable(False, False)
self._style_dialog(d)
antwort = {"wert": False}
rahmen = ttk.Frame(d, padding=20)
rahmen.pack(fill="both", expand=True)
tk.Label(rahmen, text=titel, bg=self.col_bg, fg=farbe, justify="left",
font=(SCHRIFT_NAME, 12, "bold")).pack(anchor="w")
tk.Label(rahmen, text=text, bg=self.col_bg, fg=self.col_fg, justify="left",
wraplength=460, font=(SCHRIFT_NAME, 10)).pack(anchor="w", pady=(10, 0))
leiste = ttk.Frame(rahmen)
leiste.pack(fill="x", pady=(20, 0))
def schliessen(wert):
antwort["wert"] = wert
d.destroy()
if knoepfe:
ja, nein = knoepfe
ttk.Button(leiste, text=ja, style="Haupt.TButton",
command=lambda: schliessen(True)).pack(side="right")
ttk.Button(leiste, text=nein,
command=lambda: schliessen(False)).pack(side="right", padx=(0, 8))
d.bind("<Return>", lambda _e: schliessen(vorgabe))
else:
ttk.Button(leiste, text="OK", style="Haupt.TButton",
command=lambda: schliessen(True)).pack(side="right")
d.bind("<Return>", lambda _e: schliessen(True))
d.bind("<Escape>", lambda _e: schliessen(False))
d.protocol("WM_DELETE_WINDOW", lambda: schliessen(False))
d.update_idletasks()
x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2)
y = self.winfo_rooty() + 140
d.geometry(f"+{max(0, x)}+{max(0, y)}")
d.grab_set()
d.focus_set()
self.wait_window(d)
return antwort["wert"]