Eigene TKinter-Klasse, kein Zugriff auf Entry-Field mehr

Fragen zu Tkinter.
Antworten
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Hallo, ich sitze schon seit Stunden verzweifelt an einem Problem.
Mir wurde geraten, für TKinter eine eigene Klasse anzulegen, was ich nun gemacht habe. Nun soll wie vorher das "Entry"-Feld ausgelesen werden, wenn jemand etwas eingibt und mit <RETURN> abschließt.
Hierfür gibt es dieses Binding:

Code: Alles auswählen

 
 entryField.focus()
 entryField.bind('<Return>', self.getscan())


Somit wird die Funktion <getscan> aufgerufen, die die Eingabe in Großschrift umwandelt und in die Variable <entry> speichert:

Code: Alles auswählen

 def getscan(self):
        self.entry = str.upper(self.entryField.get)
        print(self.entry)
Nun kann ich machen, was ich will, ich bekomme die Meldung "AttributeError: 'GUI' object has no attribute 'entryField'".
Wie ist es möglich, auf das Entry-Feld zuzugreifen? Entweder von außerhalb der Klasse oder innerhalb der Klasse in einer anderen Funktion?
Da ich mich erst an die objektorientierte Programmierung gewöhnen muss, liegt der Fehler sicher hier?

Thema am Rande: Ich möchte zum automatischem Update des Labels die Stringvar-Variante von TKinter nutzen.
Das Label soll also immer die Eingabe des Entry-Felds (abgeschlossen mit Return) anzeigen. Auch hier hatte ich schon diverse Probleme, dass es nicht funktioniert.
Mache ich hier auch etwas falsch?

Danke für eure Hilfe im Voraus. Trotz mehrerer Bücher und Google komme ich zu keinem schnellen Ergebnis.
P.S.: Das ist eine konkrete Aufgabe, für die ich eine Deadline habe. Parallel dazu versuche ich, mich gründlich und intensiv in Python und OOP einzuarbeiten, doch das dauert natürlich lange.

Hier der gesamte Quelltext. Die Funktion der Buttons könnt ihr ignorieren, den Code für die Funktionen habe ich aufgrund der Übersichtlichkeit herausgelöscht.

Code: Alles auswählen

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


'''
class CONTROL():

    def __init__(self):

    def getscan(self):
        print("getscanfunction")
        GUI.seriallabeltext.set = str.upper(GUI.entryField.get())
        GUI.entryField.delete(0, 100)
        print(GUI.seriallabeltext.get())
'''


class GUI(tk.Canvas):

    def __init__(self, master, entry):
        super().__init__(master)

        self.entry = entry


        #Text für Labels, TKINTER spezielle Stringvariablen, um Label automatisch upzudaten, wenn sich diese ändert
        self.seriallabeltext = tk.StringVar() #Binding variable
        self.seriallabeltext.set("-")
        self.statuslabeltext = tk.StringVar()  # Binding variable
        self.statuslabeltext.set("Enter code")

        #CREATE GUI LAYOUT, LABELS, BUTTONS and ENTRY FIELD
        canvas_front = tk.Canvas(self, height=480, width=800)
        canvas_front.pack()

        frame = tk.Frame(canvas_front, bg='#00cc99')
        frame.place(anchor='nw', height=480, width=800)

        label_top = tk.Label(frame, text="v0.1", bg='white', fg='#00cc99')
        label_top.place(relx=0, rely=0, relwidth=1, relheight=0.10)
        label_status = tk.Label(frame, textvariable=self.statuslabeltext, font=('Arial', 24), bg='white', fg='#00cc99', highlightbackground='black', borderwidth=1, relief='solid', wraplength=500)
        label_status.place(relx=0.3, rely=0.10, relwidth=0.7, relheight=0.45)
        label_serial = tk.Label(frame, textvariable=self.seriallabeltext, font=('Arial', 24), bg='white', fg='#00cc99', highlightbackground='black', borderwidth=1, relief='solid', wraplength=500)
        label_serial.place(relx=0.3, rely=0.55, relwidth=0.7, relheight=0.45)

        button_repeat = tk.Button(frame, text="Repeat Scan", font=('Arial', 16), bg='#00cc99', fg='white', activebackground='#00cc99', activeforeground='white', command=lambda: updatelabels('RepeatScan', 'Please scan', 'full'))
        button_sample = tk.Button(frame, text="Test sample", font=('Arial', 16), bg='#00cc99', fg='white', activebackground='#00cc99', activeforeground='white', command=lambda: changelabelcolor('green', 'blue'))

        button_repeat.place(relx=0.00, rely=0.10, relwidth=0.3, relheight=0.45)
        button_sample.place(relx=0.00, rely=0.55, relwidth=0.3, relheight=0.45)

        #entry field
        entryField = tk.Entry(frame, font=('Arial', 24), bg='white', fg='#00cc99', highlightbackground='black', justify='center', borderwidth=1, relief='solid')
        entryField.place(relx=0.35, rely=0.35, relwidth=0.6, relheight=0.15)
        entryField.focus()
        entryField.bind('<Return>', self.getscan())

    def getscan(self):
        self.entry = str.upper(self.entryField.get)
        print(self.entry)
        #entryField.lower() #place entry field behind main window (hide it, result will be displayed in Label)

