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
137 lines
5.3 KiB
Python
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()
|