Seine Ansagen
"gibt es noch das popup install ordner auswaehlen, das stimmt ja nicht"
"wie soll das rechnungstool und steuertool wissen wo es ist, das muss in
ravokk liegen und dann zusammen, sonst kommt das eine nicht an das andere
ran - die sind doch nur ueber die gemeinsame sqlite verbunden und mit api"
"es gibt ravokk als main verzeichnis, da liegt config und sqlite drin, und
dann gibt es den ordner rechnungstool und steuertool"
"ravokk ist meine firma und die programme gehoeren zu der firma"
"du solltest die auch mal deinstallieren und neu installieren ... dass der
ordner dann leer ist und sauber - will keine tools die unsauber sind"
Die Struktur
%LOCALAPPDATA%/ravokk/
config.json, journal.sqlite3, Nummernbuch, berichte/ <- Daten
Rechnungstool/ <- Programm
Steuerrechnungstool/ <- Programm
Die Daten liegen NEBEN dem Programm, nicht darin. Nur dadurch koennen
beide Forderungen gleichzeitig gelten: der Programmordner wird beim
Deinstallieren restlos geleert, und die Belege bleiben zehn Jahre stehen
(§ 147 AO).
Kein Ordner-Dialog mehr beim ersten Start
gemeinsam.standard_ordner() nennt den festen Ort, beide Programme legen
ihn selbst an. Gefragt wird nur noch, wer ihn ausdruecklich verschieben
will (Menue -> Gemeinsamer Ordner) - etwa auf ein Netzlaufwerk.
Der Knopf hiess "Install-Ordner auswaehlen..." und der Text riet zum
"Ordner, in dem die Programme liegen". Beides war falsch und seit dem
Installer erst recht: die beiden liegen in getrennten Unterordnern, und
ein Deinstallieren haette das Nummernbuch mitgenommen.
⚠ Dabei gefunden: das Journal lag im TEMP-Ordner
Der Merkzettel der installierten Fassung zeigte auf
%TEMP%/abnahme_bst/Daten - haengengeblieben aus einem Abnahmelauf vom
03.09. Windows raeumt den Temp-Ordner automatisch auf; das Journal waere
ohne Vorwarnung verschwunden. ablage.py verwirft einen solchen Merkzettel
jetzt, statt ihm zu folgen.
Uninstaller
Er raeumt jetzt auch, was der UPDATER angelegt hat - jede geladene Fassung
und installiert.json. Die kannte der Installer nicht; mit jeder Fassung
waere eine EXE mehr liegen geblieben. Gemessen vorher: zwei EXEs und der
halbe Datenordner.
pruef_installation.py
Installiert wirklich, legt eine Datei an wie der Updater es taete,
deinstalliert und sieht nach: Programmordner leer, Daten unangetastet.
Vorhandene Daten werden vorher gesichert und danach zurueckgelegt - ein
Pruefstand fasst niemandem seine Buchhaltung an.
pruef_gemeinsam_automatisch.py
Prueft, dass beide Programme denselben Ordner nennen und dass findet, was
das andere eintraegt - ohne dass jemand einen Pfad eingegeben hat.
Symbol
Das Windows-Symbol ist jetzt eine helle Kachel mit runden Ecken und dem
Zeichen darauf, in allen Groessen von 16 bis 256 - "so wie das rustdesk,
nur halt alle ecken in der rundung". Ein freigestelltes Logo ging auf
einem dunklen Desktop unter. Die zweite, aufgehellte ICO ist damit
ueberfluessig; das aufgehellte Logo IM Programm (Kopfzeile, Wasserzeichen)
bleibt unveraendert.
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)
|