Zugriff auf Funktionsrückgabewert bei .bind() an Entry-Widget

Fragen zu Tkinter.
Antworten
Terahnee
User
Beiträge: 13
Registriert: Dienstag 8. August 2017, 13:48

Hallo zusammen!

Zu meinem Anliegen: Meine Anwendung startet mit einem leeren Eingabefeld was über .focus() den Fokus bekommt. Mit einem Barcode-Scanner wird nun eine achtstellige Artikelnummer gefolgt von einem Enter (bzw. der Barcode-Scanner setzt einen Carriage Return nachdem er einen Code eingelesen hat) eingegeben. An das Eingabefeld habe ich daher einen Eventhandler gehängt der auf das Drücken der Enter-Taste (bzw. den Barcode-Scanner) reagiert. Diese Funktion macht dann mit dem Wert aus dem Eingabefeld eine Datenbankabfrage.
Ziel des ganzen ist dass man nur über das einscannen selbst die Daten angezeigt bekommt. Also ohne eine weitere Taste oder einen Button zu drücken.

Meine Frage ist nun: Wie kann ich mit diesen Daten weiterarbeiten? Ich umgehe das Problem momentan indem ich in der Eventhandler-Funktion die Ausgaben mache, aber das ist ja sicherlich nicht Sinn der Sache. Wenn ich die Datenbankabfrage mit einem "return" zurückgebe, wo landet das dann? Wie kann ich darauf zugreifen? Geht das überhaupt?

Vielen Dank schon mal im Voraus!

Gruß Terahnee

Code: Alles auswählen

import tkinter as tk
import sqlite3


def run_query(query, parameters=()):
    with sqlite3.connect('MeineDatenbank.db') as conn:
        cursor = conn.cursor()
        query_result = cursor.execute(query, parameters)
        conn.commit()
    return query_result


def zeige_artikel_aus_db(keypressevent):
    query = 'SELECT * FROM artikel WHERE id=?'
    parameters = [eingabe.get()]
    artikel = run_query(query, parameters)
    for row in artikel.fetchall():
        print(row[1])
        label_artikelname = tk.Label(root, text=row[1])
        label_artikelname.pack()
        label_beschreibung = tk.Label(root, text=row[2])
        label_beschreibung.pack()
        # ... und so weiter mit allen Informationen


root = tk.Tk()
lb = tk.Label(root, text="Artikelnummer: ")
lb.pack()
artikelnummer = ""
eingabe = tk.Entry(root, textvariable=artikelnummer)
eingabe.pack()
eingabe.bind("<Return>", zeige_artikel_aus_db)
eingabe.focus()
root.geometry("1024x768")
root.mainloop()
__deets__
User
Beiträge: 14480
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das geht nicht - ein Event-Handler hat per Definition keinen Rueckgabewert.

Mir ist auch noch nicht klar, was du eigentlich weiter verarbeiten willst. Du hast doch deine DB-Abfrage gemacht. Jetzt kommt doch eine Nutzerinteraktion? Wenn es doch noch was anderes zu tun gibt, warum machst du das nicht innerhalb der Ereignisbehandlung?
BlackJack

@Terahnee: Wieso ist das nicht Sinn der Sache? Wenn Du in einem `bind()`-Callback Daten zurück gibst, dann bekommt die Tk-Hauptschleife die. Was nicht gut ist weil die erwartet da selbst schon etwas, zum Beispiel ``'break'`` wenn man möchte das das Ereignis nicht noch von Handlern auf anderen Ebenen abgearbeitet werden soll.

Falls das bei Dir mit der Abfrage tatsächlich so funktioniert ist das übrigens nur Zufall. Denn ein `fetchall()` muss es auf dem Rückgabewert von `cursor.execute()` nicht geben, und vor allem dürfte das nicht funktionieren wenn man die Verbindung davor bereits geschlossen hat.

Funktionen sollte nur auf Werte (ausser Konstanten) zugreifen die als Argumente übergeben wurde. `zeige_artikel_aus_db()` sollte `eingabe` nicht einfach so kennen. Solche undurchsichtigen Abhängigkeiten vermeidet man sehr einfach in dem man keine Variablen auf Modulebene definiert. Da sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.

