rechnungstool/bauen.py
TheMockTv f7801f46f0 Darstellung waehlbar: hell, dunkel oder wie Windows
Seine Ansage
  "waere nun noch gut das mann in den einstellungen einstellen kann hell
   oder dunkel oder automatik das fehlt sollte eigendlich drin sein"

  Er hatte recht: beide Farbpaletten lagen laengst in theme.py, das Programm
  folgte aber starr dem Windows-Theme. Jetzt steht die Wahl unter
  Einstellungen -> Darstellung und wirkt SOFORT, nicht erst beim Neustart.

Umschalten heisst neu bauen
  Die Farben stehen an ueber sechzig Stellen fest in den Widgets. Sie einzeln
  nachzuziehen hiesse, ein Verzeichnis aller eingefaerbten Widgets zu fuehren
  - und wer beim naechsten neuen Widget vergisst, es einzutragen, hat einen
  hellen Fleck im dunklen Fenster, den niemand mehr findet. Neu bauen kann
  nichts vergessen.

  ⚠️ Dabei gingen im ersten Versuch Kundenname und Rechnungsnummer verloren:
  die _build_-Methoden legen ihre StringVars SELBST an, nach dem Neubau
  standen leere da. Gesichert werden jetzt ALLE Tk-Variablen des Fensters,
  nicht eine gepflegte Auswahl - wer spaeter ein Feld hinzufuegt, muss nichts
  eintragen.

Titelleiste, Symbol und Logo gehen mit
  Die Titelleiste gehoert Windows; sie folgt ueber
  DWMWA_USE_IMMERSIVE_DARK_MODE. _dark_titlebar konnte bisher nur DUNKEL und
  zeigt jetzt in beide Richtungen - vorher behielt man beim Wechsel auf hell
  eine schwarze Leiste ueber einem hellen Fenster.

  Sein Befund: "die svg fuer das dunkle heller machen weil die geht nun
  unter" und "icon musst wenn es dunkel ist mit aendern auf das hellere".
  Gemessen sind es zwei Farben: Petrol #004860 und ein fast schwarzes
  #181818 - letzteres liegt genau auf der Kachelfarbe #15171b. Umgestellt
  wird nur die HELLIGKEIT (L -> 1-L, gedeckelt bei 0.78), Farbton und
  Saettigung bleiben: sonst waere es nicht mehr sein Logo. Betrifft
  Wasserzeichen, Kopflogo und das Fenstersymbol; die aufgehellte ICO entsteht
  beim Bauen.

menueleiste.py
  Die selbstgebaute Menueleiste ist jetzt eine eigene Datei und liegt in
  beiden Programmen gleich - das Steuerrechnungstool hatte noch das native
  tk.Menu, das sich unter Windows nicht einfaerben laesst.

pruef_darstellung.py
  Prueft die drei Modi, dass beim Umschalten NICHTS verlorengeht, und dass
  danach kein Widget mehr eine Farbe der anderen Palette traegt - genau der
  helle Fleck, den man sonst nie wiederfindet.

Nebenbei: die beiden Dauertimer melden beim Beenden kein
"invalid command name ..._api_pumpe" mehr. Ein Log, in dem beim normalen
Beenden immer zwei Fehler stehen, liest irgendwann niemand mehr.

Fassung 1.8.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
2026-09-07 00:34:16 +02:00

137 lines
5.3 KiB
Python

