Recursive Erstellung und Löschung von Entrys

Fragen zu Tkinter.
Antworten
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

Ich erstelle zu Anfang 4 entrys plus label.

Ich möchte nun sobald ich in das vierte entry einen Text geschrieben habe ein fünftes hinzufügen und wenn das vierte leer ist dieses durch das fünfte ersetzen.

Derzeit schaffe ich es mehr entrys hinzuzufügen wenn das jeweils letzte einen Eintrag hat und diese wieder zu löschen bis ich Minimum wieder vier entrys habe.
Ich mache das ganze nur nicht "schön" :oops: .
Ich komme derzeit auch nicht weiter da mir einfach keine Funktion einfällt mit der ich das schön umsetzen könnte.

Hier der Code.

Ich bitte um Tipps wie ich mein Vorhaben umsetzen kann.

Die Namensgebung passe ich an wenn ich etwas funktionierendes habe.

Code: Alles auswählen

import tkinter as tk
import threading
import time

class MyApp:
    def __init__(self, root):
        self.root = root
        self.names = list(range(1, 49))
        self.entry = {}
        self.label = {}
        self.rownumber = 0
        self.entrynumber = 1
        self.minentry = 4

        for name in self.names:
            e = tk.Entry(self.root)
            self.entry[name] = e
            lb = tk.Label(self.root, text=name)
            self.label[name] = lb

        for x in range(4):
            self.entry[self.entrynumber].grid(row=self.rownumber, column=0, sticky=tk.E)
            self.label[self.entrynumber].grid(row=self.rownumber, column=1)
            self.rownumber += 1
            self.entrynumber += 1

        b = tk.Button(self.root, text="Print all", command=self.print_all_entries)
        b.grid(row=100, sticky=tk.S)

        threading.Thread(target=self.add_more).start()

    def add_more(self):
        while True:
            for name in self.names:
                if self.entry[self.minentry].get() != "":
                    self.entry[self.entrynumber].grid(row=self.rownumber, column=0, sticky=tk.E)
                    self.label[self.entrynumber].grid(row=self.rownumber, column=1)
                    self.rownumber += 1
                    self.minentry += 1
                    self.entrynumber += 1
                time.sleep(0.2)

                if self.entry[self.minentry].get() == "" and self.minentry > 4:
                    self.entry[self.rownumber].grid_forget()
                    self.label[self.rownumber].grid_forget()
                    self.rownumber -= 1
                    self.minentry -= 1
                    self.entrynumber -= 1

    def print_all_entries(self):
        for name in self.names:
            if self.entry[name].get() != "":
                print(self.entry[name].get())

if __name__ == "__main__":
    root = tk.Tk()
    app = MyApp(root)
    root.mainloop()
    
Benutzeravatar
__blackjack__
User
Beiträge: 13227
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TheBombTuber: `self.names` macht nicht wirklich Sinn. Und `self.entrynumber` und `self.rownumber` sind redundant. Das eine lässt sich trivial aus dem anderen berechnen.

Das mit dem Thread kannst Du gleich mal wieder vergessen, das darf man nicht. GUI-Elemente dürfen nur vom Hauptthread verändert werden. GUI-Programmierung ist ereignisbasiert. Man könnte die Anzeige beispielsweise aktualisieren wenn der Eingabefokus ein `Entry` verlässt.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

@ __blackjack__ Danke für die schnelle Antwort.

Ich habe den Code so überarbeitet, dass ich keinen thread mehr brauche und habe mir die Events angeschaut.
Das vergessen der Entrys klappt auch schon fast verlässlich.

Wie ich das ganze noch so hinbekomme, dass wirklich das entry verschwindet welches ich angewählt habe und auch nur wenn es leer ist, werde ich morgen mal testen.

Aber schon einmal danke für deine Hilfe :D

Code: Alles auswählen

import tkinter as tk

class MyApp:
    def __init__(self, root):
        self.root = root
        self.entrynumber = 0
        self.entry = []

        for x in range(4):
            if len(self.entry) < 4:
                print(len(self.entry))
                self.baseentry()

        b1 = tk.Button(self.root, text="ADD")
        b1.grid(row=99, sticky=tk.S)
        b1.bind("<Button-1>", self.add_more)

    def baseentry(self):
        e=tk.Entry(self.root)
        self.entry.append(e)
        self.entry[self.entrynumber].grid(row=self.entrynumber, column=0, sticky=tk.E)
        self.entry[self.entrynumber].bind("<FocusOut>",self.loose_some)
        self.entrynumber += 1

    def add_more(self,event):
        self.baseentry()

    def loose_some(self,event):
        print(len(self.entry))
        print(self.entrynumber)
        if len(self.entry) > 4:
            self.entry.pop(len(self.entry) - 1)
            self.entry[len(self.entry) - 1].grid_forget()
            self.entrynumber -= 1


if __name__ == "__main__":
    root = tk.Tk()
    app = MyApp(root)
    root.mainloop()
    
Sirius3
User
Beiträge: 17812
Registriert: Sonntag 21. Oktober 2012, 17:20

Ein My-Präfix ist meist unsinnig, wenn es nicht auch eine OurApp gibt.
Die if-Abfrage in der for-Schleife sieht komisch aus, die Anzahl der erstellten Entries sollte doch klar sein. Listen sollten immer im Plural benannt sein, als `entries`.
b1 ist kein guter Variablename, weil kryptisch und mit Nummer. Für Buttons benutzt man command und nicht bind.
`baseentry` sollte eher nach einer Tätigkeit benannt sein, `create_entry`.
entrynumber ist immer gleich der Länge der Liste entry, also unnötig.
Wenn Du erst pop aufrufst, und dann das neue letzte Element vergisst, dann ist das falsch.
Mit negativen Zahlen kann man Elemente in Listen von Hinten ansprechen, daher ist das len überflüssig.
Es fehlt eine main-Funktion:

Code: Alles auswählen

import tkinter as tk

class App:
    def __init__(self, root):
        self.root = root
        self.entries = []

        for _ in range(4):
            self.create_entry()

        button = tk.Button(self.root, text="ADD", command=self.add_more)
        button.grid(row=99, sticky=tk.S)

    def create_entry(self):
        entry = tk.Entry(self.root)
        self.entries.append(entry)
        entry.grid(row=len(self.entries), column=0, sticky=tk.E)
        entry.bind("<FocusOut>", self.loose_some)

    def add_more(self,event):
        self.create_entry()

    def loose_some(self,event):
        if len(self.entries) > 4:
            entry = self.entries.pop()
            entry.grid_forget()


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

@Sirius3 Vielen dank für deine Tipps.

Ich habe jetzt den Code soweit wie er mir gefällt und alles tut was ich brauche.

Gibt es generell noch Sachen die ich daran verbessern könnte um das ganze "effizienter" oder leserlicher zu machen?

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk

class App:
    def __init__(self, root):
        self.root = root
        self.spinnummer=tk.StringVar(value=1)
        self.siglist = ("Sig1,Sig2,Sig3,Sig4,Sig5")
        self.entries = []
        self.comboxes = []
        self.spinboxes = []
        self.buttons = []

        for _ in range(4):
            self.create_entry()


        button = ttk.Button(self.root, text="ADD", command=self.add_more)
        button.grid(row=0, sticky=tk.NSEW,columnspan=5)


    def create_entry(self):
        self.entry = ttk.Entry(self.root,width=60)
        self.entries.append(self.entry)
        self.entry.grid(row=len(self.entries), column=2, sticky=tk.E,padx=5,pady=5)
        self.entry.bind("<FocusOut>", self.loose_some)

        self.combox = ttk.Combobox(self.root,width=8)
        self.comboxes.append(self.combox)
        self.combox.grid(row=len(self.entries), column=0, sticky=tk.E,padx=5,pady=5)
        self.combox['values'] = self.siglist.split(',')
        self.combox['state']='readonly'
        self.combox.current(0)

        self.spinbox = ttk.Spinbox(self.root,from_=1,to=48,textvariable=self.spinnummer,width=3)
        self.spinboxes.append(self.spinbox)
        self.spinbox.grid(row=len(self.entries), column=1, sticky=tk.E,padx=5,pady=5)

        self.button = ttk.Button(self.root,text="...",width=2)
        self.buttons.append(self.button)
        self.button.grid(row=len(self.entries),column=3, sticky=tk.E,padx=5,pady=5)



    def add_more(self):
        self.create_entry()

    def loose_some(self,event):
        if len(self.entries) > 4:
            if self.entry.get() == "":
                entry = self.entries.pop()
                entry.grid_forget()
                combox = self.comboxes.pop()
                combox.grid_forget()
                spinbox = self.spinboxes.pop()
                spinbox.grid_forget()
                button = self.buttons.pop()
                button.grid_forget()


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

Ein Problem in meiner Version habe ich noch gefunden. Wenn ich über die nun hinzugefügten Buttons einen Eintrag in die "Entrys" vornehmen möchte, erscheint der Eintrag immer im letzten "Entry".

Gibt es eine Möglichkeit den Text in das "Entry" neben dem geklickten Button einzufügen?

Ich müsste wahrscheinlich mit der Position des geklickten Buttons arbeiten oder?

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk,filedialog

class App:
    def __init__(self, root):
        self.root = root

        dbc_frame = self.DBCFrame(root)
        dbc_frame.grid()


    def DBCFrame(self, container):
        frame=tk.Frame(container)

        self.umrandung=ttk.LabelFrame(frame,text="Paths")
        self.umrandung.grid(row=0,column=0,sticky=tk.EW)

        self.create_entry_frame = tk.LabelFrame(self.umrandung)
        self.create_entry_frame.configure({"relief":"flat", "text":""})
        self.create_entry_frame.grid(row=1,column=0)


        self.siglist = ("Sig1,Sig2,Sig3,Sig4,Sig5")
        self.entries = []
        self.comboxes = []
        self.spinboxes = []
        self.buttons = []

        for _ in range(4):
            self.create_entry()

        ButtonFrame = tk.LabelFrame(self.umrandung)
        ButtonFrame.configure({"relief":"flat", "text":""})
        ButtonFrame.grid(row=0,column=0)

        add_button = ttk.Button(ButtonFrame, text="ADD", command=self.add_more)
        add_button.grid(row=0,column=0, sticky=tk.NSEW)


        del_button = ttk.Button(ButtonFrame, text="Delete",command=self.loose_some)
        del_button.grid(row=0,column=2, sticky=tk.NSEW)

        return frame

    def create_entry(self):
        self.entry = ttk.Entry(self.create_entry_frame,width=60)
        self.entries.append(self.entry)
        self.entry.grid(row=len(self.entries), column=2, sticky=tk.E,padx=5,pady=5)
        self.entry.bind("<FocusOut>", self.loose_some)

        self.combox = ttk.Combobox(self.create_entry_frame,width=8)
        self.comboxes.append(self.combox)
        self.combox.grid(row=len(self.entries), column=0, sticky=tk.E,padx=5,pady=5)
        self.combox['values'] = self.siglist.split(',')
        self.combox['state']='readonly'
        self.combox.current(0)

        self.spinbox = ttk.Spinbox(self.create_entry_frame,from_=1,to=48,width=3)
        self.spinboxes.append(self.spinbox)
        self.spinbox.grid(row=len(self.entries), column=1, sticky=tk.E,padx=5,pady=5)

        self.button = ttk.Button(self.create_entry_frame,text="...",width=2, command=self.insert_dbc_path)
        self.buttons.append(self.button)
        self.button.grid(row=len(self.entries),column=3, sticky=tk.E,padx=5,pady=5)

    def insert_dbc_path(self):
        output_pfad=tk.filedialog.askdirectory()
        if output_pfad != "":
            self.entry.delete(0,tk.END)
            self.entry.insert(0,output_pfad)


    def add_more(self):
        self.create_entry()

    def loose_some(self):
        if len(self.entries) > 4:
            entry = self.entries.pop()
            entry.grid_forget()
            combox = self.comboxes.pop()
            combox.grid_forget()
            spinbox = self.spinboxes.pop()
            spinbox.grid_forget()
            button = self.buttons.pop()
            button.grid_forget()


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 17812
Registriert: Sonntag 21. Oktober 2012, 17:20