Und für GUI-Programmierung braucht man dann objektorientierte Programmierung (OOP).
Terahnee
User
Beiträge: 13
Registriert: Dienstag 8. August 2017, 13:48

__deets__ hat geschrieben:Das geht nicht - ein Event-Handler hat per Definition keinen Rueckgabewert.

Mir ist auch noch nicht klar, was du eigentlich weiter verarbeiten willst. Du hast doch deine DB-Abfrage gemacht. Jetzt kommt doch eine Nutzerinteraktion? Wenn es doch noch was anderes zu tun gibt, warum machst du das nicht innerhalb der Ereignisbehandlung?
Stimmt, es folgt eine Nutzerinteraktion. Man soll nun in einem weiteren Eingabefeld z.B. eine Anzahl eingeben können und das ganze wird wieder in der Datenbank abgelegt - ungefähr wie bei einer Inventur. Wenn ich das allerdings auch in der Funktion zeige_artikel_aus_db() mache, wird diese aufgebläht und ich habe mal gelernt dass eine Funktion möglichst nur einen Vorgang abhandeln sollte.
BlackJack hat geschrieben:@Terahnee: Falls das bei Dir mit der Abfrage tatsächlich so funktioniert ist das übrigens nur Zufall. Denn ein `fetchall()` muss es auf dem Rückgabewert von `cursor.execute()` nicht geben, und vor allem dürfte das nicht funktionieren wenn man die Verbindung davor bereits geschlossen hat.
Ja, die Abfrage funktioniert tadellos! Mir werden alle Informationen aus der Datenbank ausgegeben. Hm, Zufall? So etwas mag ich eher nicht. :D Wie sollte man das denn besser machen? Die Verbindung ist zwar schon geschlossen, aber das Abfrageergebnis bekomme ich ja durch run_query() und darin wird mir query_result zurück gegeben.
BlackJack hat geschrieben:
Funktionen sollte nur auf Werte (ausser Konstanten) zugreifen die als Argumente übergeben wurde. `zeige_artikel_aus_db()` sollte `eingabe` nicht einfach so kennen. Solche undurchsichtigen Abhängigkeiten vermeidet man sehr einfach in dem man keine Variablen auf Modulebene definiert. Da sollte nur Code stehen der Konstanten, Funktionen, und Klassen definiert. Das Hauptprogramm steht üblicherweise in einer Funktion die `main()` heisst.
Hm, aber zeige_artikel_aus_db() kennt `eingabe` doch nicht einfach, sondern fragt innerhalb der Funktion den Wert von `eingabe` mit eingabe.get() ab, oder verstehe ich dich falsch?
BlackJack hat geschrieben:Und für GUI-Programmierung braucht man dann objektorientierte Programmierung (OOP).
Das eigentliche Programm ist objektorientiert aufgebaut - ich habe nur den betreffenden Part ausgegliedert, damit es übersichtlicher wird. Ich vermute, ich habe damit mehr für Verwirrung gesorgt als mit dem ganzen Code. Daher sind auch keine Variablen auf Modulebene definiert. :D

Danke schon mal für eure Mühe - ich werde mich weiter in das Thema einlesen. Zur Not fliegt halt der "Automatismus" wieder raus. Eventuell geht es auch anders.
BlackJack

@Terahnee: Die Funktion `zeige_artikel_aus_db()` sollte tatsächlich nur den Artikel aus der Datenbank zeigen, mehr würde dem Namen widersprechen.

Das Abfrageergebnis wird nicht zurückgegeben, sondern der Cursor und der ist eigentlich nur solange gültig wie die Verbindung steht zu der er gehört, und die Verbindung wird vor der Rückgabe des Cursors geschlossen. Bei SQLite hätte ich hier erwartet die Datei wird geschlossen und das das dann auch zu einem Fehler führt wenn man danach noch etwas mit dem Cursor macht. Normalerweise müsste man schon innerhalb des ``with`` die `fetchall()`-Methode ausführen.

