tkinter - combobox aktualisieren

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
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Hallo zusammen,

ich habe zwei Comboboxen, die ich mit Werten aus einem Dictionary fülle. Dabei soll die zweite Combobox in Abhängigkeit der Auswahl der ersten Box gefüllt werden. Als Init bekomme ich die Boxen so gefüllt wie ich es gerne hätte. Zum späteren Aktualisieren habe ich eine Funktion geschrieben und die an die erste Combobox gebunden. Allerdings hat diese Funktion ja keinen Zugriff an die zweite Combobox und ich weiß nicht wie ich die neue Liste in die zweite Combobox bekomme. Kann ich irgendwie trotz der lambda Funktion einen Wert zurückgeben?

Also in Kurz: Wie bekomme ich über eine Funktion ein Fensterelement aktualisiert?

Hier mal ein Minimalbeispiel, was ich bisher in der Richtung habe:

Code: Alles auswählen

#!/usr/bin/env python
# coding: utf8
import tkinter as tk
import tkinter.ttk as ttk

def mainframe():
    frame = tk.Tk()
    frame.title('Frame')

    dictionary = {"Schreibschrift": {"S": [[1, 1], [0, 0]], "T": [[0, 0], [1, 1]]},
                  "Druckschrift": {"D": [[1, 0], [1, 0]], "E": [[0, 1], [0, 1]]}}

    # create the combo boxes
    combo_font = ttk.Combobox(frame)
    combo_font.grid(row=0, column=1, sticky='n')
    combo_font.bind('<<ComboboxSelected>>', lambda _event: update_letter(dictionary, combo_font.get()))

    combo_letter = ttk.Combobox(frame)
    combo_letter.grid(row=1, column=1, sticky='n')

    # Init
    font_list = []
    for font in dictionary:
        font_list.append(font)
    combo_font['values'] = font_list
    combo_font.set(font_list[0])

    letter_list = []
    for letter in dictionary[combo_font.get()]:
        letter_list.append(letter)
    combo_letter['value'] = letter_list
    combo_letter.set(letter_list[0])

    frame.mainloop()

def update_letter(data_loaded, selected_font, v):
    # Buchstaben auslesen
    letter_list = []
    for letter in data_loaded[selected_font]:
        letter_list.append(letter)
    # Was mache ich jetzt mit dieser Liste?
    # combo_letter ist hier ja nicht bekannt

if __name__ == '__main__':
    mainframe()

Schon einmal vielen Dank!
BlackJack

@Zizibee: Für funktionsaufrufübergreifenden Zustand wurde objektorientierte Programmierung (OOP) erfunden. Ohne kommt man bei GUIs nicht weit.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

@BlackJack: Danke für die Antwort. Ich hatte ja fast befürchtet, dass das in die Richtung OOP geht. Dann werde ich mal versuchen mich anhand meines Problems da einzuarbeiten und verstehe so vielleicht auch endlich mal den Sinn davon.
Die Beispiele, die ich zu OOP kenne sehen irgendwie alle so aus, als wolle man eine Datenbank zu Autos oder Robotern erstellen...
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Ich drehe mich jetzt schon den ganzen Tag im Kreis und komme nicht weiter. Nachdem ich versucht habe alles nach Art der OOP zu schreiben hänge ich an der lambda-Funktion und was ich wie und wann übergeben muss, evtl. auch wo ich die Aktion bei Änderung der ComboBox combo_font hinschreiben muss damit combo_letter geändert wird...
Im Moment habe ich auch noch einen falschen Aufruf in Zeile 47, weil ich versuche combo_font auszulesen, bevor ich es angelegt habe. Aber wenn ich versuche den Fehler zu beseitigen komme ich wieder bei der lambda Funktion nicht weiter...

Ich hoffe mir kann da jemand helfen!

Code: Alles auswählen

import tkinter as tk
import tkinter.ttk as ttk


class ComboBox:
    def __init__(self, grid_row, grid_column, handle=None):
        self.grid_row = grid_row
        self.grid_column = grid_column
        self.handle = handle
        self.ComboBox = ttk.Combobox()
        self.ComboBox.grid(row=self.grid_row, column=self.grid_column)
        if handle is not None:
            self.ComboBox.bind('<<ComboboxSelected>>', lambda: self.handle)

    def set(self, data, handle=None):
        self.data = data
        self.ComboBox['values'] = self.data
        self.ComboBox.set(self.data[0])

    def get(self):
        return self.ComboBox.get()