Etwas, das siglist heißt, sollte wirklich eine Liste sein. Warum erzeugst Du die Liste erst später?
Die Funktion DBCFrame ist wie eine Klasse geschrieben und sollte wohl am besten auch eine Klasse sein. Ansonsten ist die Funktion auch völlig überflüssig, weil alles, was darin steht genausogut in __init__ stehen könnte.
`create_entry_frame` ist wie eine Funktion benannt, das ist sehr verwirrend, ebenso ist es verwirrend, dass `ButtonFrame` wie eine Klasse geschrieben ist.
Statt vier paralleler Listen für die Tabellenzeilen würde man besser alle Widgets in eine Liste packen.
`self.entry` in `create_entry` ist quatsch, weil das ja jeweils nur das letzte Entry-Element enthält, das erzeugt worden ist, dummerweise benutzt Du das aber an anderer Stelle auch noch. `insert_dbc_path` muß als Argument das richtige Entry-Element bekommen.

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk,filedialog
from functools import partial

class App:
    def __init__(self, root):
        self.root = root
        self.siglist = ["Sig1", "Sig2", "Sig3", "Sig4", "Sig5"]
        self.entries = []

        dbc_frame = tk.Frame(container)
        dbc_frame.grid()

        self.umrandung = ttk.LabelFrame(dbc_frame, text="Paths")
        self.umrandung.grid(row=0, column=0, sticky=tk.EW)

        self.entries_frame = tk.LabelFrame(self.umrandung, relief="flat", text="")
        self.entries_frame.grid(row=1, column=0)

        for _ in range(4):
            self.create_entry()

        button_frame = tk.LabelFrame(self.umrandung, relief="flat", text="")
        button_frame.grid(row=0, column=0)

        add_button = ttk.Button(button_frame, text="ADD", command=self.add_more)
        add_button.grid(row=0, column=0, sticky=tk.NSEW)

        del_button = ttk.Button(button_frame, text="Delete", command=self.loose_some)
        del_button.grid(row=0, column=2, sticky=tk.NSEW)


    def create_entry(self):
        combobox = ttk.Combobox(self.entries_frame, width=8)
        combobox['values'] = self.siglist
        combobox['state'] = 'readonly'
        combobox.current(0)

        spinbox = ttk.Spinbox(self.entries_frame, from_=1, to=48, width=3)

        entry = ttk.Entry(self.entries_frame, width=60)
        entry.bind("<FocusOut>", self.loose_some)

        button = ttk.Button(self.entries_frame,text="...", width=2, command=partial(self.insert_dbc_path, entry))
        row = (combobox, spinbox, entry, button)
        for column_index, item in enumerate(row):
            item.grid(row=len(self.entries), column=index, sticky=tk.E, padx=5, pady=5)
        self.entries.append(row)


    def insert_dbc_path(self, entry):
        output_pfad = tk.filedialog.askdirectory()
        if output_pfad != "":
            entry.delete(0, tk.END)
            entry.insert(0, output_pfad)


    def add_more(self):
        self.create_entry()

    def loose_some(self):
        if len(self.entries) > 4:
            items = self.entries.pop()
            for item in items:
                item.grid_forget()


def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()
Benutzeravatar
__blackjack__
User
Beiträge: 13227
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Randbemerkung: Im Betreff steht was von „Recursive“, aber Rekursion hat nichts mit dem Problem oder der Lösung zu tun.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

@Sirius3 @__blackjack__

Danke euch beiden nochmal für eure Hilfe. Ich habe endlich wieder Zeit mich mit dem Thema zu beschäftigen.

@Sirius3
Die Funktion DBCFrame ist wie eine Klasse geschrieben und sollte wohl am besten auch eine Klasse sein.
Ja das stimmt. Ich habe das ganze aus einem größeren Programm welches ich schreibe herausgebrochen, daher sah das ganze in meinem Beispiel unpassend aus.
`insert_dbc_path` muß als Argument das richtige Entry-Element bekommen.
Die frage ist wahrscheinlich sehr dumm aber woher genau weiß der Knopf zur Pfad Eintragung in welches Entry das gepackt werden muss.
Hängt das damit zusammen, dass der Button und das Entry die gleiche "endnummer" haben in entries?

Code: Alles auswählen