`zeige_artikel_aus_db()` greift einfach so auf `eingabe` zu. Das wird nicht als Argument übergeben und existiert als Variable auf Modulebene. Erstelle eine Funktion wo der Code ab Zeile 26 drin steht und `zeige_artikel_aus_db()` wird nicht mehr funktionieren weil `eingabe` unbekannt ist.
Terahnee
User
Beiträge: 13
Registriert: Dienstag 8. August 2017, 13:48

Hm... stimmt. Das gefällt mir so nicht. Eventuell habe ich eh schon den falschen Weg eingeschlagen, um meine Vorstellung umzusetzen. Ich möchte, dass mir die Informationen aus der Datenbank direkt nach dem Scanvorgang angezeigt werden - möglichst "sauber", also ohne auf Variablen auf Modulebene zuzugreifen. Wie könnte man so etwas umsetzen? Hm, ich denke, dazu sollte ich lieber einen eigenen Thread aufmachen, oder?
BlackJack

@Terahnee: Ich denke das Programm *ist* bereits objektorientiert?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Terahnee: Wenn Du etwas zurückbekommen willst, musst Du die Daten dann dort hinterlegen, wo sie wieder abgeholt werden können. Man kann so etwas von einem anderen Thread aus auch als Funktion mit Rückgabewert implementieren. Die Funktion blockiert dann eben solange, bis die Daten da sind und liefert sie dann als Rückgabewert zurück.

Das macht man etwa so, dass man den Vorgang auslöst und dann auf ein event wartet. Wenn dann von der GUI oder von sonstwo die Daten da sind, werden sie eventuell auf eine Queue gelegt oder auch in eine Variable und dann das event gesetzt. Die Funktion kann dann wieder weiterlaufen, die Daten von der Queue holen oder aus der Variablen und als Rückgabewert zurückliefern.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Terahnee: hier habe ich mal dargestellt, wie man mit einem Programm mittel einer Funktion bzsw. Methode aus einem Entry Widget auslesen kann.

Code: Alles auswählen

    def program(self):
        while(True):
            value = self.read_input()
            print(value)
            if not value:
                self.app.quit()
                break
Das funktioniert so wie ein input. Man ruft einfach die Funktion zum Auslesen aus. Damit es aber so funktioniert bedarf es etwas an Programmierung:

Code: Alles auswählen

# -*- coding: utf-8 -*-

try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk

from threading import Thread, Event, Lock