def main():
    '''
    Um tkinter zu initialisieren, muss ein ROOT-Widget erzeigt werden.
    Dies geschieht mit dem Aufruf Tk().
    Das Root-Widget muss erzeugt werden, bevor irgendwelche anderen Widgets benutzt werden.
    Es kann in jeder Anwendung nur ein Root-Widget geben.
    '''
    root = tk.Tk()
    eingabe = " "
    root.title("GUI")
    root.config(cursor='none')
    #root.attributes('-fullscreen', True)
    gui = GUI(root, eingabe)
    gui.pack()

    root.mainloop()

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

Das hat nichts mit der Klassendefinition zu tun. Das ist Grundlagenwissen, wann eine Funktion aufgefunden wird, und wann eine Funktion als Parameter übergeben wird. Den selben Fehler hast du in der anderen Richtung in getscan nochmal gemacht.
entryField ist kein Attribut. Methoden werden auf der Instanz und nicht über die Klasse aufgerufen (str.upper).
partial und partialmethod werden importiert, aber nicht benutzt.
Benutzeravatar
peterpy
User
Beiträge: 188
Registriert: Donnerstag 7. März 2013, 11:35

Hallo blutec,

Code: Alles auswählen

entryField = tk.Entry(frame, font=('Arial', 24), bg='white', fg='#00cc99', highlightbackground='black', justify='center', borderwidth=1, relief='solid')
        entryField.place(relx=0.35, rely=0.35, relwidth=0.6, relheight=0.15)
        entryField.focus()
        entryField.bind('<Return>', self.getscan())
setze den Namen entryField je ein self. davor.

Gruss
Peter
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Hallo,

Sirius3 schreibt:
Das hat nichts mit der Klassendefinition zu tun. Das ist Grundlagenwissen, wann eine Funktion aufgefunden wird, und wann eine Funktion als Parameter übergeben wird.
Sicher ist das Grundlagenwissen, das ich anhand dieses Beispiels gerne erlernen möchte. Vielleicht kannst du mir das oben geschriebene anhand meines Codes etwas besser erläutern?
Ich habe gegoogelt, mir meine beiden Fachbücher angesehen und einiges über Instanzvariablen, Klassenvariablen und Methoden gelesen, kann es aber nicht auf das konkrete Beispiel übertragen.
Die Verwendung mit TKinter macht es auch nicht einfacher und verwirrt mich zusätzlich, jedoch wird es für die Aufgabe benötigt. Bevor ich hier frage, versuche ich es ohne Hilfe zu lösen, aber hier beisse ich mir gerade die Zähne aus.
__deets__
User
Beiträge: 14541
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was ist der Unterschied zwischen den beiden print statements? Wenn du das begriffen hast, denk mal darüber nach, wie solch das zu deinem getscan verhält.

Code: Alles auswählen

der foo():
     return “hallo”

print(foo())
print(foo)
Nachtrag: soweit waren wir ja schon mal: viewtopic.php?f=1&t=49695#p373310
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Code: Alles auswählen

der foo():
     return “hallo”

print(foo())
print(foo)
print(foo()) ist ein Funktionsaufruf von foo, d.h. der Rückgabewert der Funktion wird ausgegeben
print(foo) ist die Ausgabe des Inhalts der Variable foo.