.
.!frame.!labelframe.!labelframe.!combobox
.!frame.!labelframe.!labelframe.!spinbox
.!frame.!labelframe.!labelframe.!entry
.!frame.!labelframe.!labelframe.!button
.!frame.!labelframe.!labelframe.!combobox2
.!frame.!labelframe.!labelframe.!spinbox2
.!frame.!labelframe.!labelframe.!entry2
.!frame.!labelframe.!labelframe.!button2
.!frame.!labelframe.!labelframe.!combobox3
.!frame.!labelframe.!labelframe.!spinbox3
.!frame.!labelframe.!labelframe.!entry3
.!frame.!labelframe.!labelframe.!button3
.!frame.!labelframe.!labelframe.!combobox4
.!frame.!labelframe.!labelframe.!spinbox4
.!frame.!labelframe.!labelframe.!entry4
.!frame.!labelframe.!labelframe.!button4
Kannst du mir da eine Lektüre empfehlen wo ich mich darüber gezielt einlesen kann?

Denn mein ziel wäre es noch, wenn in den Entrys ein Pfad steht, die Werte aus der combobox, der spinbox und dem entry auszulesen und im Format comboboxspinbox = entry (Sig51= PFAD) zu verwenden.

Und da werde ich ja eine gleiche Vorgehensweise verwenden müssen oder?


@__blackjack__
Randbemerkung: Im Betreff steht was von „Recursive“, aber Rekursion hat nichts mit dem Problem oder der Lösung zu tun.
Stimmt, davon bin ich ja mit der Methode die Elemente per Knopfdruck zu erzeugen und wieder zu löschen weggegangen.
Wäre aber mein Ziel wieder dahinzukommen, dass sich die Einträge "selbst" erstellen und löschen, sobald die Grundfunktionen sichergestellt sind.

Und entschuldigt bitte, dass ich wahrscheinlich mit sehr dummen Fragen um die Ecke komme :oops: .
Ich habe zwar immer eine grobe Idee wie ich die dinge umsetzen könnte aber es fehlt mir einfach noch in vielen Bereichen am Basiswissen.
Benutzeravatar
__blackjack__
User
Beiträge: 13227
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TheBombTuber: Die Tk-Namen werden im Hintergrund automagisch generiert, die sind eigentlich egal. Die Nummer macht das halt eindeutig, aber das ist ein Implementierungsdetail das sich auch ändern könnte.

Der Knopf “kennt“ das zugehörige `Entry`-Objekt weil man das beim `command`-Rückruf mit einbaut. Zum Beispiel mit `functools.partial()`.

Auch beim ursprünglichen Beitrag war nichts rekursiv. Rekursiv bedeutet, das sich eine Funktion, eventuell indirekt, selbst aufruft, das heisst zur gleichen Zeit mehrfach *aktiviert* ist. Also technisch gesprochen für die gleiche Funktion zur gleichen Zeit mehr als einmal lokale Variablen existieren (im gleichen Thread). Das ist was anderes als wenn eine Funktion sich beispielsweise selbst bei einer Ereignisschleife als Rückruf registriert, der Rückruf aber erst erfolgt wenn die Funktion abgearbeitet ist.
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

So ich habe den Code jetzt erstmal soweit, dass es "ok" funktioniert :wink: . Und dann möchte ich den natürlich auch nicht vorenthalten.

Und @__blackjack__ gibt es eigentlich die Möglichkeit den Forumseintrag umzubenennen? Dann könnte ich den Namen zu etwas passenderem umbenennen.

Hier der Code:

PS: Derzeit habe ich noch einen Schwierigkeit damit die Aktualisierung der Comboboxen zuverlässig funktioniert, aber daran arbeite ich noch.
Ich werde den Code aktualisieren sobald ich etwas zuverlässiges ausgearbeitet habe.

Code: Alles auswählen

import tkinter as tk
from tkinter import ttk,filedialog
from functools import partial

