Sein Befund: "hast du bei dem windows icon das gleich farbige wie im programm, wo als wasserzeichen gilt, gemacht - weil das schaut blau aus". Er hatte recht: auf der Kachel stand das ROHE Logo mit seinem Petrol #004860, waehrend im Programm (Kopfzeile, Wasserzeichen) laengst die umgefaerbte Fassung laeuft. Zwei Farben fuer dasselbe Zeichen. Jetzt geht das Logo auch fuers Symbol durch _umfaerben() - dieselbe Funktion, dieselben Farben: Tuerkis fuer die Wolke, Dunkelgrau fuer das Zeichen daneben. Die Kachel ist weiss, also gilt die HELLE Fassung. Fassung 1.9.1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
237 lines
9.7 KiB
Python
237 lines
9.7 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")
|
|
# Dieselben Farben wie IM Programm: Tuerkis fuer die Wolke, Dunkelgrau
|
|
# fuer das Zeichen daneben. Vorher stand hier das rohe Logo mit seinem
|
|
# Petrol #004860 - sein Befund am 07.09.2026: "das schaut blau aus".
|
|
# Die Kachel ist weiss, also gilt die HELLE Fassung (dunkel=False).
|
|
zeichen = _umfaerben(roh, dunkel=False)
|
|
bilder = []
|
|
for g in ICO_GROESSEN:
|
|
bilder.append(kachel(zeichen, 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)
|