Verschachtelte Buttons

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
c.schroeder
User
Beiträge: 11
Registriert: Montag 18. Oktober 2021, 15:22

Hi,
ich habe ein Programm geschrieben, das eine Zahl einlesen soll und durch das Betätigen eines Buttons eine weitere Eingabe freigeben soll (das funktioniert soweit). Nur leider kann die zweite Zahl nicht richtig eingelesen werden. Es wird nicht erkannt, dass eine Zahl eingegeben wurde und die Ausgabe "Es muss etwas eingegeben werden!" wird angezeigt, als hätte man nichts eingegeben.

Mein Gedanke war, dass die Funktion "def button_action" nichts in der globalen Variable "eingang_text2" speichern kann, da der Wert erst durch das Bestätigen des Buttons feststeht und man vor dem Speichern direkt in die nächste Funktion rutscht.

Ein anderer Gedanke war, den gleichen Weg, wie bei der ersten Funktion "def button_anzahl", zu gehen. Ich dachte man könnte mit der Funktion "eingabefeld2.get()" (als Kommentar zu Beginn bei "def button_action2" zu sehen) auf das zweite Eingabefeld zugreifen, was nicht möglich ist, da es sich in der Funktion "def button_action" befindet.
Kann man möglicherweise von außen auf die Textfelder zugreifen? Oder den Inhalt eines Textfeldes an eine andere Funktion übergeben?

Hat jemand eine weitere Idee oder einen Tipp, wie man das Problem lösen könnte?
Darüber würde ich mich sehr freuen!
Viele Grüße Carlotta

Code: Alles auswählen

import tkinter as tk
from tkinter import*

fenster = tk.Tk()
fenster.title("Berechnung WEA:")
fenster.geometry('850x550')


def button_anzahl():
    eingang_text = eingabefeld.get()
    if (eingang_text == ""):
        berechnete_ausgabe.config(text="Es muss etwas eingegeben werden!")
    else:
        ausgang_text = "Anzahl: " + str(eingang_text) 
        berechnete_ausgabe.config(text=ausgang_text)
        button_action()
     
  
def button_action():
    label_frage2 = Label(fenster, text="Höhe: ", font=('arial', 10, 'bold'))
    eingabefeld2 = Entry(fenster, bd=5, width=20)
    global eingang_text2
    eingang_text2 = str(eingabefeld2.get())
    bestätigen_button2 = Button(fenster, text="Bestätigen", command=button_action2)

    label_frage2.grid(row = 1, column = 0)
    eingabefeld2.grid(row = 1, column = 1)
    bestätigen_button2.grid(row = 1, column = 2)


def button_action2():
    #eingang_text2 = eingabefeld2.get()
    if (eingang_text2 == ""):
        berechnete_ausgabe2 = Label(fenster)
        berechnete_ausgabe2.config(text="Es muss etwas eingegeben werden!")
        berechnete_ausgabe2.grid(row = 1, column = 3)
    else:
        ausgang_text2 = "Höhe: " + str(eingang_text2) 
        berechnete_ausgabe2 = Label(fenster)
        berechnete_ausgabe2.config(text=ausgang_text2)
        berechnete_ausgabe2.grid(row = 1, column = 3)
        global g_nabenhoehe
        g_nabenhoehe = float(eingang_text2)
        
        
#ABFRAGE ANZAHL
label_frage = Label(fenster, text="Anzahl: ", font=('arial', 10, 'bold'))
berechnete_ausgabe = Label(fenster)
eingabefeld = Entry(fenster, bd=5, width=20)
bestätigen_button = Button(fenster, text="Bestätigen", command=button_anzahl)

label_frage.grid(row = 0, column = 0)
eingabefeld.grid(row = 0, column = 1)
berechnete_ausgabe.grid(row = 0, column = 3)
bestätigen_button.grid(row = 0, column = 2)

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

@c.schroeder: Vergiss ``global`` sofort wieder und falls noch nicht geschehen, beschäftige Dich mit objektorientierter Programmierung (OOP), denn die braucht man für jede nicht-triviale GUI.

Auf Modulebene sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Damit sind dann auch automatisch die globalen Variablen beseitigt, wenn man kein ``global`` verwendet. Und man muss dann jeder Funktion alles was sie ausser Konstanten benötigt, sauber als Argument(e) übergeben. Womit Programme einfacher nachvollziehbarer und damit auch weniger fehleranfällig werden. Und auch einfacher testbar, weil man Funktionen/Methoden dann isoliert testen kann, ohne vorher schauen zu müssen welchen globalen Zustand man vor dem Aufruf herstellen muss, damit die das richtige tun.

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.

Die Grösse eines Fensters gibt man in der Regel nicht vor, die Ergibt sich automatisch aus dem Inhalt.

Daten und Code wiederholt man nicht. Das macht unnötig Arbeit beim schreiben, und noch mehr beim ändern. So etwas wie die Schriftart für Labels definiert man am Anfang einmal als Konstante. Dann lässt sich das an *einer* Stelle im Code anpassen, und man muss nicht durch den ganzen Code gehen, die Font-Daten suchen, an jeder Stelle entscheiden ob die für die Label-Art sind, die man ändern möchte oder nicht, und an Fundstelle dann gleichartig ändern.

Namen sind wichtig und falls man nicht Meister Yoda ist, dann ist eine `label_frage` etwas anderes als ein `frage_label` und bei `button_anzahl` erwarten die meisten Leser eine Zahl, welche die Anzahl von `button`-Objekten beschreibt, und keine Funktion. Funktionen und Methoden werden üblicherweise nach der Tätigkeit benannt, die sie durchführen. Bei Rückruffunktionen ist auch das Namensmuster `on_*()` üblich, mit einer Beschreibung des Ereignisses auf das die Funktion reagiert. Also beispielsweise `on_submit()` bei etwas das auf eine Benutzerbestätigung hin ausgelöst wird.

Um die Bedingung bei ``if`` (und ``while``) gehören keine Klammern.

Das Ergebnis von `Entry.get()` ist bereits eine Zeichenkette. Das zusammenstückeln von Zeichenketten und Werten mittels ``+`` und `str()` ist eher BASIC als Python. Dafür gibt es die `format()`-Methode auf Zeichenketten und f-Zeichenkettenliterale.

Man nummeriert keine Namen. Schon gar nicht sinnlos lokale Namen die überhaupt gar nicht mit den anderen Namen kollidieren würden.

Und ja, Du kannst nicht sofort nach dem Erstellen eines `Entry`-Objekts dessen Inhalt abfragen. Wie soll denn zu dem Zeitpunkt auf magische Weise schon das drin stehen was der Benutzer erst später überhaupt eingeben kann?

In der Rückruffunktion `button_action2()` wird es dann auch problematisch weil im Gegensatz zum vorhergehenden Schritt *dort* erst das Ausgabelabel erstellt wird. Der Benutzer kann aber mehrfach auf den Button drücken und da wird dann *jedes mal* ein neues Ausgabelabel erstellt. Das muss auch nur *einmal* erstellt werden, und von aussen kommen.

Und um das zu garantieren, muss man jeweils dafür sorgen, das bereits eingegebene Sachen nicht noch einmal bestätigt werden können, man muss also den jeweils vorhergehenden Button auch mit durchreichen und bei erfolgreicher Eingabe deaktivieren.

Zwischenstand wäre dann das hier:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from functools import partial

LABEL_FONT = ("arial", 10, "bold")


def on_height_submit(eingabefeld, ausgabe_label, bestaetigen_button):
    text = eingabefeld.get().strip()
    if not text:
        ausgabe_label["text"] = "Es muss etwas eingegeben werden!"
    else:
        ausgabe_label["text"] = f"Höhe {text}"
        eingabefeld["state"] = bestaetigen_button["state"] = tk.DISABLED


def create_height_entry(fenster):
    tk.Label(fenster, text="Höhe: ", font=LABEL_FONT).grid(row=1, column=0)

    eingabefeld = tk.Entry(fenster, bd=5, width=20)
    eingabefeld.grid(row=1, column=1)

    bestaetigen_button = tk.Button(fenster, text="Bestätigen")
    bestaetigen_button.grid(row=1, column=2)

    ausgabe_label = tk.Label(fenster)
    ausgabe_label.grid(row=1, column=3)

    bestaetigen_button["command"] = partial(
        on_height_submit, eingabefeld, ausgabe_label, bestaetigen_button
    )


def on_count_submit(fenster, eingabefeld, ausgabe_label, bestaetigen_button):
    text = eingabefeld.get().strip()
    if not text:
        ausgabe_label["text"] = "Es muss etwas eingegeben werden!"
    else:
        ausgabe_label["text"] = f"Anzahl: {text}"
        eingabefeld["state"] = bestaetigen_button["state"] = tk.DISABLED
        create_height_entry(fenster)


def main():
    fenster = tk.Tk()
    fenster.title("Berechnung WEA:")

    tk.Label(fenster, text="Anzahl: ", font=LABEL_FONT).grid(row=0, column=0)

    eingabefeld = tk.Entry(fenster, bd=5, width=20)
    eingabefeld.grid(row=0, column=1)

    bestaetigen_button = tk.Button(fenster, text="Bestätigen")
    bestaetigen_button.grid(row=0, column=2)

    ausgabe_label = tk.Label(fenster)
    ausgabe_label.grid(row=0, column=3)

    bestaetigen_button["command"] = partial(
        on_count_submit,
        fenster,
        eingabefeld,
        ausgabe_label,
        bestaetigen_button,
    )

    fenster.mainloop()


if __name__ == "__main__":
    main()
Da ist jetzt noch nicht berücksichtigt, das man am Ende ja vielleicht etwas mit den ganzen Eingaben machen will. Und da man keine globalen Variablen verwendet, müsste man die gesammelten Eingaben, beispielsweise in einem Wörterbuch, auch immer mit durchreichen, und ganz am Ende dann einer Funktion übergeben, die etwas damit macht.

Am aktuellen Stand fällt auf, dass die Funktionen abwechselnd *sehr* ähnlich sind, und sich nur in einigen wenigen Werten unterscheiden. So etwas macht man nicht, weil auch hier wieder eine Anpassung am Muster das da entsteht x-mal wiederholt werden muss, in jeder Kopie des Codes. Dafür gibt es Funktionen, um wiederholte Muster im Code nicht kopieren zu müssen.

Allerdings sollte man den Ablauf in der GUI hier sowieso mal grundsätzlich in Frage stellen. Warum muss man jede Eingabe bestätigen und bekommt dann erst die nächste? So eine GUI ist keine Konsolenanwendung. Normalerweise bekommt der Benutzer eine Eingabemaske, gibt dort alle seine Werte ein, und drückt dann ”Bestätigen”. Daraufhin läuft Code der *alle* Eingabefelder validiert, und ggf. eine Meldung ausgibt, was da noch falsch ist.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
c.schroeder
User
Beiträge: 11
Registriert: Montag 18. Oktober 2021, 15:22

@__blackjack__: Vielen Dank für die schnelle und ausführliche Antwort! Die Erklärungen helfen mir wirklich sehr!

Ich habe jedoch noch einige Nachfragen:

-- Die Funktion "def on_count_submit" wird ja anscheinend über den Term:

Code: Alles auswählen

bestaetigen_button["command"] = partial(
        on_count_submit,
        fenster,
        eingabefeld,
        ausgabe_label,
        bestaetigen_button,
    )
aufgerufen. Warum wird hier die Funktion partial() verwendet, was tut sie?

-- Im Internet habe ich gelesen, dass eckigen Klammern darauf hinweisen, dass die Argumente optional sind. Ist das so?
Und wofür stehen die Argumente ["state"], ["command"] und ["text"] in den Klammern?

-- Und die letzte Frage: Was ist mit den letzten beiden Codezeilen gemeint?

Code: Alles auswählen

if __name__ == "__main__":
    main()
Viele Grüße Carlotta
Benutzeravatar
__blackjack__
User
Beiträge: 13075
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@c.schroeder: Das `command`-Argument erwartet eine Funktion die *keine* Argumente erwartet. `on_count_submit()` erwartet aber Argumente. `functools.partial()` nimmt eine Funktion und Argumente und liefert als Ergebnis eine Funktion, die wenn man sie aufruft, diese Argumente bekommt. Man muss nicht alle Argumente, wie in diesem Fall, übergeben, sondern kann auch nur einen Teil angeben. Deswegen `partial()`. Das klassische Einführungsbeispiel für so etwas wie `partial()` ist das hier:

Code: Alles auswählen

In [170]: from operator import add                                              

In [171]: add(23, 42)                                                           
Out[171]: 65

In [172]: add_5 = partial(add, 5)                                               

In [173]: add_5(4711)                                                           
Out[173]: 4716
Man hat eine `add()`-Funktion die zwei Argumente erwartet und addiert. Und daraus kann man mit `partial()` eine Funktion machen, die weniger Argumente erwarte, also hier zum Beispiel eine Funktion die immer 5 auf das Argument addiert, weil das erste Argument an 5 gebunden wird.

Eckige Klammern bedeuten bei Syntax- oder Optionsbeschreibungen oft, dass es sich um einen optionalen Teil handelt. Das kann man nicht einfach verallgemeinern. In Python, und auch vielen anderen Programmiersprachen, dienen die als Operator für einen Index oder Schlüsselzugriff. Was das konkret bedeutet, hängt von dem Wert beziehungsweise dessen Datentyp ab, was *der* daraus dann macht.

Das klingt ein bisschen so, als wenn Du mit GUI-Programmierung anfängst bevor Du die Grundlagen durchgearbeitet hast. Denn das sollte Dir bei einigen Grunddatentypen (Zeichenketten, Listen, Tupel, Wörterbücher) schon deutlich mehr als einmal über den Weg gelaufen sein.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
c.schroeder
User
Beiträge: 11
Registriert: Montag 18. Oktober 2021, 15:22

@__blackjack__: Vielen Dank für die Hilfe und die vielen Anmerkungen!
Antworten