class App:
    def __init__(self, root):
        self.root = root
        self.root.title("Test")
        self.siglist = ["CAN", "LIN"]


        frame = tk.Frame(root)
        self.entries = []
        self.dbcs = []
        frame.grid()

        self.umrandung = ttk.LabelFrame(frame, text="Paths")
        self.umrandung.grid(row=0, column=0)

        self.dbc_canvas = tk.Canvas(self.umrandung, borderwidth=0, width=550)
        self.dbc_frame = tk.Frame(self.dbc_canvas)
        self.vsb = tk.Scrollbar(self.umrandung, orient="vertical",command=self.dbc_canvas.yview)
        self.dbc_canvas.configure(yscrollcommand=self.vsb.set)

        self.dbc_canvas.grid(row=1,column=0)
        self.vsb.grid(row=1,column=1,sticky=tk.NS)
        self.dbc_canvas.create_window((4,4), window=self.dbc_frame, anchor="nw", tags="self.dbc_frame")

        self.dbc_frame.bind("<Configure>", self.onFrameConfigure)

        button_frame = tk.LabelFrame(self.umrandung, relief="flat", text="")
        button_frame.grid(row=0, column=0)

        add_button = ttk.Button(button_frame, text="ADD", command=partial(self.add_more))
        add_button.grid(row=0, column=0, sticky=tk.NSEW)

        del_button = ttk.Button(button_frame, text="Delete", command=self.loose_some)
        del_button.grid(row=0, column=1, sticky=tk.NSEW)

        print_button = ttk.Button(button_frame, text="Print", command=partial(self.print_all))
        print_button.grid(row=0, column=2, sticky=tk.NSEW)

        for _ in range(4):
            self.create_entry()


    def onFrameConfigure(self, event):
        '''Reset the scroll region to encompass the inner frame'''
        self.dbc_canvas.configure(scrollregion=self.dbc_canvas.bbox("all"))

    def create_entry(self):
        combobox = ttk.Combobox(self.dbc_frame, width=8)
        combobox['values'] = self.siglist
        combobox['state'] = 'readonly'
        combobox.current(0)

        spinbox = ttk.Spinbox(self.dbc_frame, from_=1, to=48, width=3)

        entry = ttk.Entry(self.dbc_frame, width=60)

        combobox.unbind('<MouseWheel>')
        combobox.bind("<Enter>", partial(self.delete_dbc_entries, combobox, spinbox, entry))
        # combobox.bind("<Leave>", partial(self.update_dbc_entries, combobox, spinbox, entry))
        combobox.bind("<<ComboboxSelected>>", partial(self.update_dbc_entries, combobox, spinbox, entry))
        #TODO Scenario handeln in dem die COMBOBOX selektiert wird der Inhalt allerdings nicht geändert wird
        spinbox.bind("<Enter>", partial(self.delete_dbc_entries, combobox, spinbox, entry))
        spinbox.bind("<Leave>", partial(self.update_dbc_entries, combobox, spinbox, entry))

        button = ttk.Button(self.dbc_frame,text="...", width=2, command=partial(self.insert_dbc_path, combobox ,spinbox ,entry))
        row = (combobox, spinbox, entry, button)
        for column_index, item in enumerate(row):
            item.grid(row=len(self.entries), column=column_index, sticky=tk.E, padx=5, pady=5)
            spinbox.set(len(self.entries) + 1)
        self.entries.append(row)

        return entry

    def insert_dbc_path(self, combobox ,spinbox ,entry):
        oldentry = entry.get()
        output_pfad = tk.filedialog.askopenfilename()
        if output_pfad != "":
            if oldentry != output_pfad:
                try:
                    self.dbcs.remove((combobox.get(),spinbox.get(),"=", entry.get()))
                    entry.delete(0, tk.END)
                    entry.insert(0, output_pfad)
                    self.dbcs.append((combobox.get(),spinbox.get(),"=", entry.get()))
                except:
                    entry.delete(0, tk.END)
                    entry.insert(0, output_pfad)
                    self.dbcs.append((combobox.get(),spinbox.get(),"=", entry.get()))


    def delete_dbc_entries(self,combobox,spinbox,entry, event):
        self.saved_values=combobox.get()
        print("first",event.state)
        if entry.get() != "":
            try:
                self.dbcs.remove((combobox.get(),spinbox.get(),"=", entry.get()))
            except:
                print("")

    def update_dbc_entries(self,combobox,spinbox,entry, event):
        print("second",event.state)
        if entry.get() != "":
            self.dbcs.append((combobox.get(),spinbox.get(),"=", entry.get()))

    def add_more(self):
        self.create_entry()

    def print_all(self):
        for data in self.dbcs:
            print("".join(data))

    def loose_some(self):
        if len(self.entries) > 4:
            items = self.entries.pop()
            for item in items:
                item.grid_forget()

def main():
    root = tk.Tk()
    app = App(root)
    root.mainloop()

if __name__ == "__main__":
    main()
Sirius3
User
Beiträge: 17812
Registriert: Sonntag 21. Oktober 2012, 17:20