def get_fonts(dictionary):
    font_list = []
    for font in dictionary:
        font_list.append(font)
    return font_list


def get_letters(dictionary, key):
    letter_list = []
    for letter in dictionary[key]:
        letter_list.append(letter)
    return letter_list


def main():
    frame = tk.Tk()
    frame.title('Frame')

    dictionary = {"Schreibschrift": {"S": [[1, 1], [0, 0]], "T": [[0, 0], [1, 1]]},
                  "Druckschrift": {"D": [[1, 0], [1, 0]], "E": [[0, 1], [0, 1]]}}

    # create combobox
    combo_letter = ComboBox(1, 0)
    combo_font = ComboBox(0, 0, combo_letter.set(get_letters(dictionary, combo_font.get())))

    # set init-values
    combo_font.set(get_fonts(dictionary))
    combo_letter.set(get_letters(dictionary, combo_font.get()))

    frame.mainloop()


if __name__ == '__main__':
    main()

__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du versuchst auf combo_font zuzugreifen, bevor es existiert. Das kracht dann. Schau mal auf die rechte Seite in Zeile 47, da darf kein Name vorkommen, der nicht schon existiert. Ausser du machst das "lazy" mit einem lambda (was wahrscheinlich irgendwie dein Gedanke war). Aber so wie du es jetzt machst rufst du direkt auf, und das kracht.

Und die Vererbung die du aufbaust erschliesst sich mir nicht. Deine ComboBox (nicht gut, dass sie genauso heisst wie ttk.ComboBox) tut ja gar nix. Ausser ein Ereignis anzumelden (und das falsch, denn das lambda macht genau gar nix, es ruft handle gar nicht auf).

Ich wuerde stattdessen eine Hilfsklasse definieren, welche die beiden combo-boxen als Argument bekommt, und die Daten der einen in Abhaengigkeit von der anderen setzt.

Code: Alles auswählen


class SelectionUpdater(object):

    def __init__(self, parent, child, data):
        self._parent = parent
        self._child = child
        self._data = data
        self._parent.bind('<<ComboboxSelected>>', self._update)


    def _update(self, *_a):
        new_data = self._data[self._parent.get()]
        self._child["values"] = new_data
        
font_updater = SelectionUpdater(font_combo, letter_combo, font2letter)        

Das ist ungetestet & einfach nur abgeleitet von deinem Code - ob zB die Zuweisung neuer Werte so funktioniert weiss ich nicht.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Danke __deets__ für deine Antwort und den Code. Ein Teil der Probleme war mir bekannt, leider hatte ich keine Ahnung, wie ich sie lösen könnte...
Ich habe deinen Code angepasst und jetzt funktioniert es :D Ich selber wäre wohl nie auf die Idee gekommen es auf diese Weise anzugehen.
Ob meine Änderungen aber programmiertechnisch gut / sinnvoll sind, kann ich leider nicht sagen...

Code: Alles auswählen

import tkinter as tk
import tkinter.ttk as ttk


class SelectionUpdater(object):
    def __init__(self, parent, child, data):
        self._parent = parent
        self._child = child
        self._data = data
        self._parent.bind('<<ComboboxSelected>>', self._update)

    def _update(self, *_a):
        new_data = self._get_letters()
        self._child["values"] = new_data
        self._child.set(new_data[0])

    def _get_letters(self):
        letter_list = []
        for letter in self._data[self._parent.get()]:
            letter_list.append(letter)
        return letter_list


def get_fonts(dictionary):
    font_list = []
    for font in dictionary:
        font_list.append(font)
    return font_list


def main():
    frame = tk.Tk()
    frame.title('Frame')

    dictionary = {"Schreibschrift": {"S": [[1, 1], [0, 0]], "T": [[0, 0], [1, 1]]},
                  "Druckschrift": {"D": [[1, 0], [1, 0]], "E": [[0, 1], [0, 1]]}}

    font_combo = ttk.Combobox(frame)
    font_combo.grid(row=0, column=1, sticky='n')
    font_combo.set(get_fonts(dictionary))
    font_combo['values'] = get_fonts(dictionary)
    font_combo.set(get_fonts(dictionary)[0])

    letter_combo = ttk.Combobox(frame)
    letter_combo.grid(row=1, column=1, sticky='n')

    font_updater = SelectionUpdater(font_combo, letter_combo, dictionary)
    font_updater._update()

    frame.mainloop()

