Berechnungen in per Funktion erstellten Entryboxen

Fragen zu Tkinter.
Antworten
leoderfranke
User
Beiträge: 1
Registriert: Donnerstag 26. August 2021, 11:23

Moin Moin,

ich hab recht frisch angefangen mir programmieren beizubringen und Python zu lernen und bin auf ein Problem gestoßen das ich nicht so richtig durchdringe.
Es wäre toll wenn sich jemand von euch erbarmen würde mir zu erklären wo mein Fehler liegt bzw. wie ich das lösen könnte.

Es geht darum das ich eine kleine Eingabemaske bauen wollte in die erlaubt Kalkulationen zu erstellen und in einer df zu übergeben.
Um das flexibel zu gestalten wollte ich über einen Button Reihen Entry boxen hinzufügen können.

Aus dem Wert aus der Von und der Bis box sollen die Stunden berechnet werden und in die Stunden Box der jeweiligen Reihe geschrieben werden.
Das funktioniert soweit auch gut solange ich eine Reihe nach der anderen hinzufüge und ausfülle und erst nach dem ausfüllen den Button wieder betätige.

Sobald ich mehr als eine Reihe hinzufüge wird der errechnete Stunden Wert immer in die Stunden box in der letzten Reihe geschrieben.
Das soll so nicht sein, aber ich hab keine Idee mehr wie ich das verhindern kann.

Kann mir jemand sagen wo mein Fehler liegt, wie ich das vermeide ?

Der Code dazu:

from tkinter import *
from tkinter import ttk
from datetime import datetime
import pandas as pd

root = Tk()
root.title("Boxen Hinzufügen")
root.geometry("900x300")
meine_eingaben = []
meine_liste = []
rowcount = 2

# Labels erstellen
def create_header():
Kunde_label = Label(root, text="Kunde:")
Kunde_label.grid(row=1, column=0, sticky=W, )
anzahl_label = Label(root, text="Anzahl")
anzahl_label.grid(row=2, column=0, sticky=W, )
artikel_label = Label(root, text="Artikel")
artikel_label.grid(row=2, column=1, sticky=W, padx=10)
von_label = Label(root, text="Von")
von_label.grid(row=2, column=2, sticky=W, padx=10)
bis_label = Label(root, text="Bis")
bis_label.grid(row=2, column=3, sticky=W, padx=10)
stunden_label = Label(root, text="h")
stunden_label.grid(row=2, column=4, sticky=W, padx=10)
min_label = Label(root, text="Min. h")
min_label.grid(row=2, column=5, sticky=W, padx=10)
stunden_preis_label = Label(root, text="preis/h")
stunden_preis_label.grid(row=2, column=6, sticky=W, padx=10)
zeilen_summe_label = Label(root, text="Summe")
zeilen_summe_label.grid(row=2, column=7, sticky=W, padx=10)

#Funktion zum Speichern der Werte in einen DataFrame
def save():
x=0
for eintrag in meine_eingaben:
meine_liste.insert(x, eintrag.get())
x+=1
n = 8
output = [meine_liste[i:i + n] for i in range(0, len(meine_liste), n)]
df = pd.DataFrame(output, columns=['Anzahl', 'Artikel', 'von', 'Bis', 'h', 'min.h', 'preis/h', 'Summe'])
print(df.to_string(index=False))

#Funktion zum erstellen der Boxen
def create():

#Funktion zu berechnung der Arbeitsstunden
def stunden_berchnung(event):
stunden_box.delete(0, END)
bis = datetime.strptime(bis_box.get(), "%H:%M")
von = datetime.strptime(von_box.get(), "%H:%M")

if bis >= von:
stunden = bis - von
zerlegt1 = str(stunden).split(":")
hh = zerlegt1[0]
mm = zerlegt1[1]
ergebnis = float(hh) + float(mm) / 60
stunden_box.insert(0, str(ergebnis))

else:
stunden = bis - von
zerlegt1 = str(stunden).split(",")
zerlegt2 = str(zerlegt1[1]).split(":")
hh = zerlegt2[0]
mm = zerlegt2[1]

ergebnis = float(hh) + float(mm) / 60
stunden_box.insert(0, str(ergebnis))

global rowcount
anzahl_box = Entry(root, width=5)
anzahl_box.grid(row=rowcount+1, column=0, sticky=W, )
meine_eingaben.append(anzahl_box)

global artikel_drop
# Dropbox
artikel_drop = ttk.Combobox(root, value=["auswahl_1", "auswahl_2", "auswahl_3"])
artikel_drop.grid(row=rowcount+1, column=1)
meine_eingaben.append(artikel_drop)

von_box = Entry(root, width=5)
von_box.grid(row=rowcount+1, column=2, sticky=W, padx=10, pady=10)
meine_eingaben.append(von_box)

bis_box = Entry(root, width=5)
bis_box.grid(row=rowcount+1, column=3, sticky=W, padx=10, pady=10)
meine_eingaben.append(bis_box)