Soweit, sogut, da hab ich wohl mit den beiden Klammern bei getscan einen Fehler gemacht. Der Hinweis von peterpy hat mich auch ein ganzes Stück weiter gebracht. Danke!

Ich habe nun alles nochmal neu und Schritt für Schritt neu geschrieben und nach und nach alles ausprobiert.
Das läuft jetzt schon mal schön, ich habe ein Klasse, die meine GUI baut und kann das Entry-Field auslesen.
Das Label updated automatisch seinen Inhalt, wenn die StringVar()-Variable "seriallabeltext" einen anderen Wert bekommt.

nur eine Sache geht nicht:

Code: Alles auswählen

  self.eingabe.bind('<Return>', self.getentry)
oder

Code: Alles auswählen

 self.eingabe.bind('<Return>', self.getentry())
Bei ersterem erscheint der Fehler "getentry() takes 1 positional argument but 2 were given", obwohl ich doch gar nichts übergebe und der Funktionsaufruf beim Druck auf den Button doch der gleiche ist. Dort funktioniert er.
Ausserdem importiere ich wild die Funktionen aus tkinter. Das habe ich aus einem Beispiel abgeschaut, ist aber genauso wie der import mit Sternchen bestimmt auch nicht gut.

Hier der gesamte Code meines letzten Versuchs, Verbesserungsvorschläge und hinweise zum Return-Binding erwünscht:

Code: Alles auswählen

#!/usr/bin/env python3


from tkinter import Tk, BOTH, Entry, Button, StringVar
from tkinter.ttk import Frame, Label, Style
#import tkinter as tk

class Example(Frame):

    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):

        self.seriallabeltext = StringVar()  # Binding variable

        self.master.title("Entry field test")
        self.pack(fill=BOTH, expand=1)

        Style().configure("NewFrame", background="#222")
        self.labeltop = Label(self, textvariable=self.seriallabeltext)
        self.labeltop.config(background='white')
        self.labeltop.grid(column=0, row=0)
        self.eingabe = Entry(self, font=('Arial', 12), bg='white', fg="black", highlightbackground='black', justify='center', borderwidth=1, relief='solid')
        self.eingabe.bind('<Return>', self.getentry)
        self.eingabe.grid(column=0, row=1)

        self.hiButton = Button(self, text="GET", bg="green", fg="white", command=self.getentry)
        self.hiButton.grid(column=0, row=2)

    def getentry(self):

        self.seriallabeltext.set(self.eingabe.get())
        print(self.eingabe.get())
        self.eingabe.delete(0,'end')

def main():

    root = Tk()
    root.geometry("300x280+300+300")
    app = Example()
    root.mainloop()


if __name__ == '__main__':
    main()
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Ah, ich hab´s! Es werden beim Binding meiner Return-Taste tatsächlich 2 Argumente übergeben: ein "self" und ein "string".
Der String hat den Inhalt: <KeyPress event state=Mod1 keysym=Return keycode=13 char='\r' x=89 y=33>

So geht es nun auch mit dem Binding der Return-Taste:

Code: Alles auswählen

self.eingabe.bind('<Return>', self.getentry)

Code: Alles auswählen

 def getentry(self, string):
        print("RETURN detected")
        print(string)
        self.seriallabeltext.set(self.eingabe.get())
        print(self.eingabe.get())
        self.eingabe.delete(0, 'end')
__deets__
User
Beiträge: 14541
Registriert: Mittwoch 14. Oktober 2015, 14:29

Jein. Deine Antwort auf meine Frage war schon fragwürdig. Ja, print(foo) gibt das Objekt auf das der Name foo zeigt aus. Aber der entscheidende Teil ist, dass das ein Funktionsobjekt ist. Das du oder eben auch tkinter aufrufen können. Und im Fall von tkinter vieles eben per Rückruffunktion gelöste ist, weil die Natur von GUIs ereignisbasiert ist.

Und auch hier ist das Verständnis wieder leicht daneben. Es werden keine zwei Argument übergeben. Sondern nur eines. Was auch klar in der Dokumentation von bind steht.

https://effbot.org/tkinterbook/tkinter- ... ndings.htm

“””
If an event matching the event description occurs in the widget, the given handler is called with an object describing the event.“””

Weder ist das also ein String. Noch werden zwei Argumente übergeben. Es ist EIN Argument, und das ist ein Event-Objekt.

