Gleiche Umstellung wie im Rechnungstool, gleiche gemeinsame Dateien
(gemeinsam.py, ordnerwahl.py, wasserzeichen.py, bauen.py).
%LOCALAPPDATA%/ravokk/
journal.sqlite3, berichte/, Nummernbuch, config.json <- Daten
Rechnungstool/ <- Programm
Steuerrechnungstool/ <- Programm
⚠ Hier gefunden und behoben: das Journal der INSTALLIERTEN Fassung lag im
Temp-Ordner. Der Merkzettel (ablage.py) zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09.2026. Windows raeumt den Temp-Ordner von selbst auf; das Journal
waere ohne Vorwarnung verschwunden. Ein Merkzettel, der dorthin zeigt,
wird jetzt verworfen statt befolgt.
Sein Echtbestand (238 Buchungen) lag davon unberuehrt in der Quelle und
ist gesichert.
Kein Ordner-Dialog mehr beim ersten Start - der gemeinsame Ordner steht
fest. Der Knopf hiess dort "Install-Ordner auswaehlen...", was schon immer
falsch war: es geht um das gemeinsame Nummernbuch, nicht um den
Installationsort.
Uninstaller raeumt jetzt auch die vom Updater geladenen Fassungen weg.
pruef_installation.py prueft beides zusammen: Programmordner leer, Daten
unangetastet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
232 lines
9.3 KiB
Python
232 lines
9.3 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Das Programmlogo - als Fenstersymbol und als blasses Wasserzeichen.
|
|
|
|
WOHER DAS BILD KOMMT
|
|
--------------------
|
|
Die Quelle ist die SVG aus dem Entwurf (`assets/programm_logo.svg`). Sie wird
|
|
BEIM BAUEN einmal in 512 Pixel gerendert (`assets/programm_logo.png`) und in
|
|
eine `programm.ico` fuer Windows umgesetzt. Hier zur Laufzeit wird nur noch
|
|
skaliert.
|
|
|
|
Warum nicht direkt aus der SVG zeichnen? Weil dafuer `svglib` und `lxml` mit in
|
|
die EXE muessten - und bis 512 Pixel ist das Ergebnis pixelgenau dasselbe. Wird
|
|
das Wasserzeichen je groesser gebraucht, gehoert das umgestellt.
|
|
|
|
Warum ueberhaupt eine ICO? Weil Windows das Symbol einer EXE als Ressource in
|
|
der Datei erwartet, und dieses Format ist ICO. Das gilt fuer jede Sprache, nicht
|
|
nur fuer Python - eine SVG kann Windows dort nicht verwenden.
|
|
|
|
Diese Datei liegt in BEIDEN Programmen gleich. Wer sie aendert, kopiert sie
|
|
hinueber.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tkinter as tk
|
|
|
|
_lager = {} # (fenster, groesse, deckkraft) -> PhotoImage
|
|
|
|
|
|
def _ordner():
|
|
"""Alle Orte, an denen die Bilder liegen koennen - Quelle wie EXE."""
|
|
hier = os.path.dirname(os.path.abspath(__file__))
|
|
aus_exe = getattr(sys, "_MEIPASS", "")
|
|
neben_exe = os.path.dirname(os.path.abspath(sys.executable)) \
|
|
if getattr(sys, "frozen", False) else ""
|
|
return [p for p in (aus_exe, neben_exe, hier) if p]
|
|
|
|
|
|
def pfad(name):
|
|
"""Voller Pfad zu einer Datei in assets/ - oder ''."""
|
|
for basis in _ordner():
|
|
p = os.path.join(basis, "assets", name)
|
|
if os.path.exists(p):
|
|
return p
|
|
return ""
|
|
|
|
|
|
def kachel(bild, kante, grund=(0xFF, 0xFF, 0xFF), rand=0.16, radius=0.22):
|
|
"""Das Logo auf eine helle Kachel mit runden Ecken setzen.
|
|
|
|
Seine Ansage am 07.09.2026: *"bei den windows icons kannst du einen
|
|
weissen bg setzen hinter die icons, mit abgerundeten ecken, und da das svg
|
|
drauf - das geht auf dem desktop unter"*.
|
|
|
|
Ein freigestelltes Zeichen ohne Flaeche verschwindet auf einem dunklen
|
|
Hintergrundbild - und welchen Hintergrund jemand eingestellt hat, weiss
|
|
das Programm nicht. Eine helle Kachel ist auf jedem sichtbar; so machen es
|
|
die meisten Programme, und deshalb sieht es auch nicht fremd aus.
|
|
|
|
`rand` ist der Abstand zum Kachelrand, `radius` die Eckenrundung - beides
|
|
als Anteil der Kantenlaenge, damit es in jeder Groesse gleich aussieht.
|
|
Die 22 % entsprechen etwa dem, was Windows fuer Kacheln verwendet.
|
|
"""
|
|
from PIL import Image, ImageDraw
|
|
kante = int(kante)
|
|
# In vierfacher Groesse zeichnen und herunterrechnen: sonst werden die
|
|
# runden Ecken bei 32 oder 48 Pixeln treppig.
|
|
fein = kante * 4
|
|
maske = Image.new("L", (fein, fein), 0)
|
|
ImageDraw.Draw(maske).rounded_rectangle(
|
|
(0, 0, fein - 1, fein - 1), radius=int(fein * radius), fill=255)
|
|
flaeche = Image.new("RGBA", (fein, fein), tuple(grund) + (255,))
|
|
flaeche.putalpha(maske)
|
|
|
|
innen = max(1, int(fein * (1 - 2 * rand)))
|
|
zeichen = bild.convert("RGBA").resize((innen, innen), Image.LANCZOS)
|
|
versatz = (fein - innen) // 2
|
|
flaeche.paste(zeichen, (versatz, versatz), zeichen)
|
|
return flaeche.resize((kante, kante), Image.LANCZOS)
|
|
|
|
|
|
# Groessen, die eine Windows-ICO enthalten soll. 16 und 24 fuer Titelleiste
|
|
# und Listen, 32/48 fuer den Desktop, 256 fuer die grosse Ansicht im Explorer.
|
|
ICO_GROESSEN = (16, 24, 32, 48, 64, 128, 256)
|
|
|
|
|
|
def symbol_bauen(ziel):
|
|
"""Die Windows-ICO aus dem Logo erzeugen. Gibt den Pfad zurueck oder ''.
|
|
|
|
JEDE Groesse wird als Kachel gebaut: helle Flaeche, runde Ecken, Zeichen
|
|
darauf - *"so wie das rustdesk, nur halt alle ecken in der rundung"*. Ein
|
|
freigestelltes Logo verschwindet sonst auf einem dunklen
|
|
Desktop-Hintergrund.
|
|
|
|
Auch die 16 Pixel der Titelleiste: dort ist die Kachel zwar winzig, aber
|
|
sichtbar. Ein freigestelltes Zeichen war dort je nach Darstellung mal da
|
|
und mal nicht - und ein Symbol, das manchmal fehlt, ist schlechter als
|
|
eines, das immer klein ist.
|
|
|
|
Bei den kleinsten Groessen wird der Rand knapper gefasst: bliebe der
|
|
Abstand anteilig gleich, waere vom Zeichen bei 16 Pixeln fast nichts mehr
|
|
uebrig.
|
|
"""
|
|
quelle = pfad("programm_logo.png")
|
|
if not quelle:
|
|
return ""
|
|
try:
|
|
from PIL import Image
|
|
roh = Image.open(quelle).convert("RGBA")
|
|
bilder = []
|
|
for g in ICO_GROESSEN:
|
|
bilder.append(kachel(roh, g, rand=0.10 if g <= 24 else 0.16))
|
|
# Pillow schreibt die uebrigen Groessen selbst aus dem groessten Bild -
|
|
# deshalb wird das groesste uebergeben und der Rest angehaengt.
|
|
bilder[-1].save(ziel, format="ICO",
|
|
sizes=[(g, g) for g in ICO_GROESSEN],
|
|
append_images=bilder[:-1])
|
|
return ziel
|
|
except Exception as e: # noqa: BLE001 - ohne eigenes Symbol laeuft es weiter
|
|
print("[symbol] konnte nicht gebaut werden:", e)
|
|
return ""
|
|
|
|
|
|
def symbol_setzen(fenster, melden=print, dunkel=False):
|
|
"""Das Programmsymbol auf ein Fenster legen. True bei Erfolg.
|
|
|
|
Die EXE traegt es ohnehin - das Fenster aber nicht von selbst. Ohne das
|
|
steht in der Titelleiste das nackte Tk-Symbol, obwohl die Datei ein eigenes
|
|
hat.
|
|
|
|
`dunkel` wird nicht mehr gebraucht und nur noch entgegengenommen, damit
|
|
bestehende Aufrufe weiterlaufen: seit das Symbol eine helle Kachel ist, ist
|
|
es in jeder Darstellung sichtbar. Vorher gab es dafuer eine zweite,
|
|
aufgehellte Datei - die ist mit der Kachel ueberfluessig geworden.
|
|
"""
|
|
p = pfad("programm.ico")
|
|
if not p:
|
|
melden("[symbol] assets/programm.ico nicht gefunden")
|
|
return False
|
|
try:
|
|
fenster.iconbitmap(default=p)
|
|
return True
|
|
except Exception as e: # noqa: BLE001 - ohne Symbol laeuft es weiter
|
|
melden(f"[symbol] konnte nicht gesetzt werden: {e}")
|
|
return False
|
|
|
|
|
|
# Die Farben des Logos - gemessen, nicht geraten:
|
|
# #004860 das dunkle Blaugruen (Wolke, Pfeile)
|
|
# #181818 das fast schwarze Grau (Dokument bzw. Paragraphenzeichen)
|
|
#
|
|
# Seine Ansage am 07.09.2026: *"das dunkel blaue machen wir in unserm tuerkis
|
|
# gruen und die andere seite in weiss ... wenn es dunkel ist, und beim hellen
|
|
# machen wir weiss in dunkel grau"*. Das Zeichen traegt damit dieselbe Farbe
|
|
# wie das Programm - und die zweite Haelfte steht in beiden Darstellungen im
|
|
# Kontrast statt unterzugehen.
|
|
TUERKIS = (0x06, 0xC6, 0xA4) # die Programmfarbe, wie col_accent
|
|
NEUTRAL_DUNKEL = (0xF2, 0xF3, 0xF5) # fast weiss - auf dunklem Grund
|
|
NEUTRAL_HELL = (0x33, 0x36, 0x3B) # dunkelgrau - auf hellem Grund
|
|
|
|
|
|
def _umfaerben(bild, dunkel):
|
|
"""Das Logo auf die Programmfarben umsetzen.
|
|
|
|
Unterschieden wird nach SAETTIGUNG, nicht nach dem genauen Farbwert: das
|
|
Blaugruen ist kraeftig gesaettigt, das Grau gar nicht. So bleibt die
|
|
Zuordnung auch dann richtig, wenn die Vorlage einmal in einem anderen Ton
|
|
gezeichnet wird - und die weichen Kanten aus dem Skalieren gehen denselben
|
|
Weg wie die Flaeche, zu der sie gehoeren.
|
|
|
|
Die Deckkraft bleibt unangetastet; die Form kommt aus dem Alphakanal.
|
|
"""
|
|
import colorsys
|
|
bild = bild.convert("RGBA")
|
|
neutral = NEUTRAL_DUNKEL if dunkel else NEUTRAL_HELL
|
|
punkte = list(bild.getdata())
|
|
zwischen = {}
|
|
neu = []
|
|
for r, g, b, a in punkte:
|
|
if a == 0:
|
|
neu.append((r, g, b, a))
|
|
continue
|
|
schluessel = (r, g, b)
|
|
if schluessel not in zwischen:
|
|
_, _, saettigung = colorsys.rgb_to_hls(r / 255, g / 255, b / 255)
|
|
zwischen[schluessel] = TUERKIS if saettigung > 0.25 else neutral
|
|
neu.append(zwischen[schluessel] + (a,))
|
|
bild.putdata(neu)
|
|
return bild
|
|
|
|
|
|
def hole(groesse=240, deckkraft=0.06, master=None, melden=print, dunkel=False):
|
|
"""Das Logo blass als PhotoImage - oder None.
|
|
|
|
`deckkraft` 0..1, klein halten. Ein Wasserzeichen, das man LIEST, stoert
|
|
beim Arbeiten; eines, das man ahnt, gehoert zum Programm.
|
|
|
|
`master` MUSS mitgegeben werden, wenn mehr als ein Tk-Fenster laeuft:
|
|
sonst gehoert das Bild dem falschen Fenster und Tk meldet spaeter, es
|
|
kenne es nicht.
|
|
"""
|
|
fenster = master if master is not None else tk._default_root
|
|
if fenster is None:
|
|
return None
|
|
schluessel = (id(fenster), groesse, round(deckkraft, 3), bool(dunkel))
|
|
if schluessel in _lager:
|
|
return _lager[schluessel]
|
|
p = pfad("programm_logo.png")
|
|
if not p:
|
|
melden("[wasserzeichen] assets/programm_logo.png nicht gefunden")
|
|
return None
|
|
try:
|
|
from PIL import Image, ImageTk
|
|
bild = Image.open(p).convert("RGBA").resize((groesse, groesse), Image.LANCZOS)
|
|
bild = _umfaerben(bild, dunkel)
|
|
bild.putalpha(bild.getchannel("A").point(lambda w: int(w * deckkraft)))
|
|
fertig = ImageTk.PhotoImage(bild, master=fenster)
|
|
except Exception as e: # noqa: BLE001 - ein Wasserzeichen darf nichts blockieren
|
|
melden(f"[wasserzeichen] nicht ladbar: {e}")
|
|
return None
|
|
_lager[schluessel] = fertig
|
|
return fertig
|
|
|
|
|
|
def kopf(hoehe=28, master=None, melden=print, dunkel=False):
|
|
"""Das Logo fuer die Kopfzeile - volle Deckkraft, kleine Hoehe.
|
|
|
|
Getrennt von hole(): dort geht es um ein blasses Wasserzeichen, hier um ein
|
|
Erkennungszeichen. Zwei Zwecke, zwei Funktionen - sonst haette eine davon
|
|
einen Schalter, den man beim Lesen nachschlagen muss.
|
|
"""
|
|
return hole(hoehe, 1.0, master=master, melden=melden, dunkel=dunkel)
|