`frame.grid()` ohne Parameter sieht etwas komisch aus. Möchtest Du eigentlich `frame.pack()`?
partial braucht man nur, wenn man auch tatsächlich weitere Parameter übergeben möchte.
Nackte except`s darf es nicht geben. Was für ein Fehler soll denn in insert_dbc_path abgefangen werden?

Es ist immer schlecht, wenn man die Datenhaltung getrennt von der GUI hat, weil es schwierig wird, die Daten synchron zu halten.
Der erste Eintrag in der Liste dbcs sollte auch der ersten Zeile in Deiner GUI entsprechen. So wie es jetzt ist, wird munter verschiedenste Einträge gelöscht oder hinzugefügt. Dann sollte sichergestellt sein, dass jedes Update auch wirklich synchron erfolgt. `remove` ist da die falsche Methode.
Im Prinzip steckt ja in dbcs und entries die selbe Information. Für was wird diese Dopplung gebraucht?
Warum ist der dritte Eintrag Deines Tuples immer "="? Was bedeutet das? Kann das mal auch "<" oder ">" sein?
Benutzeravatar
__blackjack__
User
Beiträge: 13227
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TheBombTuber: Feste Grössenvorgaben in Pixeln sind nicht gut. Das ist bei mir unbenutzbar weil die hart vorgegebenen 550 Pixel Canvas-Breite nicht ausreichen um die Eingabefelder für die Pfade anzuzeigen. Die Schaltflächen zur Dateiauswahl sind überhaupt nicht mehr sichtbar und damit auch nicht per Maus zu erreichen. Vergrössern des Fensters bringt nichts, weil da nichts mitwächst.

Also sollte man mindestens dafür sorgen, dass das verändern der Fenstergrösse auch einen Einfluss auf den Fensterinhalt hat, so dass man das gross genug ziehen kann damit alles sichtbar wird was man mit der Maus erreichen können soll.

`root`, `umrandung`, und `vsb` muss man nicht an das `App`-Objekt binden.

`frame` ist sinnlos weil das einzige was da rein gesteckt wird ein weiterer Frame ist.

Ein `LabelFrame` bei dem man die Umrandung weg konfiguriert und keinen Text setzt, macht keinen Sinn.

`partial()` mit nur einer Funktion als Argument macht auch keinen Sinn.

Wenn es für Argumente im `tkinter`-Modul Konstanten gibt, sollte man die benutzen, statt den Wert selbst noch mal als Zeichenkette zu schreiben.

Bei `ttk.Combobox()` werden Optionen gleich nach dem erstellen gesetzt, die man auch beim erstellen schon hätte angeben können.

Das Ergebnis von `askopenfilename()` sollte man nur auf ”truthiness” testen und nicht explizit mit der leeren Zeichenkette vergleichen. Es gibt auch Tk/`tkinter`-Versionen und Situationen wo an der Stelle ein leeres Tupel zurückgegeben wird wenn der Benutzer den Dialog abbricht.

Die beiden ``if``\s lassen sich auch sehr einfach zu einem zusammenfassen.

Keine nackten ``except:`` ohne konkrete Ausnahme(n). Da behandelt man *alle* Ausnahmen, auch solche mit denen man gar nicht gerechnet hat. Fehler fallen dann nicht auf und/oder sind nur schwer zu finden.

In `insert_dbc_path()` wird im ``except``-Zweig alles bis auf die erste Zeile im ``try``-Block wiederholt, also eigentlich sollte der ``try``-Block nur diese Zeile umfassen. Und die Ausnahme die da behandelt werden soll, ist vermute ich mal der `ValueError` der auftritt wenn das Element nicht in der Liste ist. (Was ein bisschen komisch wäre, und doch eigentlich auf ein Problem hinweisen würde‽)

Wenn man nichts machen will, aus syntaktischen Gründen aber etwas schreiben muss, dann gibt es die ``pass``-Anweisung. Ein ``print("")`` macht da keinen Sinn.

In `delete_dbc_entries()` wird plötzlich ein Attribut `saved_values` eingeführt, was es vorher nicht gab und das auch nirgends verwendet wird‽

`add_more()` ist nur ein anderer Name für `create_entry()`.

Zwischenstand (ungetestet):

Code: Alles auswählen

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


class App:
    def __init__(self, root):
        root.title("Test")
        self.siglist = ["CAN", "LIN"]
        self.entries = []
        self.dbcs = []

        umrandung = ttk.LabelFrame(root, text="Paths")
        umrandung.pack(expand=True, fill=tk.BOTH)
        umrandung.columnconfigure(0, weight=1)
        umrandung.rowconfigure(1, weight=1)
        
        button_frame = tk.Frame(umrandung)
        button_frame.grid(row=0, column=0)
        for i, (text, action) in enumerate(
            [
                ("ADD", self.create_entry),
                ("Delete", self.loose_some),
                ("Print", self.print_all),
            ]
        ):
            ttk.Button(button_frame, text=text, command=action).grid(
                row=0, column=i, sticky=tk.NSEW
            )

        self.dbc_canvas = tk.Canvas(umrandung, borderwidth=0, width=550)
        self.dbc_canvas.grid(row=1, column=0, sticky=tk.NSEW)
        self.dbc_frame = tk.Frame(self.dbc_canvas)
        scrollbar = tk.Scrollbar(
            umrandung, orient=tk.VERTICAL, command=self.dbc_canvas.yview
        )
        scrollbar.grid(row=1, column=1, sticky=tk.NS)
        self.dbc_canvas.configure(yscrollcommand=scrollbar.set)

        self.dbc_canvas.create_window(
            (4, 4), window=self.dbc_frame, anchor=tk.NW
        )

        self.dbc_frame.bind("<Configure>", self.on_frame_configure)

        for _ in range(4):
            self.create_entry()

    def on_frame_configure(self, _event=None):
        """
        Reset the scroll region to encompass the inner frame.
        """
        self.dbc_canvas.configure(scrollregion=self.dbc_canvas.bbox(tk.ALL))

    def create_entry(self):
        combobox = ttk.Combobox(
            self.dbc_frame, width=8, values=self.siglist, state="readonly"
        )
        combobox.current(0)

        spinbox = ttk.Spinbox(self.dbc_frame, from_=1, to=48, width=3)

        entry = ttk.Entry(self.dbc_frame, width=60)

        combobox.unbind("<MouseWheel>")
        combobox.bind(
            "<Enter>",
            partial(self.delete_dbc_entries, combobox, spinbox, entry),
        )
        combobox.bind(
            "<<ComboboxSelected>>",
            partial(self.update_dbc_entries, combobox, spinbox, entry),
        )
        #
        # TODO Scenario handeln in dem die COMBOBOX selektiert wird der Inhalt
        #   allerdings nicht geändert wird.
        #
        spinbox.bind(
            "<Enter>",
            partial(self.delete_dbc_entries, combobox, spinbox, entry),
        )
        spinbox.bind(
            "<Leave>",
            partial(self.update_dbc_entries, combobox, spinbox, entry),
        )

        button = ttk.Button(
            self.dbc_frame,
            text="...",
            width=2,
            command=partial(self.insert_dbc_path, combobox, spinbox, entry),
        )
        row = (combobox, spinbox, entry, button)
        for column_index, item in enumerate(row):
            item.grid(
                row=len(self.entries),
                column=column_index,
                sticky=tk.E,
                padx=5,
                pady=5,
            )
            spinbox.set(len(self.entries) + 1)
        self.entries.append(row)

        return entry

    def insert_dbc_path(self, combobox, spinbox, entry):
        old_output_path = entry.get()
        output_path = filedialog.askopenfilename()
        if output_path and output_path != old_output_path:
            try:
                self.dbcs.remove(
                    (combobox.get(), spinbox.get(), "=", entry.get())
                )
            except ValueError:
                pass  # Intentionally ignored.

            entry.delete(0, tk.END)
            entry.insert(0, output_path)
            self.dbcs.append((combobox.get(), spinbox.get(), "=", entry.get()))

    def delete_dbc_entries(self, combobox, spinbox, entry, _event=None):
        if entry.get():
            try:
                self.dbcs.remove(
                    (combobox.get(), spinbox.get(), "=", entry.get())
                )
            except ValueError:
                pass  # Intentionally ignored.

    def update_dbc_entries(self, combobox, spinbox, entry, _event=None):
        if entry.get():
            self.dbcs.append((combobox.get(), spinbox.get(), "=", entry.get()))

    def print_all(self):
        for data in self.dbcs:
            print("".join(data))

    def loose_some(self):
        if len(self.entries) > 4:
            for item in self.entries.pop():
                item.grid_forget()


def main():
    root = tk.Tk()
    _app = App(root)
    root.mainloop()


if __name__ == "__main__":
    main()
Jetzt ist dieses ganze aktuell halten von `self.dbcs` umständlich. Muss man das überhaupt? Kann man das nicht einfach immer dann erstellen wenn es benötigt wird?

Dann steht ``(combobox.get(), spinbox.get(), "=", entry.get())`` nur noch einmal im Code und das ganze aktualisieren von `self.dbcs` fällt weg. `insert_dbc_path()` braucht dann nur noch `entry` als Argument. `delete_dbc_entries()` und `update_dbc_entries() werden gar nicht mehr benötigt. Dadurch fällt eine Menge Code einfach weg. Auch in `create_entry()`.