if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Schoen das es funktioniert. Aber warum hast du _get_letters gebaut? self._data[self._parent.get()] ist doch schon eine Liste, die baust du jetzt muehselig ein eine neue Liste ein. Selbst wenn das notwendigk waere (weil es zB ein generator ist, oder weil du eine Kopie brauchst), reicht doch ein einfaches list(self._data[self._parent.get()]), und fertig ist.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

So ganz wird mir der Sinn der SelectionUpdater-Klasse noch nicht klar; die weiß mir zu viel, und macht zu wenig, als dass es für eine Klasse reichen würde.

Code: Alles auswählen

import tkinter as tk
import tkinter.ttk as ttk

def update(combobox, values):
    values = list(values)
    combobox['values'] = values
    combobox.set(values[0])

def main():
    frame = tk.Tk()
    frame.title('Frame')

    dictionary = {"Schreibschrift": {"S": [[1, 1], [0, 0]], "T": [[0, 0], [1, 1]]},
                  "Druckschrift": {"D": [[1, 0], [1, 0]], "E": [[0, 1], [0, 1]]}}

    font_combo = ttk.Combobox(frame)
    font_combo.grid(row=0, column=1, sticky='n')
    update(font_combo, dictionary)

    letter_combo = ttk.Combobox(frame)
    letter_combo.grid(row=1, column=1, sticky='n')
    update(letter_combo, dictionary[font_combo.get()])
    font_combo.bind('<<ComboboxSelected>>', lambda e: update(letter_combo, dictionary[font_combo.get()]))
    frame.mainloop()

if __name__ == '__main__':
    main()
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ziel der Klasse (vor der IMHO unnoetigen Spezialisierung durch _get_letters) ist es gewesen, die Verbindung zwischen zwei ComboBoxen zu abstrahieren. Solange die Daten dem Protokoll genuegen spart man sich den verteilten Boilerplate.

Natuerlich koennte man statt eine Klasse auch ein Closure verwenden, aber das ist dann auch ein bisschen Haarespalterei.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

__deets__ hat geschrieben:Aber warum hast du _get_letters gebaut? [...] reicht doch ein einfaches list(self._data[self._parent.get()]), und fertig ist.
Das liegt daran, dass ich nicht an die list-Funktion gedacht habe, bzw. auch gar nicht wusste, was die alles macht. Jetzt in der neusten Version habe ich get_font und get_letter durch list ersetzt.

Code: Alles auswählen

import tkinter as tk
import tkinter.ttk as ttk


class SelectionUpdater(object):
    def __init__(self, parent, child, data):
        self._parent = parent
        self._child = child
        self._data = data
        self._parent.bind('<<ComboboxSelected>>', self._update)

    def _update(self, *_a):
        new_data = list(self._data[self._parent.get()])
        self._child["values"] = new_data
        self._child.set(new_data[0])


def main():
    frame = tk.Tk()
    frame.title('Frame')

    dictionary = {"Schreibschrift": {"S": [[1, 1], [0, 0]], "T": [[0, 0], [1, 1]]},
                  "Druckschrift": {"D": [[1, 0], [1, 0]], "E": [[0, 1], [0, 1]]}}

    font_combo = ttk.Combobox(frame)
    font_combo.grid(row=0, column=1, sticky='n')
    font_combo.set(list(dictionary))
    font_combo['values'] = list(dictionary)
    font_combo.set(list(dictionary)[0])

    letter_combo = ttk.Combobox(frame)
    letter_combo.grid(row=1, column=1, sticky='n')

    font_updater = SelectionUpdater(font_combo, letter_combo, dictionary)
    font_updater._update()

    frame.mainloop()

if __name__ == '__main__':
    main()
@Sirius3: Danke für die Version ohne Klasse!