Der Unterschied liegt in “self.methode”. OHNE Klammern. Das erzeugt eine “bound method”, die das self-argument schon angebunden hat. Und darum nur noch den Rest an Argumenten benötigt.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@blutec: jetzt hast Du noch den Fehler, dass Du getentry an zwei verschiedenen Stellen benutzt, mit bind das einen weiteren Parameter übergibt, und als Button, ohne diesen Parameter. Sowas löst man mit default-Werten für Parameter.

Die Methode initUI ist eigentlich überflüssig, weil das direkt in __init__ stehen kann.
Die Parameter, die man an einen Frame übergeben kann, sollten an super.__init__ weitergereicht werden. Da kommt hinzu, dass Du auf Master zugreifst, den Master aber beim Erstellen nicht explizit angibst. Du solltest auch in einer Instanz direkt Master ändern. Ein Frame sollte sich auch nicht selbst positionieren. Das Setzen eines globalen Styles hat dort auch nichts zu suchen.

Code: Alles auswählen

from tkinter import Tk, BOTH, Entry, Button, StringVar
from tkinter.ttk import Frame, Label, Style

class Example(Frame):
    def __init__(self, *args, **kw):
        super().__init__(*args, **kw)
        self.seriallabeltext = StringVar()  # Binding variable
        Label(self, textvariable=self.seriallabeltext, background='white').grid(column=0, row=0)
        self.eingabe = Entry(self, font=('Arial', 12), bg='white', fg="black",
            highlightbackground='black', justify='center', borderwidth=1, relief='solid')
        self.eingabe.bind('<Return>', self.getentry)
        self.eingabe.grid(column=0, row=1)
        Button(self, text="GET", bg="green", fg="white", command=self.getentry).grid(column=0, row=2)

    def getentry(self, event=None):
        self.seriallabeltext.set(self.eingabe.get())
        print(self.eingabe.get())
        self.eingabe.delete(0,'end')

def main():
    root = Tk()
    root.title("Entry field test")
    root.geometry("300x280+300+300")
    Style().configure("NewFrame", background="#222")
    app = Example(root)
    app.pack(fill=BOTH, expand=1)
    root.mainloop()


if __name__ == '__main__':
    main()
Benutzeravatar
peterpy
User
Beiträge: 188
Registriert: Donnerstag 7. März 2013, 11:35

Hallo blutec,

Code: Alles auswählen

def getentry(self, string):
Da kannst Du an Stelle von string auch pipfax reinschreiben.
Richtig wäre:

Code: Alles auswählen

def getentry(self,  event):
Denn so weiss man, dass diese Methode durch einen Event aufgrufen wird.
Zu den Importen:
Warum importierst Du BOTH? Willst Du Masslinien zeichnen?
Üblich ist, import tkinter as tk
Dann schreibst Du

Code: Alles auswählen

self.labeltop = tk.Label(
somit hast Du nur die Elemente, welche benötigt werden, also keine Klasse BOTH

Mit dem Button hast Du dir ein Ei gelegt. Der wird nicht funktionieren, da Du dem Aufruf der Methode getentry noch ein Argument mitgeben musst.
Du kannst den Eventaufruf an eine neue Methode binden, welche dann getentry aufruft. Oder das Argument mit event=None angeben.

Code: Alles auswählen

def getentry(self, event=None):

Code: Alles auswählen

def event_return(self, event):
        self.getentry()
Natürlich musst Du die Eventbindung auch an die neue Methode anpassen.

Wenn Du die Gui in der init Methode erstellst, kannst Du dir die super Methode sparen.
Auch das Label Widget muss nicht an self gebunden werden, es wird ja nur beim Erstellen gebraucht und bracht auch keinen Namen.

Code: Alles auswählen

Label(self, textvariable=self.seriallabeltext,
              background='white').grid(column=0, row=0)
Gruss
Peter
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@peterpy: ich versteh nicht, was Du mit der "Klasse BOTH" sagen willst. Ob man die Namen explizit importiert oder nur das Modul, ist von der Nachvollziehbarkeit egal. Das eine ist beim Schreiben des Imports mehr Aufwand, das andere beim Nutzen der Namen.

Die __init__-Methode von Frame muß man immer aufrufen. Ob mit super oder explizit per Frame.__init__ gibt es viel Diskussion, allgemein wird zweiteres empfohlen.
Antworten