# -*- coding: utf-8 -*-
"""Der ganze Weg von der Quelle zur Setup-Datei - an EINER Stelle.
WARUM DAS HIER STEHT
--------------------
Bis hierher lag der Bau in einer Folge von Befehlen, die jemand von Hand
eintippen musste: PyInstaller fuers Programm, PyInstaller fuer den Updater,
umbenennen, Inno Setup mit den richtigen Schaltern. Wer einen Schritt
vergisst, liefert eine Setup-Datei aus, die eine alte EXE enthaelt - und
niemand sieht es ihr an. Genau das ist am 06.09.2026 passiert (der Installer
schrieb ueber eine laufende, gesperrte EXE hinweg und meldete trotzdem Erfolg).
Ab jetzt ist der Bau ein Befehl:
python bauen.py
WAS ER TUT
----------
1. Die Nummer kommt aus version.py - nicht von der Kommandozeile. So kann sie
nicht auseinanderlaufen mit dem, was das Programm ueber sich selbst sagt.
2. Programm und Updater werden gebaut.
3. Die Programm-EXE bekommt die Nummer in den Namen. Der Updater erkennt daran,
welche Fassungen installiert sind, und legt neue DANEBEN statt darueber -
deshalb kann er auch dann austauschen, wenn Windows die alte Datei sperrt.
4. Inno Setup baut die Setup-Datei.
5. Zum Schluss stehen die SHA256-Summen da. Die gehoeren in den Release-Text:
nur damit laesst sich spaeter nachweisen, dass die heruntergeladene Datei
genau die gebaute ist.
"""
import hashlib
import os
import shutil
import subprocess
import sys
HIER = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HIER)
import version # noqa: E402
# Inno Setup liegt bei ihm unter LOCALAPPDATA (Installation ohne Adminrechte).
ISCC_ORTE = [
os.path.join(os.environ.get("LOCALAPPDATA", ""), "Programs", "Inno Setup 6", "ISCC.exe"),
r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
r"C:\Program Files\Inno Setup 6\ISCC.exe",
]
def summe(pfad):
h = hashlib.sha256()
with open(pfad, "rb") as f:
for brocken in iter(lambda: f.read(1 << 20), b""):
h.update(brocken)
return h.hexdigest()
def lauf(befehl, was):
print(f"--- {was}")
e = subprocess.run(befehl, cwd=HIER, capture_output=True, text=True,
encoding="utf-8", errors="replace")
if e.returncode != 0:
print(e.stdout[-3000:])
print(e.stderr[-3000:])
raise SystemExit(f"{was} fehlgeschlagen (Rueckgabe {e.returncode})")
def iscc():
for p in ISCC_ORTE:
if p and os.path.exists(p):
return p
raise SystemExit("ISCC.exe nicht gefunden - Inno Setup 6 fehlt")
def main():
stamm, endung = os.path.splitext(version.EXE_NAME)
mit_nummer = f"{stamm}-{version.VERSION}{endung}"
# Die Bauordner werden geleert. Ein PyInstaller-Lauf ueber einen alten
# Bauordner uebernimmt sonst Reste, die es in der Quelle nicht mehr gibt -
# und in setup/ laege noch die Datei der VORIGEN Nummer herum. Beim
# Ausliefern greift man dann leicht zur falschen.
for ordner in ("build", "dist", "setup", "release"):
shutil.rmtree(os.path.join(HIER, ordner), ignore_errors=True)
# Die aufgehellte Fassung des Symbols fuer die dunkle Darstellung. Sie
# wird HIER erzeugt und nicht erst beim ersten dunklen Start: sonst muesste
# das Programm in seinen eigenen Ordner schreiben duerfen - unter
# "Programme" darf es das nicht.
sys.path.insert(0, HIER)
import wasserzeichen
dunkles = wasserzeichen.symbol_datei_dunkel()
print(f"--- Symbol dunkel: {dunkles or 'FEHLT'}")
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", f"{stamm}.spec"],
f"{stamm} {version.VERSION}")
lauf([sys.executable, "-m", "PyInstaller", "--noconfirm", "Updater.spec"],
"Updater")
quelle = os.path.join(HIER, "dist", version.EXE_NAME)
ziel = os.path.join(HIER, "dist", mit_nummer)
os.replace(quelle, ziel)
lauf([iscc(), f"/DVersion={version.VERSION}", f"/DExeProgramm={mit_nummer}",
"Installer.iss"], "Setup")
# ---- release/ : genau das, was ins Release gehoert ---------------------
# Der Anhang, den der Updater sucht, heisst OHNE Nummer - so steht es in
# version.EXE_NAME, und danach sucht passendes_release(). Haengt man ihn
# mit Nummer an, findet der Updater nie ein Update und meldet stumm
# "aktuell". Die Nummer kommt erst beim Ablegen dazu (updater.dateiname).
#
# Daneben die Pruefsumme als eigene Datei: ohne sie prueft der Updater nur
# die GROESSE. Eine Datei, die unterwegs verfaelscht wurde, hat aber genau
# die richtige Groesse.
freigabe = os.path.join(HIER, "release")
os.makedirs(freigabe, exist_ok=True)
shutil.copy2(ziel, os.path.join(freigabe, version.EXE_NAME))
with open(os.path.join(freigabe, version.EXE_NAME + ".sha256"),
"w", encoding="ascii") as f:
f.write(summe(ziel) + chr(10))
shutil.copy2(os.path.join(HIER, "dist", "Updater.exe"),
os.path.join(freigabe, "Updater.exe"))
for name in os.listdir(os.path.join(HIER, "setup")):
shutil.copy2(os.path.join(HIER, "setup", name),
os.path.join(freigabe, name))
print()
print(f"FERTIG | {stamm} {version.VERSION}")
print()
print(" release/ - genau diese Dateien gehoeren ans Release:")
for name in sorted(os.listdir(freigabe)):
p = os.path.join(freigabe, name)
print(f" {name:44s} {os.path.getsize(p):>10,d} B {summe(p)}")
if __name__ == "__main__":
main()