class Application(tk.Tk):

    def __init__(self,start,output,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.entry = tk.Entry(self)
        self.entry.pack()
        self.entry['state'] = 'disabled'
        self.start = start
        self.output = output
        self.entry.bind('<Return>',self.output_value)
        self.pull_start()

    def pull_start(self):
        if self.start():
            self.entry['state'] = 'normal'
            self.entry.delete(0,'end')
        self.after(100,self.pull_start)

    def output_value(self,event):
        self.entry['state'] = 'disabled'
        self.output(self.entry.get())


class ThreadWithGUIInput:

    def __init__(self):

        self.start_flag = False
        self.start_lock = Lock()
        self.input_ready = Event()
        self.output_value = ''
        self.app = Application(self.start_gui_input,self.gui_output)

    def start_gui_input(self):
        self.start_lock.acquire()
        ret_val = self.start_flag
        self.start_flag = False
        self.start_lock.release()
        return ret_val

    def gui_output(self,value):
        self.output_value = value
        self.input_ready.set()

    def read_input(self):
        self.start_lock.acquire()
        self.start_flag = True
        self.input_ready.clear()
        self.start_lock.release()
        self.input_ready.wait()
        return self.output_value

    def program(self):
        while(True):
            value = self.read_input()
            print(value)
            if not value:
                self.app.quit()
                break
        
def main():
    loopthread = ThreadWithGUIInput()
    Thread(target=loopthread.program).start()
    loopthread.app.mainloop()
    

if __name__ == '__main__':
    main()
BlackJack

@Alfons Mittelmeyer: Das wird aus einem anderen Thread als dem in dem die Tk-Hauptschleife läuft `quit()` aufgerufen. Das kann gut gehen — muss es aber nicht.

Und ist insgesamt alles komplizierter als es sein müsste. Man muss sich halt einfach auf ereignisbasierte Programmierung einlassen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Das wird aus einem anderen Thread als dem in dem die Tk-Hauptschleife läuft `quit()` aufgerufen. Das kann gut gehen — muss es aber nicht.

Und ist insgesamt alles komplizierter als es sein müsste. Man muss sich halt einfach auf ereignisbasierte Programmierung einlassen.
Jetzt ist es aber richtig:

Code: Alles auswählen

# -*- coding: utf-8 -*-
 
try:
    import tkinter as tk
except ImportError:
    import Tkinter as tk
 
from threading import Thread, Event, Lock
 
class Application(tk.Tk):
 
    def __init__(self,start_end,output,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.entry = tk.Entry(self)
        self.entry.pack()
        self.entry['state'] = 'disabled'
        self.start_end = start_end
        self.output = output
        self.entry.bind('<Return>',self.output_value)
        self.pull_start()
 
    def pull_start(self):
        start_end = self.start_end()
        if start_end == 2:
            self.quit()
            return
        elif start_end:
            self.entry['state'] = 'normal'
            self.entry.delete(0,'end')
        self.after(100,self.pull_start)
 
    def output_value(self,event):
        self.entry['state'] = 'disabled'
        self.output(self.entry.get())
 
 
class ThreadWithGUIInput:
 
    def __init__(self):
 
        self.start_flag = False
        self.start_lock = Lock()
        self.input_ready = Event()
        self.output_value = ''
        self.app = Application(self.start_end_gui,self.gui_output)
 
    def start_end_gui(self,value=0):
        self.start_lock.acquire()
        ret_val = self.start_flag
        self.start_flag = value
        self.start_lock.release()
        return ret_val
 
    def gui_output(self,value):
        self.output_value = value
        self.input_ready.set()
 
    def read_input(self):
        self.input_ready.clear()
        self.start_end_gui(1)
        self.input_ready.wait()
        return self.output_value
 
    def program(self):
        while(True):
            value = self.read_input()
            print(value)
            if not value:
                self.start_end_gui(2)
                break
       
def main():
    loopthread = ThreadWithGUIInput()
    Thread(target=loopthread.program).start()
    loopthread.app.mainloop()
   
 
if __name__ == '__main__':
    main()
Und wieso komplizierter als es sein müßte? Einen Lock braucht man, man könnte aber auch eine Queue nehmen, die in diesem Fall aber wenig Sinn macht. Und einen Event braucht man auch. Und es ging darum zu zeigen, dass man auch Rückgabewerte erhalten kann. Natürlich sollte man ein Programm ereignisgetriggert schreiben. Aber wenn ich einfach eine Inputschleife habe, dann ist es eben eine Inputschleife.
BlackJack

@Alfons Mittelmeyer: Man braucht weder Lock noch Event, denn man braucht den Thread nicht. Nicht Details Deiner Lösung sind komplizierter als es sein müsste, sondern dieser Ansatz insgesamt.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Man braucht weder Lock noch Event, denn man braucht den Thread nicht. Nicht Details Deiner Lösung sind komplizierter als es sein müsste, sondern dieser Ansatz insgesamt.
Natürlich braucht man den Thread nicht um das Problem zu lösen. Man kann es ja Eventgetriggert tun, einfach die Daten weitergeben. Wenn man allerdings eine Funktion mit Rückgabewert haben möchte, dann braucht man ihn schon.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Man braucht weder Lock noch Event, denn man braucht den Thread nicht. Nicht Details Deiner Lösung sind komplizierter als es sein müsste, sondern dieser Ansatz insgesamt.
Ja, Du hast recht, es geht auch noch viel einfacher ganz ohne Thread und ohne pollen mit after.

Hier ist das Gui Programm

gui_input.py

Code: Alles auswählen

import tkinter as tk

class Application(tk.Tk):
 
    def __init__(self,variable,geometry,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.geometry(geometry)
        self.entry = tk.Entry(self)
        self.entry.pack()
        self.variable = variable
        self.entry.bind('<Return>',self.output_value)
        self.entry.focus_set()

    def output_value(self,event):
        self.variable.set(self.entry.get())
        self.destroy()
Und hier ist die Abfrage in der Loop:

Code: Alles auswählen

import gui_input

class Var:
    value = ''
    def set(self,value):
        self.value = value
    def get(self):
        return self.value

def get_input():
    value = Var()
    gui_input.Application(value,'+0+0').mainloop()
    return value.get()

while True:
    value = get_input()
    if not value:
        break
    print(value)
BlackJack

@Alfons Mittelmeyer: Da gehen ja dauernd Fenster auf und wieder zu. Es geht *noch* einfacher, in dem man einfach eine ganz normale GUI-Anwendung programmiert, ereingnisbasiert, und nicht den Versuch unternimmt das irgendwie wieder in einen ”linearen” Programmablauf zwängen zu wollen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:@Alfons Mittelmeyer: Da gehen ja dauernd Fenster auf und wieder zu. Es geht *noch* einfacher, in dem man einfach eine ganz normale GUI-Anwendung programmiert, ereingnisbasiert, und nicht den Versuch unternimmt das irgendwie wieder in einen ”linearen” Programmablauf zwängen zu wollen.
Das Thema war aber: Zugriff auf Funktionsrückgabewert. Bei einer normalen GUI-Anwendung erhält man auch einen Wert aber es ist kein 'Funktionsrückgabewert', sondern ein Weitergabewert.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Terahnee hat geschrieben: Meine Frage ist nun: Wie kann ich mit diesen Daten weiterarbeiten? Ich umgehe das Problem momentan indem ich in der Eventhandler-Funktion die Ausgaben mache, aber das ist ja sicherlich nicht Sinn der Sache. Wenn ich die Datenbankabfrage mit einem "return" zurückgebe, wo landet das dann? Wie kann ich darauf zugreifen? Geht das überhaupt?
Du brauchst gar nicht das mit dem Eventhandler zu machen, um zu wissen was im Entry Feld steht.
Anstatt den eingescannten Wert an das Entryfeld zu übergeben und wegzuwerfen, kannst Du Dir ihn ja vorher merken. Außerdem kann man auch den Wert aus dem Entryfeld herauslesen ohne dass Return gedrückt wird und Du dann einen Eventhandler brauchst.

Es geht hier ganz ohne Return:

Code: Alles auswählen

import tkinter as tk

class Application(tk.Tk):
 
    def __init__(self,**kwargs):
        tk.Tk.__init__(self,**kwargs)
        # widget definitions ===================================
        self.variable = tk.StringVar()
        self.entry = tk.Entry(self,textvariable = self.variable,state='readonly')
        self.entry.pack()
        self.entry.focus_set()


        # later
        self.variable.set(self.scan_barcode())

        # wie bekommt man heraus, was im entry Feld steht?

        # stattdessen geht es auch so

        value = self.scan_barcode()
        self.variable.set(value)

        # testen
        entry_value = self.entry.get()
        yes = value == entry_value

        print("{} == {} ? {}!".format(value,self.entry.get(),'ja' if yes else 'nein'))
        

    def scan_barcode(self):
        # wert zum testen
        return '34516896'
        

Application().mainloop()
Terahnee
User
Beiträge: 13
Registriert: Dienstag 8. August 2017, 13:48

Alfons Mittelmeyer hat geschrieben: Du brauchst gar nicht das mit dem Eventhandler zu machen, um zu wissen was im Entry Feld steht.
Anstatt den eingescannten Wert an das Entryfeld zu übergeben und wegzuwerfen, kannst Du Dir ihn ja vorher merken. Außerdem kann man auch den Wert aus dem Entryfeld herauslesen ohne dass Return gedrückt wird und Du dann einen Eventhandler brauchst.

Es geht hier ganz ohne Return:
Ich möchte es ja gerade mit einem Return machen, weil mir der Barcode-Scanner beim Scanvorgang direkt ein Return mitliefert. Das dient der Bequemlichkeit, da der Benutzer so nach Einlesen des Barcodes keinen Knopf mehr zu drücken braucht.

Dein Hinweis, dass man einen Wert aus einem Entryfeld natürlich an jeder Stelle lesen kann, hat mich aus meinem falschen Denken herausgebracht. Es funktioniert jetzt so wie es sollte! Vielen Dank an alle Beteiligten.
Antworten