global stunden_box
stunden_box = Entry(root, width=5)
stunden_box.grid(row=rowcount+1, column=4, sticky=W, padx=10, pady=10)
stunden_box.bind("<FocusIn>",stunden_berchnung)
meine_eingaben.append(stunden_box)

global min_box
min_box = Entry(root, width=5)
min_box.grid(row=rowcount+1, column=5, sticky=W, padx=10, pady=10)
meine_eingaben.append(min_box)

global stunden_preis_box
stunden_preis_box = Entry(root, width=5)
stunden_preis_box.grid(row=rowcount+1, column=6, sticky=W, padx=10, pady=10)
meine_eingaben.append(stunden_preis_box)

global zeilen_summe_box
zeilen_summe_box = Entry(root, width=5)
zeilen_summe_box.grid(row=rowcount+1, column=7, sticky=W, padx=10, pady=10)
meine_eingaben.append(zeilen_summe_box)

rowcount += 1


create_header()

btn_add = Button(root, text="Add", command=create)
btn_add.grid(row=0,column=0)
btn_save = Button(root, text="Save", command=save)
btn_save.grid(row=0, column=1)



root.mainloop()
Benutzeravatar
__blackjack__
User
Beiträge: 13064
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@leoderfranke: Das liegt am ``global`` das dafür sorgt, dass es nur eine globale `stunden_box` gibt, das heisst die lokalen Funktionen die Du für das <FocusIn>-Ereignis erstellst, greifen alle auf dieses eine, globale `stunden_box` zu, und das hat immer als Wert das zuletzt erstellte `Entry`.

``global`` solltest Du sowieso schnell wieder vergessen. Das hat in einem sauberen Programm nichts zu suchen. Bei GUI-Programmierung kommt man bei jeder nicht-trivialen GUI nicht wirklich um objektorientierte Programmierung (OOP) herum. Also mindestens eine eigene Klasse. Und wenn man Klassen verwendet, dann gibt es auch gar keine Ausrede mehr für ``global``.

Weitere Anmerkungen:

Sternchen-Importe sind Böse™. Da holt man sich gerade bei `tkinter` fast 200 Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Auch Namen die gar nicht in `tkinter` definiert werden, sondern ihrerseits von woanders importiert werden. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Das bedeutet dann auch keine globalen Variablen und das Funktionen und Methoden alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. In sehr einfachen Fällen kann man sich da für Rückruffunktionen noch mit `functools.partial()` behelfen, aber wie schon gesagt: im Allgemeinen braucht man für GUI-Programmierung OOP.

`rowcount` wird an einer Stelle im Programm fest mit einer magischen 2 initialisiert, wo nicht sofort klar wird warum das 2 ist. Und man muss daran denken das anzupassen wenn man an einer ganz anderen Stelle im Programm Veränderungen an der GUI vornimmt. Am besten fängt man bei 0 an und erhöht das auch für den statischen Teil jeweils dort, wo neue Zeilen in dem Grid-Layout erstellt werden. So kann man an beliebiger Stelle Änderungen vornehmen, ohne an einer festen Stelle eine magische Zahl anfassen zu müssen. Ein `itertools.count()` würde sich da anbieten um die Zeilenindexwerte zu erzeugen.

Ähnlich ungünstig ist die magische 8 für die Länge eines Datensatzes. Die existiert hauptsächlich weil die Datenstruktur falsch ist. Statt alles in eine eindimensionale Liste zu stecken, würde man hier auch schon eine Liste mit Listen von Eingabefeldern verwenden, wo alle Eingabefelder für einen Datensatz in einer Liste zusammengefasst sind, statt das hinterher nachträglich umzubasteln.

Man muss nicht alles an einen Namen binden. Die ganzen lokalen Namen für die Label im Kopfbereich kann man sich sparen. Da wird auch eine Menge Code wiederholt. Die Label unterscheiden sich ja eigentlich nur im Text. Man könnte da also eine Schleife über die Texte schreiben und den Code zum Label erstellen nur *einmal* in diese Schleife schreiben. Ähnliches gilt für die Erstellung der Eingabefelder.

Kommentare sollen dem Leser einen Mehrwert über den Code geben. Faustregel: Kommentare beschreiben nicht *was* der Code macht, denn das steht da bereits als Code, sondern warum er das macht. Sofern das nicht offensichtlich ist. Offensichtlich ist in aller Regel auch was in der Dokumentation von Python und den verwendeten Bibliotheken steht.

In Beschreibungen von Funktionen in Kommentaren bzw. Docstrings muss man nicht noch mal schreiben, dass es sich um eine Funktion handelt. Das wird schon aus dem Code klar. Der Kommentar für die Stundenberechnung ist überflüssig, weil man dessen Informationsgehalt am Funktionsnamen ablesen kann. Der bei erstellen der Eingabefelder wäre überflüssig wenn der Funktionsname nicht nur `create()` wäre, was beim Leser ja sofort die Frage aufwirft *was* da erzeugt wird. Das sollte im Funktionsnamen stehen.

