Sein Befund
"die namen und datums bloecke koennen schmaler werden sind zu breit und
verdrengen die posten block"
Gemessen stimmte das genau: die sieben Spalten der Leistungstabelle
brauchten 743 Pixel, sichtbar waren 618 - "E.-Preis" und "Gesamt" standen
ausserhalb des Bildes.
Wo die Breite wirklich herkam
An den Feldbreiten zu drehen half NICHTS. Die Arbeitsflaeche verteilte
ueber eine uniform-Gruppe streng im Verhaeltnis 4:3; die Formularspalte war
so breit, weil das Gewicht es vorschrieb, nicht weil der Inhalt es
brauchte - 433 Pixel, ob die Felder nun 38 oder 22 Zeichen breit waren.
Jetzt bekommt die Formularseite eine feste Mindestbreite und kein Gewicht,
die Liste allen uebrigen Platz. Wird das Fenster groesser, waechst allein
die Tabelle - genau dort nuetzt Breite, waehrend ein 600 Pixel breites Feld
fuer eine Postleitzahl nichts besser macht. 433 -> 329.
Dazu passend
- Startbreite 1060 -> 1180 und Mindestbreite 900 -> 1080. Unterhalb von
1080 passen die sieben Spalten selbst dann nicht nebeneinander, wenn die
Textspalte auf ihrer Mindestbreite steht.
- Die Eingabefelder links auf das Mass gekuerzt, das sie wirklich brauchen
(22 statt 38 Zeichen; Datum 12 statt 18). Das senkt die Mindestbreite der
Formularseite - erst dadurch ist sie so klein moeglich.
- "Anreise (TT.MM.JJJJ)" -> "Anreise". Die Form steht jetzt einmal als
graue Zeile unter dem Block. Als Platzhalter IM Feld waere sie
gefaehrlich: ein nicht geloeschter Platzhalter stuende als Anreisedatum
auf der Rechnung.
- Der Textumbruch der Leistungsspalte folgt jetzt ihrer echten Breite. Fest
eingetragen schnitt er den Text ab, sobald die Spalte schmaler wurde.
pruef_spaltenbreiten.py
Prueft ueber die ganze erlaubte Spanne, dass alle Spalten hineinpassen, die
Formularseite lesbar bleibt und NICHT mitwaechst, und der Umbruch zur
Spalte passt. Kein anderer Pruefstand haette das gefunden: es stuerzt
nichts ab und rechnet nichts falsch - man sieht die Preise nur nicht.
Er hat sofort einen zweiten Fehler gefunden: beim Aendern der
Fenstergroesse lief der Umbruch nicht hinterher, weil ich abgebrochen
hatte, sobald sich mein eigener Wert nicht mehr aenderte. Die Spalte kann
aber breiter werden, ohne dass ich etwas aendere. Abbruch jetzt, wenn zwei
Messungen dieselbe Spaltenbreite ergeben.
Fassung 1.7.1.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V4t48uxDok5rJXhC9bX1Ax
308 lines
13 KiB
Python
308 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""Arbeitsflaeche: die Kaesten des Fensters lassen sich verschieben.
|
|
|
|
Seine Ansage: *"rechnungsdaten oben links und darunter rechnung an kunde und den
|
|
tabellen kasten rechts - oder noch besser dnd einbauen, das man die kaesten
|
|
selber schieben kann"*. Beides ist hier drin: die genannte Anordnung ist die
|
|
Vorgabe, und wer sie anders haben will, zieht die Kaesten um.
|
|
|
|
WIE ES FUNKTIONIERT
|
|
-------------------
|
|
Wie bei OPNsense: normalerweise sitzt alles fest. Ein kleiner Knopf oben rechts
|
|
schaltet das ANORDNEN ein - dann zeigen die Kaesten einen Griff, lassen sich
|
|
ziehen, und daneben stehen "Speichern" und "Verwerfen". Erst mit Speichern
|
|
steht die neue Anordnung; Verwerfen holt die alte zurueck.
|
|
|
|
So kann beim taeglichen Arbeiten nichts verrutschen - man muss das Verschieben
|
|
erst absichtlich einschalten.
|
|
|
|
Die Flaeche ist ein Raster aus zwei Spalten. Jeder Kasten sitzt in einer Zelle
|
|
und kann sich ueber beide Zeilen strecken. Gezogen wird am KOPF des Kastens;
|
|
der Kasten unter dem Zeiger wird hervorgehoben, beim Loslassen tauschen die
|
|
beiden ihre Plaetze.
|
|
|
|
Getauscht wird, nicht "irgendwo abgelegt": ein freies Verschieben auf Pixel
|
|
braucht Regeln fuer Ueberlappung, Groesse und leere Zellen - und am Ende steht
|
|
das Fenster schief. Ein Tausch ist in einem Griff erledigt und kann nichts
|
|
kaputt machen.
|
|
|
|
Die Anordnung steht in der config (`fenster_layout`) und ueberlebt den Neustart.
|
|
"""
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
|
|
import config
|
|
|
|
# Vorgabe: seine Anordnung. Je Kasten (Spalte, Zeile, Zeilen hoch).
|
|
STANDARD = {
|
|
"daten": (0, 0, 1),
|
|
"kunde": (0, 1, 1),
|
|
"leistungen": (1, 0, 2),
|
|
}
|
|
|
|
|
|
class Arbeitsflaeche(ttk.Frame):
|
|
# Wie breit die beiden Spalten mindestens sein muessen. Die Formularseite
|
|
# ist gemessen, nicht geschaetzt: das breiteste, was dort steht, ist die
|
|
# Beschriftung "Nachname / Firma *" neben einem 22 Zeichen breiten Feld.
|
|
MIN_FORMULAR = 330
|
|
MIN_LISTE = 520
|
|
|
|
def __init__(self, master, farben=None, beim_umbauen=None):
|
|
super().__init__(master)
|
|
self.f = farben if farben is not None else master
|
|
self.beim_umbauen = beim_umbauen # wird nach jedem Tausch gerufen
|
|
self.kaesten = {} # name -> dict(rahmen, kopf, titel)
|
|
self.layout = {}
|
|
self._vorher = {} # Stand vor dem Anordnen
|
|
self.anordnen_an = False # Verschieben eingeschaltet?
|
|
self._zieht = None
|
|
self._ziel = None
|
|
# Die Gewichte setzt `anordnen()` - sie haengen davon ab, WO die lange
|
|
# Liste gerade steht.
|
|
|
|
# ---- Aufbau -----------------------------------------------------------
|
|
def kasten(self, name, titel):
|
|
"""Legt einen Kasten an und gibt seinen Inhaltsrahmen zurueck."""
|
|
aussen = ttk.Frame(self, style="Karte.TFrame")
|
|
kopf = tk.Label(aussen, text=titel.upper(), anchor="w", cursor="fleur",
|
|
bg=self.f.col_card, fg=self.f.col_accent,
|
|
font=(self._schrift(), 9, "bold"), padx=12, pady=8)
|
|
kopf.pack(side="top", fill="x")
|
|
inhalt = ttk.Frame(aussen, style="Karte.TFrame", padding=(12, 0, 12, 12))
|
|
inhalt.pack(side="top", fill="both", expand=True)
|
|
|
|
kopf.bind("<Button-1>", lambda e, n=name: self._greifen(n))
|
|
kopf.bind("<B1-Motion>", lambda e, n=name: self._ziehen(e, n))
|
|
kopf.bind("<ButtonRelease-1>", lambda e, n=name: self._loslassen(n))
|
|
self.kaesten[name] = {"aussen": aussen, "kopf": kopf, "inhalt": inhalt,
|
|
"titel": titel}
|
|
return inhalt
|
|
|
|
def anordnen(self, layout=None):
|
|
"""Kaesten ins Raster setzen. Ohne Angabe: was in der config steht."""
|
|
self.layout = dict(layout or self._gespeichert())
|
|
for name, kasten in self.kaesten.items():
|
|
spalte, zeile, hoch = self.layout.get(name, STANDARD.get(name, (0, 0, 1)))
|
|
kasten["aussen"].grid(row=zeile, column=spalte, rowspan=hoch,
|
|
sticky="nsew", padx=6, pady=6)
|
|
self._platz_verteilen()
|
|
|
|
def _platz_verteilen(self):
|
|
"""Der freie Platz gehoert dem Kasten mit der langen Liste.
|
|
|
|
Feste Gewichte an festen Zellen gehen nicht: die Liste wandert beim
|
|
Verschieben, und wer das Gewicht an einer festen Zelle laesst,
|
|
schneidet danach den Kundenkasten unten ab - genau das ist beim ersten
|
|
Versuch passiert.
|
|
|
|
WARUM NICHT MEHR `uniform` (06.09.2026)
|
|
---------------------------------------
|
|
Vorher standen beide Spalten in einer uniform-Gruppe mit den Gewichten
|
|
4 und 3. Das teilt die Breite STRENG in diesem Verhaeltnis auf - bei
|
|
1040 Pixeln also 594 zu 446, ganz gleich, was in den Kaesten steht.
|
|
Sein Befund: *"die namen und datums bloecke koennen schmaler werden
|
|
sind zu breit und verdrengen die posten block"*. Er hatte recht, aber
|
|
an den Feldbreiten zu drehen half nichts: die Spalte war so breit, weil
|
|
das Gewicht es vorschrieb, nicht weil der Inhalt es brauchte.
|
|
|
|
Jetzt bekommen die Formularkaesten nur, was sie brauchen (feste
|
|
Mindestbreite, kein Gewicht), und der ganze uebrige Platz geht an die
|
|
Liste. Wird das Fenster groesser, waechst allein die Tabelle - genau
|
|
dort nuetzt die Breite, waehrend ein Adressfeld von 600 Pixeln nichts
|
|
besser macht.
|
|
"""
|
|
for z in range(2):
|
|
self.rowconfigure(z, weight=0, minsize=0)
|
|
for sp in range(2):
|
|
self.columnconfigure(sp, weight=0, uniform="", minsize=0)
|
|
lang = self.layout.get("leistungen")
|
|
if not lang:
|
|
for sp in range(2):
|
|
self.columnconfigure(sp, weight=1)
|
|
return
|
|
spalte, zeile, hoch = lang
|
|
for z in range(zeile, zeile + hoch):
|
|
self.rowconfigure(z, weight=1)
|
|
# Die Liste nimmt allen freien Platz ...
|
|
self.columnconfigure(spalte, weight=1, minsize=self.MIN_LISTE)
|
|
# ... die Formularseite genau so viel, wie ihr breitestes Feld braucht.
|
|
self.columnconfigure(1 - spalte, weight=0, minsize=self.MIN_FORMULAR)
|
|
# Steht die Liste nur in EINER Zeile, braucht die andere trotzdem Luft
|
|
# fuer ihren Inhalt - sonst wird sie zusammengequetscht.
|
|
if hoch == 1:
|
|
self.rowconfigure(1 - zeile, weight=1, minsize=170)
|
|
|
|
def zuruecksetzen(self):
|
|
self.anordnen(STANDARD)
|
|
self._speichern()
|
|
self._koepfe_zeigen()
|
|
|
|
# ---- Ziehen -----------------------------------------------------------
|
|
# ---- Anordnen ein- und ausschalten ------------------------------------
|
|
def anordnen_starten(self):
|
|
self._vorher = dict(self.layout)
|
|
self.anordnen_an = True
|
|
self._koepfe_zeigen()
|
|
|
|
def anordnen_speichern(self):
|
|
self.anordnen_an = False
|
|
self._speichern()
|
|
self._koepfe_zeigen()
|
|
|
|
def anordnen_verwerfen(self):
|
|
self.anordnen_an = False
|
|
if self._vorher:
|
|
self.anordnen(self._vorher)
|
|
self._koepfe_zeigen()
|
|
|
|
def _koepfe_zeigen(self):
|
|
"""Im Anordnen-Modus tragen die Koepfe einen Griff und einen Zeiger."""
|
|
import icons
|
|
for kasten in self.kaesten.values():
|
|
kopf = kasten["kopf"]
|
|
if self.anordnen_an:
|
|
bild = icons.hole("drag_indicator", 16, self.f.col_accent, master=self)
|
|
kopf.configure(image=bild, compound="left", cursor="fleur",
|
|
bg=self.f.col_sel)
|
|
kopf.bild = bild # sonst raeumt Python das Bild weg
|
|
else:
|
|
kopf.configure(image="", compound="none", cursor="",
|
|
bg=self.f.col_card)
|
|
kopf.bild = None
|
|
|
|
def _greifen(self, name):
|
|
if not self.anordnen_an:
|
|
return
|
|
self._zieht = name
|
|
self.kaesten[name]["kopf"].configure(bg=self.f.col_sel)
|
|
# Der gegriffene Kasten wird blass - man sieht, WAS man in der Hand hat.
|
|
self.kaesten[name]["aussen"].configure(style="Gegriffen.TFrame")
|
|
self._etikett_zeigen(name)
|
|
|
|
def _ziehen(self, ereignis, name):
|
|
if self._zieht is None or not self.anordnen_an:
|
|
return
|
|
self._etikett_bewegen()
|
|
ziel = self._kasten_unter(ereignis)
|
|
if ziel == self._ziel:
|
|
return
|
|
self._marke(self._ziel, False)
|
|
self._ziel = ziel if ziel != name else None
|
|
self._marke(self._ziel, True)
|
|
|
|
def _loslassen(self, name):
|
|
if self._zieht is None:
|
|
return
|
|
self._etikett_weg()
|
|
self.kaesten[name]["aussen"].configure(style="Karte.TFrame")
|
|
self.kaesten[name]["kopf"].configure(bg=self.f.col_sel)
|
|
self._marke(self._ziel, False)
|
|
ziel, self._zieht, self._ziel = self._ziel, None, None
|
|
if ziel and ziel != name:
|
|
# Getauscht wird sofort sichtbar - gespeichert erst mit dem Knopf.
|
|
self.layout[name], self.layout[ziel] = self.layout[ziel], self.layout[name]
|
|
self.anordnen(self.layout)
|
|
self._koepfe_zeigen()
|
|
if self.beim_umbauen:
|
|
self.beim_umbauen()
|
|
|
|
# ---- Das Etikett am Zeiger -------------------------------------------
|
|
def _etikett_zeigen(self, name):
|
|
"""Ein kleines Schild haengt am Zeiger, solange gezogen wird.
|
|
|
|
Ohne das sieht man nur, WOHIN es geht, aber nicht, dass man ueberhaupt
|
|
etwas in der Hand hat - es wirkte "statisch". Ein Widget selbst kann in
|
|
Tk nicht mitwandern; ein randloses Fensterchen kann es.
|
|
"""
|
|
import icons
|
|
self._etikett_weg()
|
|
try:
|
|
schild = tk.Toplevel(self)
|
|
schild.overrideredirect(True)
|
|
schild.attributes("-topmost", True)
|
|
try:
|
|
schild.attributes("-alpha", 0.85)
|
|
except tk.TclError:
|
|
pass
|
|
rahmen = tk.Frame(schild, bg=self.f.col_accent, padx=1, pady=1)
|
|
rahmen.pack()
|
|
bild = icons.hole("drag_indicator", 16, self.f.col_accent, master=self)
|
|
inhalt = tk.Label(rahmen, text=" " + self.kaesten[name]["titel"],
|
|
image=bild, compound="left",
|
|
bg=self.f.col_card, fg=self.f.col_accent,
|
|
font=(self._schrift(), 10, "bold"), padx=12, pady=8)
|
|
inhalt.bild = bild
|
|
inhalt.pack()
|
|
self._etikett = schild
|
|
self._etikett_bewegen()
|
|
except tk.TclError as e:
|
|
print("[flaeche] Etikett nicht moeglich:", e)
|
|
self._etikett = None
|
|
|
|
def _etikett_bewegen(self):
|
|
schild = getattr(self, "_etikett", None)
|
|
if schild is None:
|
|
return
|
|
try:
|
|
schild.geometry(f"+{self.winfo_pointerx() + 14}+{self.winfo_pointery() + 14}")
|
|
except tk.TclError:
|
|
self._etikett = None
|
|
|
|
def _etikett_weg(self):
|
|
schild = getattr(self, "_etikett", None)
|
|
if schild is not None:
|
|
try:
|
|
schild.destroy()
|
|
except tk.TclError:
|
|
pass
|
|
self._etikett = None
|
|
|
|
def _marke(self, name, an):
|
|
if not name:
|
|
return
|
|
self.kaesten[name]["kopf"].configure(
|
|
fg=self.f.col_accent_hell if an else self.f.col_accent)
|
|
self.kaesten[name]["aussen"].configure(
|
|
style="Ziel.TFrame" if an else "Karte.TFrame")
|
|
|
|
def _kasten_unter(self, ereignis):
|
|
"""Welcher Kasten liegt unter dem Zeiger?"""
|
|
x = self.winfo_pointerx() - self.winfo_rootx()
|
|
y = self.winfo_pointery() - self.winfo_rooty()
|
|
for name, kasten in self.kaesten.items():
|
|
w = kasten["aussen"]
|
|
lx, ly = w.winfo_x(), w.winfo_y()
|
|
if lx <= x <= lx + w.winfo_width() and ly <= y <= ly + w.winfo_height():
|
|
return name
|
|
return None
|
|
|
|
# ---- Merken -----------------------------------------------------------
|
|
def _schrift(self):
|
|
import theme
|
|
return theme.SCHRIFT_NAME
|
|
|
|
def _gespeichert(self):
|
|
cfg = getattr(self.f, "cfg", {}) or {}
|
|
roh = cfg.get("fenster_layout") or {}
|
|
aus = {}
|
|
for name in self.kaesten:
|
|
wert = roh.get(name)
|
|
# Nur uebernehmen, was auch wirklich passt - eine von Hand
|
|
# verstellte config darf das Fenster nicht zerlegen.
|
|
if (isinstance(wert, (list, tuple)) and len(wert) == 3
|
|
and all(isinstance(z, int) for z in wert)):
|
|
aus[name] = tuple(wert)
|
|
else:
|
|
aus[name] = STANDARD.get(name, (0, 0, 1))
|
|
return aus
|
|
|
|
def _speichern(self):
|
|
cfg = getattr(self.f, "cfg", None)
|
|
if cfg is None:
|
|
return
|
|
cfg["fenster_layout"] = {n: list(v) for n, v in self.layout.items()}
|
|
try:
|
|
config.save_config(cfg)
|
|
except OSError as e:
|
|
print("[flaeche] Anordnung nicht gespeichert:", e)
|