Gibt es denn Regeln oder Richtlinien nach denen man abschätzen kann, ob bzw. wann man eine Klasse nutzt und wann man das über Funktionen realisiert? Das scheint ja nicht immer so ganz eindeutig zu sein...
Ist grundsätzlich eine Art vorzuziehen wenn beides möglich ist?
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@Zizibee: die Magie mit dem ›*_a‹ ist unschön. ›bind‹ erwartet eine Funktion mit einem event-Argument. Dass Du dieselbe Funktion auch ohne aufrufen willst, kann man per ›def update(self, event=None)‹ lösen, der Unterstrich bei update gehört da nicht hin. Dass drei mal hintereinander ›list(dictionary)‹ aufgerufen wird, sollte auch nicht sein. Zeile 27 ist zudem überflüssig und falsch. Was mich an der Klasse stört, ist, dass sie zu viel Zeug miteinander verknüpft.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ich würde den Unterstrich belassen, aber den Aufruf von _update in den Konstruktor ziehen, da er eh immer erfolgen soll.
Zizibee
User
Beiträge: 229
Registriert: Donnerstag 12. April 2007, 08:36

Vielen Dank für die Antworten, ich habe dadurch viel dazu gelernt!

@Sirius3: Was ich mir in Zeile 27 gedacht habe, kann ich dir nicht mehr sagen, wahrscheinlich aber nicht so viel... :roll:
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Zizibee hat geschrieben:Gibt es denn Regeln oder Richtlinien nach denen man abschätzen kann, ob bzw. wann man eine Klasse nutzt und wann man das über Funktionen realisiert? Das scheint ja nicht immer so ganz eindeutig zu sein...
Ist grundsätzlich eine Art vorzuziehen wenn beides möglich ist?
Wenn eine Klasse neben __init__() nur eine weitere Methode enthält und diese Methode auch nur einmalig aufgerufen wird (so wie es bei dir der Fall ist), dann ist das häufig ein Zeichen für eine überflüssige Klasse. Manchmal will man Code als Initialisierung haben und dann mittels Methode den Folgeschritt abrufen. Dies ist grundsätzlich aber auch als Generator mithilfe von yield möglich. In Ausnahmefällen, wo man z.B. eine API nachbaut, kann man IMHO von der Regel abweichen und eine Klasse definieren, die eigentlich auch gut als (Generator-)Funktion implementiert werden könnte.
__deets__
User
Beiträge: 14523
Registriert: Mittwoch 14. Oktober 2015, 14:29

@snafu so richtig deine Ausfuehrungen im allgemeinen sind, hier passen sie nicht. Die Methode _update wird *mitnichten* nur einmal aufgerufen, sondern ist als callback registriert. Und was bei der ganzen Sache nun ein Generator helfen sollte, wenn es um eine auf einem Ereignis basierende Aktion geht und nicht um einen expliziten Pull von neuen Daten erschliesst sich mir auch nicht.

Ja, wie schon gesagt: man koennte das ganze auch mit einem closure machen. Wie heisst es so schoen "closures are a poor man's objects, and objects are a poor man's closures". Aber der TE moechte/sollte sich in OOP ueben, da jetzt noch die *implizite* Variante mit closures zu empfehlen - bin ich skeptisch.

Last but not least verstehe ich das Gerede von "weiss zu viel" nicht. Man braucht drei Dinge - die zwei ComboBoxen und die Daten. Das muss sie wissen, und das weiss sie. Im Moment wird tatsaechlich nur lesend zugegriffen, und dann wuerde der closure direkt funktionieren.

Wenn man aber auch nur ein mu weiterdenkt will man wahrscheinlich etwas einbauen das verhindert, dass bei Auswahl des gleichen Elements in der Parent-Box trotzdem immer die Children gesetzt werden, weil das die Auswahl zerstoert. Und schon hat man mutablen Zustand, und closures werden vor Python 2 eklig, und danach mit nonlocal nun auch nicht gerade schnieke.
Benutzeravatar
snafu
User
Beiträge: 6738
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

__deets__ hat geschrieben:@snafu so richtig deine Ausfuehrungen im allgemeinen sind, hier passen sie nicht. Die Methode _update wird *mitnichten* nur einmal aufgerufen, sondern ist als callback registriert. Und was bei der ganzen Sache nun ein Generator helfen sollte, wenn es um eine auf einem Ereignis basierende Aktion geht und nicht um einen expliziten Pull von neuen Daten erschliesst sich mir auch nicht.
Das mit parent.bind() da oben habe ich übersehen. Die Ausführungen waren tatsächlich eher allgemein gemeint und haben sich größtenteils nicht auf hier gezeigten Code bezogen.
Antworten