rechnungstool/einzelinstanz.py
TheMockTv 2277a12e0f Zwei getrennte Knoepfe und app.py in Module aufgeteilt
Knoepfe: "Storno" und "Berichtigen" setzen den Weg schon beim Oeffnen fest,
das Popup fragt dann nur noch die Nummer. Vorher entschied ein zweiter Knopf
im Popup - der ist raus.

app.py war 1802 Zeilen, jetzt 672. Neu daneben:
  helfer.py        Zahlen, Datum, Namen, Pruefungen        (87)
  bestand.py       was im Rechnungsordner liegt, nur lesen (171)
  storno.py        den spiegelbildlichen Storno rechnen     (57)
  einzelinstanz.py nur ein Fenster zulassen                 (90)
  theme.py         Farben nach Windows hell/dunkel         (120)
  korrektur.py     Storno- und Berichtigungsweg            (306)
  einstellungen.py die Dialoge aus dem Menue               (358)

Die Oberflaeche bleibt eine Klasse - die Teile sind Mixins, damit kein
einziger self-Zugriff umgeschrieben werden musste. Kein Modul importiert
app.py zurueck, keine ungenutzten Importe mehr. Die Pruefstaende holen die
Funktionen jetzt aus dem Modul, dem sie gehoeren.

pruef_storno.py 100 gruen, pruef_nummern.py 7 gruen, Fenster nach der
Aufteilung gestartet und angesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EKvGkdNW1vKdnMPAM9Bp3X
2026-09-02 19:41:00 +02:00

90 lines
3.2 KiB
Python

# -*- coding: utf-8 -*-
"""Nur ein Fenster zulassen und das offene nach vorn holen.
Zwei Fenster lesen beim Start denselben Zähler und schlagen dieselbe
Rechnungsnummer vor - die Nummer wird dann doppelt vergeben.
"""
import os
import sys
import config
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