Ungetestet:

Code: Alles auswählen

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


class App:
    def __init__(self, root):
        root.title("Test")
        self.siglist = ["CAN", "LIN"]
        self.entries = []

        umrandung = ttk.LabelFrame(root, text="Paths")
        umrandung.pack(expand=True, fill=tk.BOTH)
        umrandung.columnconfigure(0, weight=1)
        umrandung.rowconfigure(1, weight=1)

        button_frame = tk.Frame(umrandung)
        button_frame.grid(row=0, column=0)
        for i, (text, action) in enumerate(
            [
                ("ADD", self.create_entry),
                ("Delete", self.loose_some),
                ("Print", self.print_all),
            ]
        ):
            ttk.Button(button_frame, text=text, command=action).grid(
                row=0, column=i, sticky=tk.NSEW
            )

        self.dbc_canvas = tk.Canvas(umrandung, borderwidth=0, width=550)
        self.dbc_canvas.grid(row=1, column=0, sticky=tk.NSEW)
        self.dbc_frame = tk.Frame(self.dbc_canvas)
        scrollbar = tk.Scrollbar(
            umrandung, orient=tk.VERTICAL, command=self.dbc_canvas.yview
        )
        scrollbar.grid(row=1, column=1, sticky=tk.NS)
        self.dbc_canvas.configure(yscrollcommand=scrollbar.set)

        self.dbc_canvas.create_window(
            (4, 4), window=self.dbc_frame, anchor=tk.NW
        )

        self.dbc_frame.bind("<Configure>", self.on_frame_configure)

        for _ in range(4):
            self.create_entry()

    @property
    def dbcs(self):
        return (
            (combobox.get(), spinbox.get(), "=", entry.get())
            for combobox, spinbox, entry in self.entries
            if entry.get()
        )

    def on_frame_configure(self, _event=None):
        """
        Reset the scroll region to encompass the inner frame.
        """
        self.dbc_canvas.configure(scrollregion=self.dbc_canvas.bbox(tk.ALL))

    def create_entry(self):
        combobox = ttk.Combobox(
            self.dbc_frame, width=8, values=self.siglist, state="readonly"
        )
        combobox.current(0)
        combobox.unbind("<MouseWheel>")
        spinbox = ttk.Spinbox(self.dbc_frame, from_=1, to=48, width=3)
        entry = ttk.Entry(self.dbc_frame, width=60)
        button = ttk.Button(
            self.dbc_frame,
            text="...",
            command=partial(self.set_dbc_path, entry),
        )
        row = (combobox, spinbox, entry, button)
        for column_index, item in enumerate(row):
            item.grid(
                row=len(self.entries),
                column=column_index,
                sticky=tk.E,
                padx=5,
                pady=5,
            )
            #
            # BUG Der Wertebereich von der `spinbox` ist begrenzt, aber hier
            #     könnte ein grösserer Wert gesetzt werden!
            # 
            spinbox.set(len(self.entries) + 1)
        self.entries.append(row)

        return entry

    def set_dbc_path(self, entry):
        output_path = filedialog.askopenfilename()
        if output_path:
            entry.delete(0, tk.END)
            entry.insert(0, output_path)

    def print_all(self):
        for data in self.dbcs:
            print("".join(data))

    def loose_some(self):
        if len(self.entries) > 4:
            for item in self.entries.pop():
                item.grid_forget()


def main():
    root = tk.Tk()
    _app = App(root)
    root.mainloop()


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

@__blackjack__ UFF :shock: Da muss ich mich erstmal in vieles reinlesen wie es ausschaut. Aber danke für die ausführliche Antwort.

Ich habe grade noch auf die schnelle zwei dinge in dem Code ausgebessert.

Einmal:

Code: Alles auswählen

@property
def dbcs(self):
	return (
        (combobox.get(), spinbox.get(), "= ", entry.get())
        for combobox, spinbox, entry, *rest in self.entries
        if entry.get()
        )
Dort bekommt man ohne das *rest einen Fehler da ja auch der Button in entries gespeichert ist.

Und den "BUG" habe ich mit einem IF gestoppt. Ist das so eine Saubere Lösung?

VORHER:

Code: Alles auswählen

spinbox.set(len(self.entries) + 1)
NACHHER:

Code: Alles auswählen

if len(self.entries) < int(spinbox.config('to')[4]):
	spinbox.set(len(self.entries) + 1)
else:
	spinbox.set(int(spinbox.config('to')[4]))
@sirius Warum ist der dritte Eintrag Deines Tuples immer "="? Was bedeutet das? Kann das mal auch "<" oder ">" sein?