Funktionen sollte man nicht verschachteln. Auch wenn die Berechnung/Aktualisierung der Stunden hier technisch gesehen als Closure sogar Sinn machen würde, ist das sehr unübersichtlich bei der Anzahl der lokalen Namen der umgebenden Funktion, so dass man nicht leicht sehen kann, wie die Schnittstelle der verschachtelten Funktion eigentlich aussieht. Und man kann verschachtelte Funktionen auch nicht einfach einzeln Testen. Und diese Funktion macht ja durchaus etwas komplexeres mit der Berechnung vom Stundenwert.

Die Vorsilbe `meine_*` ist bedeutungslos, die kann weg. `liste` ist zu nichtssagend. Der Leser will nicht wissen, dass das eine Liste ist, sondern was die Werte darin bedeuten. Diese Liste sollte aber sowieso nicht ausserhalb der Speicherfunktion existieren. Denn momentan wird die bei jedem mal speichern länger, weil jedes mal die bisherigen Daten *noch mal* an diese Liste angehängt werden. Das macht keinen Sinn.

Beim berechnen der Stunden wird ein `timedelta`-Objekt erst in eine Zeichenkette zerlegt, um dann daraus wieder Zahlendarstellungen raus zu popeln und mit denen dann wieder zu rechnen: das macht man nicht. Die Zahlen existieren bereits als Attribute. Und am besten rechnet man einfach mit der Gesamtzahl der Sekunden in dem Objekt.

Zwischenstand:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from datetime import datetime as DateTime
from functools import partial
from itertools import count
from tkinter import ttk
from collections import namedtuple

import pandas as pd

RecordEntries = namedtuple(
    "RecordEntries", "anzahl artikel von bis stunden min stundenpreis summe"
)


def create_header(master, row_indices):
    row_index = next(row_indices)
    tk.Label(master, text="Kunde:").grid(row=row_index, column=0, sticky=tk.W)

    row_index = next(row_indices)
    for column_index, text in enumerate(
        ["Anzahl", "Artikel", "Von", "Bis", "h", "Min. h", "Preis/h", "Summe"]
    ):
        tk.Label(master, text=text).grid(
            row=row_index, column=column_index, sticky=tk.W, padx=10
        )


def aktualisiere_stunden(von_entry, bis_entry, stunden_entry, _event=None):
    stunden_entry.delete(0, tk.END)
    bis = DateTime.strptime(bis_entry.get(), "%H:%M")
    von = DateTime.strptime(von_entry.get(), "%H:%M")
    stunden = (bis - von).total_seconds() / (60 * 60)
    if stunden < 0:
        stunden += 24
    assert stunden >= 0
    stunden_entry.insert(0, stunden)


def create_entry_row(master, row_indices, eingabefelder):
    record_entries = RecordEntries(
        tk.Entry(master, width=5),
        ttk.Combobox(master, value=["auswahl_1", "auswahl_2", "auswahl_3"]),
        tk.Entry(master, width=5),
        tk.Entry(master, width=5),
        tk.Entry(master, width=5),
        tk.Entry(master, width=5),
        tk.Entry(master, width=5),
        tk.Entry(master, width=5),
    )

    record_entries.stunden.bind(
        "<FocusIn>",
        partial(
            aktualisiere_stunden,
            record_entries.von,
            record_entries.bis,
            record_entries.stunden,
        ),
    )

    row_index = next(row_indices)
    for column_index, entry in enumerate(record_entries):
        entry.grid(
            row=row_index, column=column_index, sticky=tk.W, padx=10, pady=10
        )

    eingabefelder.append(record_entries)


def save(eingabefelder):
    print(
        pd.DataFrame(
            [[entry.get() for entry in row] for row in eingabefelder],
            columns=[
                "Anzahl",
                "Artikel",
                "von",
                "bis",
                "h",
                "min.h",
                "preis/h",
                "Summe",
            ],
        ).to_string(index=False)
    )


def main():
    eingabefelder = []

    root = tk.Tk()
    root.title("Boxen Hinzufügen")

    row_indices = count()

    row_index = next(row_indices)
    tk.Button(
        root,
        text="Add",
        command=partial(create_entry_row, root, row_indices, eingabefelder),
    ).grid(row=row_index, column=0)

    tk.Button(root, text="Save", command=partial(save, eingabefelder)).grid(
        row=row_index, column=1
    )

    create_header(root, row_indices)

    root.mainloop()


if __name__ == "__main__":
    main()
Das Speichern macht so keinen Sinn, weil da alles als Zeichenketten im Dataframe landet, dafür sind Dataframes aber nicht gedacht, dass man dort Zahlenwerte als Zeichenketten speichert.

Die Stundenspalte sollte man nicht speichern. Das ist redundant, weil man die ja aus den anderen Werten ausrechnen kann. Und man kann momentan auch falsche Werte speichern, weil der Stundenwert nur neu berechnet wird, wenn das entsprechende Eingabefeld den Fokus erhält, und nicht wenn man einen Wert ändert, der zum Stundenwert beiträgt. Das gleich wird wahrscheinlich für die „Summe“-Spalte gelten.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten