From 688dfb53c4244f79940fa1c60c3a744288ba1cee Mon Sep 17 00:00:00 2001 From: TheMockTv Date: Mon, 7 Sep 2026 00:52:58 +0200 Subject: [PATCH] Popups gehen mittig ueber dem Programmfenster auf Seine Ansage "da gehen noch die popups auf dem video monitor auf" "einfach so programmieren das die popup auf dem fenster mittig vom main fenster auf gehen" Was falsch war An sechs Stellen stand `max(0, x)`. Das sollte verhindern, dass ein Dialog ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE Koordinate auf null. Bei ihm steht der zweite Monitor links (x = -1920): jedes Popup sprang damit zurueck auf den Hauptbildschirm, obwohl das Programm nebenan lief. Dazu kamen feste Versaetze wie "+120+140", die mit der Fenstergroesse nichts zu tun hatten. theme.mittig(dialog, ueber) Eine Stelle fuer alle Dialoge: mittig ueber dem Elternfenster, etwas oberhalb der Mitte (ein Drittel statt der Haelfte), damit der Blick dort ist und die Mitte des Formulars frei bleibt. Das ist zugleich die Antwort auf die Monitorfrage: steht das Programm auf dem zweiten Bildschirm, geht der Dialog dort auf - ohne dass irgendwo ein Monitor gesucht werden muss. Die Sonderbehandlung im Testlauf konnte deshalb entfallen; geblieben ist dort nur der Verzicht auf grab_set. theme.auf_bildschirm(x, y, breite, hoehe) Begrenzt eine Position auf den GESAMTEN Desktop ueber alle Monitore (SM_*VIRTUALSCREEN) statt auf den Hauptmonitor. Das war die eigentliche Absicht hinter dem alten max(0, ...). Gemessen statt geglaubt: Dialogmitte und Fenstermitte liegen 8 Pixel auseinander (Fensterrahmen), der Dialog liegt innerhalb des Fensters. Steht als Pruefung in pruef_darstellung.py. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax --- app.py | 6 ++-- korrektur.py | 2 +- ordnerwahl.py | 3 +- pruef_darstellung.py | 22 ++++++++++++++ pruef_release.py | 10 ++++++- pruef_start_exe.py | 7 ++++- theme.py | 71 +++++++++++++++++++++++++++++++++++++++----- 7 files changed, 106 insertions(+), 15 deletions(-) diff --git a/app.py b/app.py index e2c9186..c26bd2b 100644 --- a/app.py +++ b/app.py @@ -574,7 +574,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, 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") - d.geometry(f"+{self.winfo_rootx() + 120}+{self.winfo_rooty() + 140}") + theme.mittig(d, self) return d def _umbruch_nachziehen(self, runde=0, vorige_breite=None): @@ -1078,7 +1078,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 110}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) if fertig["ok"]: @@ -1428,7 +1428,7 @@ class RechnungsApp(ThemeMixin, MeldungMixin, KorrekturMixin, EinstellungenMixin, d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 130}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) return wahl["art"] diff --git a/korrektur.py b/korrektur.py index 45b71e1..cbbdb82 100644 --- a/korrektur.py +++ b/korrektur.py @@ -513,7 +513,7 @@ class KorrekturMixin: d.bind("", lambda _e: d.destroy()) d.update_idletasks() x = self.winfo_rootx() + max(0, (self.winfo_width() - d.winfo_reqwidth()) // 2) - d.geometry(f"+{max(0, x)}+{self.winfo_rooty() + 110}") + theme.mittig(d, self) d.grab_set() self.wait_window(d) if wahl["art"] == "vorhanden": diff --git a/ordnerwahl.py b/ordnerwahl.py index 6cc89b3..367c470 100644 --- a/ordnerwahl.py +++ b/ordnerwahl.py @@ -106,7 +106,8 @@ class OrdnerWahl(tk.Toplevel): 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}+{max(0, x)}+{max(0, y)}") + self.geometry(f"{BREITE}x{hoehe}") + theme.mittig(self, master) self.minsize(BREITE, hoehe) self.grab_set() self.lift() diff --git a/pruef_darstellung.py b/pruef_darstellung.py index be96ebd..3c076b6 100644 --- a/pruef_darstellung.py +++ b/pruef_darstellung.py @@ -167,6 +167,28 @@ for wunsch, soll_dunkel in (("hell", False), ("dunkel", True)): f"in '{wunsch}' ist keine Farbe der anderen Darstellung " f"uebrig ({reste[:3]})") +# ---- Dialoge gehen mittig ueber dem Programmfenster auf ------------------ +# Seine Ansage: "einfach so programmieren das die popup auf dem fenster mittig +# vom main fenster auf gehen". Das ist zugleich die Antwort auf die +# Monitorfrage - steht das Programm auf dem zweiten Bildschirm, geht der +# Dialog dort auf. Vorher klemmte ein max(0, x) jede negative Koordinate auf +# null und riss die Fenster auf den Hauptmonitor zurueck. +d = f.dlg_darstellung() +pumpen(1.2) +d.update_idletasks() +mitte_dialog = d.winfo_rootx() + d.winfo_width() // 2 +mitte_fenster = f.winfo_rootx() + f.winfo_width() // 2 +pruefe(abs(mitte_dialog - mitte_fenster) <= 30, + f"der Dialog steht mittig ueber dem Fenster " + f"({mitte_dialog} vs. {mitte_fenster})") +pruefe(d.winfo_rootx() >= f.winfo_rootx() - 40 + and d.winfo_rootx() + d.winfo_width() <= f.winfo_rootx() + f.winfo_width() + 40, + "und liegt waagerecht innerhalb des Fensters") +pruefe(d.winfo_rooty() >= f.winfo_rooty() - 40, + "und nicht oberhalb davon") +d.destroy() +pumpen(0.3) + # ---- Zurueck auf automatisch -------------------------------------------- f.cfg["darstellung"] = "auto" config.save_config(f.cfg) diff --git a/pruef_release.py b/pruef_release.py index 0616fa8..6722570 100644 --- a/pruef_release.py +++ b/pruef_release.py @@ -64,7 +64,15 @@ if dieses is None: print("ROT | ohne Release ist der Rest nicht pruefbar") sys.exit(1) -anhaenge = {a.get("name"): a for a in dieses.get("assets") or []} +roh = dieses.get("assets") or [] +anhaenge = {a.get("name"): a for a in roh} +# Doppelte Namen: Forgejo laesst zwei Anhaenge mit demselben Namen zu. Beim +# Ersetzen einer Datei blieb die alte einmal stehen (07.09.2026), und welche +# von beiden der Updater dann laedt, ist Glueckssache - unter Umstaenden die +# alte, mit der falschen Pruefsumme daneben. +doppelt = sorted({a["name"] for a in roh + if sum(1 for b in roh if b["name"] == a["name"]) > 1}) +pruefe(not doppelt, f"kein Anhang haengt doppelt am Release ({doppelt})") print(f" (Anhaenge: {', '.join(sorted(anhaenge)) or 'keine'})") pruefe(version.EXE_NAME in anhaenge, diff --git a/pruef_start_exe.py b/pruef_start_exe.py index 04f02fb..adccc6b 100644 --- a/pruef_start_exe.py +++ b/pruef_start_exe.py @@ -57,7 +57,12 @@ if not os.path.exists(EXE): sys.exit(0) u = ctypes.windll.user32 -p = subprocess.Popen([EXE]) +# Mit --test: dann geht das Fenster auf dem ZWEITEN Monitor auf und nicht +# mitten auf dem Hauptbildschirm, wo gerade jemand arbeitet. Seine Ansage am +# 07.09.2026 - und dieser Pruefstand war der eine, der sie noch nicht befolgte, +# weil er die gebaute EXE ganz normal startet. Sichtbar bleibt das Fenster, +# und genau das wird hier ja geprueft. +p = subprocess.Popen([EXE, "--test"]) print(f" (gestartet: {os.path.basename(EXE)}, PID {p.pid})") RUECKRUF = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) diff --git a/theme.py b/theme.py index 25502ef..309f261 100644 --- a/theme.py +++ b/theme.py @@ -121,6 +121,63 @@ def pruefplatz(): return "" +def auf_bildschirm(x, y, breite=0, hoehe=0): + """Eine Fensterposition auf den sichtbaren Bereich begrenzen. + + Frueher stand an diesen Stellen `max(0, x)`. Das sollte verhindern, dass + ein Dialog ausserhalb des Bildschirms aufgeht - klemmt aber jede NEGATIVE + Koordinate auf null. Steht der zweite Monitor links (bei ihm x = -1920), + sprang damit jedes Popup zurueck auf den Hauptbildschirm, obwohl das + Programm nebenan lief. Sein Befund am 07.09.2026: *"da gehen noch die + popups auf dem video monitor auf"*. + + Begrenzt wird deshalb auf den GESAMTEN Desktop ueber alle Monitore + (SM_*VIRTUALSCREEN), nicht auf den Hauptmonitor. + """ + if not sys.platform.startswith("win"): + return int(x), int(y) + try: + import ctypes + hole = ctypes.windll.user32.GetSystemMetrics + links, oben = hole(76), hole(77) + rechts, unten = links + hole(78), oben + hole(79) + if breite: + x = min(x, rechts - breite) + if hoehe: + y = min(y, unten - hoehe) + return int(max(links, x)), int(max(oben, y)) + except Exception: # noqa: BLE001 - dann eben ungebremst + return int(x), int(y) + + +def mittig(dialog, ueber): + """Einen Dialog mittig ueber sein Elternfenster setzen. + + Seine Ansage am 07.09.2026: *"einfach so programmieren das die popup auf + dem fenster mittig vom main fenster auf gehen"*. + + Das ist zugleich die Antwort auf die Monitorfrage: steht das Programm auf + dem zweiten Bildschirm, geht der Dialog dort auf - ohne dass irgendwo ein + Monitor gesucht werden muesste. Vorher standen an den Dialogen feste + Versaetze ("+120+140") und ein `max(0, x)`, das jede negative Koordinate + auf null klemmte und die Fenster damit auf den Hauptmonitor zurueckriss. + + Etwas oberhalb der Mitte (ein Drittel statt der Haelfte): so sitzt der + Dialog dort, wo der Blick ohnehin ist, und verdeckt nicht die Mitte des + Formulars darunter. + """ + try: + dialog.update_idletasks() + breite = dialog.winfo_reqwidth() or dialog.winfo_width() + hoehe = dialog.winfo_reqheight() or dialog.winfo_height() + x = ueber.winfo_rootx() + (ueber.winfo_width() - breite) // 2 + y = ueber.winfo_rooty() + (ueber.winfo_height() - hoehe) // 3 + x, y = auf_bildschirm(x, y, breite, hoehe) + dialog.geometry(f"+{x}+{y}") + except tk.TclError: + pass # Fenster schon zu - dann ist die Position gleichgueltig + + def testlauf(): """Faehrt gerade ein Pruefstand das Programm? @@ -532,11 +589,12 @@ class ThemeMixin: und Tastatur fest, waehrend nebenan jemand schreibt. """ d.configure(bg=self.col_bg) + # Mittig ueber dem Programmfenster - und damit automatisch auf dem + # Bildschirm, auf dem das Programm gerade steht. + d.after(1, lambda: mittig(d, self)) if testlauf(): - # Wie das Hauptfenster: auf den zweiten Monitor, und ohne grab_set - - # ein Dialog, der Maus und Tastatur festhaelt, blockiert den - # Menschen, der nebenher arbeitet. - d.geometry(pruefplatz() or f"+{d.winfo_screenwidth() + 20}+40") + # Ohne grab_set: ein Dialog, der Maus und Tastatur festhaelt, + # blockiert den Menschen, der nebenher arbeitet. d.grab_set = lambda *a, **k: None try: d.attributes("-toolwindow", True) # nicht in die Taskleiste @@ -639,10 +697,7 @@ class MeldungMixin: d.bind("", 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)}") + mittig(d, self) # Nach vorn holen und den Tastaturfokus nehmen. Ohne das kann der Dialog # beim Start hinter dem Hauptfenster liegen - der Nutzer sieht nur ein # Fenster, das nicht reagiert, und haelt es fuer haengen geblieben.