Nur eine Instanz: zweiter Start holt das offene Fenster nach vorn
Zwei offene Fenster lesen beim Start denselben Zaehler aus der config und schlagen darum dieselbe Rechnungsnummer vor - die Nummer wurde doppelt vergeben. - Windows-Mutex sperrt eine zweite Instanz ab. - Die laufende Instanz legt ihre PID neben der config.json ab; die zweite sucht darueber das Fenster (nicht ueber den Titel - der haengt am Firmennamen aus der config) und holt es nach vorn. - Popup nur noch als Rueckfall, wenn kein Fenster gefunden wird. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
This commit is contained in:
parent
a0cfc31ae6
commit
1564d769c9
2 changed files with 106 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -8,3 +8,4 @@ config.json
|
|||
_tmp/
|
||||
*.7z.*
|
||||
release/
|
||||
rechnungstool.pid
|
||||
|
|
|
|||
105
app.py
105
app.py
|
|
@ -48,6 +48,94 @@ def safe_filename(s):
|
|||
return "".join(c for c in s if c.isalnum() or c in keep).strip() or "Rechnung"
|
||||
|
||||
|
||||
def einzelinstanz_pruefen():
|
||||
"""Sperrt das Programm gegen ein zweites Fenster.
|
||||
|
||||
Beide Fenster lesen beim Start denselben Zaehler aus der config und
|
||||
schlagen darum dieselbe Rechnungsnummer vor -> die Nummer wird doppelt
|
||||
vergeben. Darum: nur eine Instanz je Windows-Sitzung.
|
||||
|
||||
Rueckgabe: Handle (offen halten, solange das Programm laeuft) oder
|
||||
False, wenn schon eine Instanz laeuft.
|
||||
"""
|
||||
if not sys.platform.startswith("win"):
|
||||
return None
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
ERROR_ALREADY_EXISTS = 183
|
||||
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
||||
k32.CreateMutexW.argtypes = [wintypes.LPVOID, wintypes.BOOL, wintypes.LPCWSTR]
|
||||
k32.CreateMutexW.restype = wintypes.HANDLE
|
||||
handle = k32.CreateMutexW(None, False, "RechnungstoolCampinghofBartl")
|
||||
if ctypes.get_last_error() == ERROR_ALREADY_EXISTS:
|
||||
return False
|
||||
return handle
|
||||
|
||||
|
||||
|
||||
# Merkzettel mit der PID der laufenden Instanz - liegt neben der config.json.
|
||||
PID_DATEI = os.path.join(config.BASE_DIR, "rechnungstool.pid")
|
||||
|
||||
|
||||
def eigene_pid_merken():
|
||||
"""Schreibt die eigene PID, damit eine zweite Instanz das Fenster findet."""
|
||||
try:
|
||||
with open(PID_DATEI, "w", encoding="ascii") as f:
|
||||
f.write(str(os.getpid()))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def laufende_pid_lesen():
|
||||
"""PID der laufenden Instanz aus dem Merkzettel; None wenn unlesbar."""
|
||||
try:
|
||||
with open(PID_DATEI, "r", encoding="ascii") as f:
|
||||
return int(f.read().strip())
|
||||
except (OSError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def fenster_nach_vorn(pid):
|
||||
"""Holt das Fenster des Prozesses <pid> nach vorn. True bei Erfolg.
|
||||
|
||||
Gesucht wird ueber die PID, nicht ueber den Fenstertitel - der Titel
|
||||
haengt am Firmennamen aus der config und aendert sich mit ihm.
|
||||
"""
|
||||
if not pid or not sys.platform.startswith("win"):
|
||||
return False
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
SW_RESTORE = 9
|
||||
u32 = ctypes.WinDLL("user32", use_last_error=True)
|
||||
u32.IsWindowVisible.argtypes = [wintypes.HWND]
|
||||
u32.IsIconic.argtypes = [wintypes.HWND]
|
||||
u32.ShowWindow.argtypes = [wintypes.HWND, ctypes.c_int]
|
||||
u32.SetForegroundWindow.argtypes = [wintypes.HWND]
|
||||
u32.GetWindowTextLengthW.argtypes = [wintypes.HWND]
|
||||
u32.GetWindowThreadProcessId.argtypes = [wintypes.HWND, ctypes.POINTER(wintypes.DWORD)]
|
||||
|
||||
treffer = []
|
||||
|
||||
def besuch(hwnd, _lparam):
|
||||
fremd = wintypes.DWORD()
|
||||
u32.GetWindowThreadProcessId(hwnd, ctypes.byref(fremd))
|
||||
# nur das echte Hauptfenster: sichtbar und mit Titel
|
||||
if fremd.value == pid and u32.IsWindowVisible(hwnd) and u32.GetWindowTextLengthW(hwnd):
|
||||
treffer.append(hwnd)
|
||||
return False
|
||||
return True
|
||||
|
||||
CB = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM)
|
||||
u32.EnumWindows(CB(besuch), 0)
|
||||
if not treffer:
|
||||
return False
|
||||
hwnd = treffer[0]
|
||||
if u32.IsIconic(hwnd):
|
||||
u32.ShowWindow(hwnd, SW_RESTORE)
|
||||
u32.SetForegroundWindow(hwnd)
|
||||
return True
|
||||
|
||||
|
||||
def windows_dark_mode():
|
||||
"""True, wenn Windows auf das dunkle App-Theme eingestellt ist."""
|
||||
if not sys.platform.startswith("win"):
|
||||
|
|
@ -931,5 +1019,22 @@ class RechnungsApp(tk.Tk):
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Muss VOR dem Fenster passieren - das Handle bleibt bis zum Ende offen.
|
||||
_sperre = einzelinstanz_pruefen()
|
||||
if _sperre is False:
|
||||
# Laeuft schon: dessen Fenster nach vorn holen und still beenden.
|
||||
if not fenster_nach_vorn(laufende_pid_lesen()):
|
||||
_r = tk.Tk()
|
||||
_r.withdraw()
|
||||
messagebox.showerror(
|
||||
"Rechnungstool läuft bereits",
|
||||
"""Das Rechnungstool ist schon geöffnet, das Fenster ließ sich
|
||||
aber nicht nach vorn holen - bitte über die Taskleiste zurückwechseln.
|
||||
|
||||
Ein zweites Fenster würde dieselbe Rechnungsnummer noch einmal vergeben.""")
|
||||
_r.destroy()
|
||||
sys.exit(0)
|
||||
|
||||
eigene_pid_merken()
|
||||
app = RechnungsApp()
|
||||
app.mainloop()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue