Tkinter Button List/Dict: Selbst löschende Buttons

Fragen zu Tkinter.
Antworten
fschober
User
Beiträge: 2
Registriert: Freitag 6. Dezember 2019, 17:28
Wohnort: Hallein
Kontaktdaten:

Liebes Python Forum!

Ich sitze nun seit einer Woche an diesem Problem, und bin langsam am verzweifeln. Ich stecke wohl einfach schon zu tief drinnen.
Folgendes Szenario:

Ich programmiere eine Database Software, und dafür gerade eine Entry Form. In dieser Entry Form gibt es einen Bereich, in dem man Worte aus einem Optionmenu auswählen kann.
Wenn ein Button ausgewählt wird, erstellt die callback function einen kleinen button darunter, mitsamt dem wort, welches dann auch zu einem array (liste, dict, etc) hinzugefügt wird. So weit so gut.
Die Buttons werden bei jeder aktion mit einer update function und einer for schleife neu erstellt.
Wenn man nun auf einen Button klickt, so soll er sich selber löschen und das Wort effektiv aus dem Array entfernen. Und genau das will einfach nicht funktionieren.
Ursprünglich ist alles auf Listen aufgebaut gewesen, und der lambda funktion habe ich die indizies übergeben. Mittlerweile benütze ich ein Dict,
welches glaube ich besser funktioniert.

Hier der code, aufbereitet zum ausprobieren:

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk

def rel_add(*args):
    operators[tkvar.get()] = ""
    rel_buttons_update()


def del_button(name):
    
    operators[name].destroy()
    del operators[name]
    rel_buttons_update()


def rel_buttons_update():
    i = 0
    for name, button in operators.items():
        # print(name)
        temp = tk.Button(master=rel_grid,
                            text=name, command=lambda x=name: del_button(name) )
        operators[name] = temp
        operators[name].grid(column=i, row=0, sticky="nw")
        # print(operators)
        i += 1



operators = {}
win = tk.Tk()                           
 
tkvar = tk.StringVar(win)             # Create a Tkinter variable

choices_words = ["oolbath", "pflanze", "haus", "wasser", "brimbambum"]      # Create Variable List

tkvar.set('Related Words...')              # set the default option
choices_words.sort()                        # Sort List

tk.Label(win, text="Related Words: ").grid(row=0,column=0, sticky="w")
rel = tk.OptionMenu(win, tkvar, *choices_words)   
rel.grid(row=0,column=1, sticky="w")  

# Callbuck Function for Dropdown Menu

tkvar.trace("w", rel_add)

rel_grid = tk.Frame(win)

# Display the Buttons for the related Words

rel_grid.grid(row=1,column=1, sticky="w") 


win.mainloop()
Ich weiß dass ich die Buttons nicht ordentlich referenziere...
ich bin wirklich für jede Hilfe dankbar!

Lg
Fabian
Benutzeravatar
__blackjack__
User
Beiträge: 14087
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@fschober: Ein Fehler ist das Du den falschen Namen an `del_button()` übergibst. Du nimmst da `name`, was ja durch die Schleife verändert wird und zu dem Zeitpunkt wo der Button geklickt wird nur dann den richtigen Wert hat, wenn der `name` vom letzten Schleifendurchlauf zufällig der richtige ist. Du müsstest an der Stelle das Argument vom ``lambda``-Ausdruck benutzen das im Schleifendurchlauf an das `name` aus dem jeweiligen Schleifendurchlauf gebunden wird. An der Stelle würde ich aber auch eher `functools.partial` empfehlen statt des ``lambda``-mit-default-wert-Tricks.

Der zweite Fehler ist das Du bei jedem `rel_buttons_update()` keine Buttons aktualisierst, sondern jedes mal neue erzeugst und die in die gleichen Zellen setzt wie die aus dem vorherigen Durchlauf. Das *er*setzt die *nicht*, sondern stapelt die immer mehr werdenden Buttons einfach nur übereinander so das die zuletzt hinzugefügten die davor überdecken. Wenn Du dann welche löscht werden die dahinter halt wieder sichtbar. Beziehungsweise löscht Du ja nur einen Button und erzeugst dann gleich wieder alle bis auf den einen neu.

Sonstige Anmerkungen: Auf Modulebene gehört nur Code der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst. Die Rückruffunktionen können dann nicht mehr so einfach auf globale Variablen zugreifen, was sie aber auch nicht sollten, denn Funktionen und Methoden sollten alles was sie ausser Konstanten benötigen als Argument(e) übergeben bekommen. Da braucht man dann noch an ein paar anderen Stellen `functools.partial()` beziehungsweise kommt man bei jeder nicht-trivialen GUI nicht um objektorientierte Programmierung herum.

Faustregel für Kommentare: Nicht beschreiben *was* gemacht wird, denn das steht bereits im Code, sondern *warum* der das *so* macht, sofern das nicht offensichtlich ist. Hinter ein ``something.sort()`` den Kommentar ``# Sort list`` zu schreiben bringt dem Leser genau Null Erkenntnisgewinn.

Abkürzungen bei Namen sind nicht gut. Ein Name soll dem Leser vermitteln was der Wert dahinter bedeutet, nicht zum rätselraten zwingen.

Eine leere Zeichenkette ist kein sinnvoller Platzhalterwert für einen `Button`. Das ist verwirrend. Für ”Nichts” ist der Wert `None` da. Wobei man sich an der Stelle überlegen sollte ob man da nicht gleich den `Button` erzeugt. Das würde den Code einfacher machen und auch die Fehlerbehebung das immer wieder unnötig Buttons erzeugt und übereinander gestapelt werden. Wobei an der Stelle auch das Wörterbuch entweder problematisch ist, weil jedes Wort nur einmal hinzugefügt werden kann, oder man einen Test an der Stelle braucht, der aktiv verhindert das ein Wort mehr als einmal hinzugefügt werden kann. Falls man jedes Wort nur einmal zur gleichen Zeit verwenden können soll, wäre es in der GUI auch gut wenn der Benutzer Worte die er nicht hinzufügen kann entweder gar nicht erst sieht, oder sie zumindest optisch als nicht aktiv erkennbar sind.

Beim aktualisieren werden die vorhandenen Buttons in dem Wörterbuch überhaupt nicht verwendet. Das sollten sie aber, statt immer neue zu erzeugen. Und zwar nur die Buttons und nicht die Namen/Worte, denn wenn man neue Buttons schon in der Funktion zum hinzufügen hinzugefügt (dazu ist die ja da), dann braucht man in der Aktualisierungsfunktion nur die bereits vorhandenen Buttons neu anzuordnen. Und man muss dann auch den `Frame` in dem die Buttons stecken auch gar nicht in die Funktion zum aktualisieren weiterreichen.

Wenn man über irgendetwas iteriert und zusätzlich eine laufende Zahl benötigt, gibt es dafür die `enumerate()`-Funktion.

Wörterbücher haben eine `pop()`-Methode die einen Wert zu einem Schlüssel liefert *und* das Schlüssel/Wert-Paar aus dem Wörterbuch entfernt.

Code: Alles auswählen

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


def add_related_word(word_variable, word_to_button, words_frame, *_args):
    word = word_variable.get()
    if word not in word_to_button:
        word_to_button[word] = tk.Button(
            words_frame,
            text=word,
            command=partial(
                delete_related_word_button, word_to_button, word
            ),
        )
        update_related_word_buttons(word_to_button)


def delete_related_word_button(word_to_button, word):
    word_to_button.pop(word).destroy()
    update_related_word_buttons(word_to_button)


def update_related_word_buttons(word_to_button):
    for i, button in enumerate(word_to_button.values()):
        button.grid(column=i, row=0, sticky=tk.NW)


def main():
    words = ["brimbambum", "haus", "oolbath", "pflanze", "wasser"]

    window = tk.Tk()

    tk.Label(window, text="Related Words: ").grid(row=0, column=0, sticky=tk.W)

    word_to_button = {}
    word_variable = tk.StringVar(value="Related Words...")
    tk.OptionMenu(window, word_variable, *words).grid(
        row=0, column=1, sticky=tk.W
    )

    words_frame = tk.Frame(window)
    words_frame.grid(row=1, column=1, sticky=tk.W)

    word_variable.trace(
        "w",
        partial(add_related_word, word_variable, word_to_button, words_frame),
    )

    window.mainloop()


if __name__ == "__main__":
    main()
“Vir, intelligence has nothing to do with politics!” — Londo Mollari
fschober
User
Beiträge: 2
Registriert: Freitag 6. Dezember 2019, 17:28
Wohnort: Hallein
Kontaktdaten:

Hallo @__blackjack__ !

Ja bist du wahnsinnig, mit so einer ausführlichen Antwort habe ich nicht gerechnet... hab mir jetzt nochmal Zeit genommen und deinen Code in Ruhe durchgeschaut.
Echt, Riesendank dafür!

Ich mach das Ganze auf jeden Fall Objektorientiert... ist aber mein erstes "richtiges" Programm, und es wird immer größer. Nachdem was du auch über die Kommentare gesagt hast, werde ich definitiv nochmal alle Kommentare in meinem Code neu schreiben, weil die machen jetzt nämlich genau das was sie nicht tun sollten! :D

Nochmals herzlichen Dank!

Fabian
Antworten