Das ist die Schreibweise wie ich sie in meiner eigentlichen APP später brauche. daher das "= " da mit drin.

Und danke an euch beide für euren Einsatz hier im Forum und die schnellen Antworten meinen Größten Respekt dafür.
Sirius3
User
Beiträge: 17812
Registriert: Sonntag 21. Oktober 2012, 17:20

Aber wenn das "=" immer gleich ist, kann man das auch weglassen.
TheBombTuber
User
Beiträge: 49
Registriert: Samstag 9. September 2017, 15:48

Nee ich brauch das da so, damit erspare ich mir das spätere zusammenbauen der einzelnen Elemente.

Oder meinst du etwas anderes?
Sirius3
User
Beiträge: 17812
Registriert: Sonntag 21. Oktober 2012, 17:20

Meinst Du mit "zusammenbauen der Elemente" das:

Code: Alles auswählen

        for data in self.dbcs:
            print("".join(data))
Das sollte sowieso anders aussehen, z.B:

Code: Alles auswählen

        for signal, channel, path in self.dbcs:
            print(f"{signal}{channel}={path}")
Benutzeravatar
__blackjack__
User
Beiträge: 13227
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@TheBombTuber: `cget()` ist einfacher als mit `config()` abzufragen. Das liefert nur den einen Wert und auch gleich als Zahl. Das ``if`` kann man sich sparen in dem man einfach den kleineren der beiden Werte setzt:

Code: Alles auswählen

        spinbox.set(min(len(self.entries) + 1, spinbox.cget("to")))
Den Button hatte ich übersehen/vergessen. Gut das ich „ungetestet“ dran geschrieben habe. 😇

Bei Tupeln bin ich persönlich auch aus diesem Grund gerne relativ schnell dabei eher eine Klasse zu schreiben, oder zumindest `collections.namedtuple` zu verwenden.

Ungetestet:

Code: Alles auswählen

#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk, filedialog


class DBCRowUi:
    def __init__(self, master, siglist, number):
        self.combobox = ttk.Combobox(
            master, width=8, values=siglist, state="readonly"
        )
        self.combobox.current(0)
        self.combobox.unbind("<MouseWheel>")
        self.spinbox = ttk.Spinbox(master, from_=1, to=48, width=3)
        self.spinbox.set(min(number, self.spinbox.cget("to")))
        self.entry = ttk.Entry(master, width=60)
        self.button = ttk.Button(master, text="...", command=self.set_path)

    def __str__(self):
        return f"{self.combobox.get()}{self.spinbox.get()}={self.entry.get()}"

    @property
    def widgets(self):
        return (self.combobox, self.spinbox, self.entry, self.button)

    @property
    def is_empty(self):
        return not self.entry.get()

    def layout_row(self, row_index, **kwargs):
        for column_index, widget in enumerate(self.widgets):
            widget.grid(row=row_index, column=column_index, **kwargs)

    def forget_layout(self):
        for widget in self.widgets:
            widget.grid_forget()

    def set_path(self):
        output_path = filedialog.askopenfilename()
        if output_path:
            self.entry.delete(0, tk.END)
            self.entry.insert(0, output_path)


class App:
    def __init__(self, root):
        root.title("Test")
        self.siglist = ["CAN", "LIN"]
        self.entries = []

        umrandung = ttk.LabelFrame(root, text="Paths")
        umrandung.pack(expand=True, fill=tk.BOTH)
        umrandung.columnconfigure(0, weight=1)
        umrandung.rowconfigure(1, weight=1)

        button_frame = tk.Frame(umrandung)
        button_frame.grid(row=0, column=0)
        for i, (text, action) in enumerate(
            [
                ("ADD", self.create_entry),
                ("Delete", self.loose_some),
                ("Print", self.print_all),
            ]
        ):
            ttk.Button(button_frame, text=text, command=action).grid(
                row=0, column=i, sticky=tk.NSEW
            )

        self.dbc_canvas = tk.Canvas(umrandung, borderwidth=0, width=550)
        self.dbc_canvas.grid(row=1, column=0, sticky=tk.NSEW)
        self.dbc_frame = tk.Frame(self.dbc_canvas)
        scrollbar = tk.Scrollbar(
            umrandung, orient=tk.VERTICAL, command=self.dbc_canvas.yview
        )
        scrollbar.grid(row=1, column=1, sticky=tk.NS)
        self.dbc_canvas.configure(yscrollcommand=scrollbar.set)

        self.dbc_canvas.create_window(
            (4, 4), window=self.dbc_frame, anchor=tk.NW
        )

        self.dbc_frame.bind("<Configure>", self.on_frame_configure)

        for _ in range(4):
            self.create_entry()

    @property
    def dbcs(self):
        return (entry_ui for entry_ui in self.entries if not entry_ui.is_empty)

    def on_frame_configure(self, _event=None):
        """
        Reset the scroll region to encompass the inner frame.
        """
        self.dbc_canvas.configure(scrollregion=self.dbc_canvas.bbox(tk.ALL))

    def create_entry(self):
        entry_ui = DBCRowUi(
            self.dbc_frame, self.siglist, len(self.entries) + 1
        )
        entry_ui.layout_row(len(self.entries), sticky=tk.E, padx=5, pady=5)
        self.entries.append(entry_ui)

    def print_all(self):
        for data in self.dbcs:
            print(data)

    def loose_some(self):
        if len(self.entries) > 4:
            for item in self.entries.pop():
                item.grid_forget()


def main():
    root = tk.Tk()
    _app = App(root)
    root.mainloop()


if __name__ == "__main__":
    main()
“There will always be things we wish to say in our programs that in all known languages can only be said poorly.” — Alan J. Perlis
Antworten