Tkinter Listbox reihenweise beschreiben

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
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Hallo,

ich bin gerade dabei eine GUI mit Tkinter zu basteln welche mir Parameter nach einander auflisten soll. Die Nutzung sieht nacher so aus, dass ich Daten Sende und Empfange und diese in einer Listbox anzeigen möchte.

Beispiel, was in der Listbox stehen soll:

Sende Daten 123
Empfange Daten 456
Sende Daten 789
Empfange Daten abc

An sich ganz simple dachte ich, Problem ist aber das die Listbox erst beschrieben wird, wenn meine Funktion fertig ist also alle Daten gesendet und Empfangen worden sind, dadurch erhalte ich kein Feedback innerhalb meiner Funktion und das Ergebnis ist auch nicht wie gewollt:

Sende Daten 123
Sende Daten 456
Empfange Daten 789
Empfange Daten abc

Um meine Funktion als Fehler auszuschließen hab ich das gleiche mit sleep getestet und wollte im Sekunden Takt Zeilen beschrieben.

Code: Alles auswählen

listbox_run = tkinter.Listbox(run_test, state="normal")
listbox_run.pack(fill= "both", expand=1)

for i in range(10):
    listbox_run.insert( i, str(i) )
    sleep(1)
Ergebnis ist aber das die Listbox 10 Sekunden lang leer ist und danach sofort voll geschrieben wird.
Ist die Listbox an sich für so etwas ungeeignet? Mein Ziel war es eigentlich, dass man nachdem die Listbox voll ist, die einzelnen Items anklicken kann und es möglich ist deren Inhalt weiter zu betrachten.
Sirius3
User
Beiträge: 18224
Registriert: Sonntag 21. Oktober 2012, 17:20

Um diesen und andere Fehler bei der Benutzung von TCP erklären zu können, bräuchten wir schon den kompletten Code.
Die direkte Methode wäre `createfilehandler` um auf Input vom Socket zu warten. Die indirekte Methode, wäre, wie immer bei solchen Problemen, einen eigenen Thread für die Kommunikation und eine Queue, die periodisch mit after abgefragt wird.
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Nun leider verwende ich nicht TCP, die Daten die ich Sende und Empfange gehen über ein CAN-Bus System. Für das kleine Beispiel was ich oben genannt habe mit dem "sleep" gibt es keine simple Lösung? Im Normalfall sollte ich dies ja auf meine Funktion übertragen können, wenn es aber nicht möglich ist, dann sprengt das hier den Rahmen.

Problem bezüglich des ganzen Codes ist, dass dieser nun leider auch schon wieder an die 1000 an Zeilen hat. Außerdem greife ich mit Python auf einen Treiber zu der die Kommunikation handhabt. Mein Programm bezieht sich hierbei nur auf das Transportprotokoll, damit meine ich, dass ich im Programm nur sage was für Bytes gesendet werden sollen. Für die Antworten stelle ich nur Speicher für eine Que bereit. Aus dieser kann ich dann die Empfangen Daten entnehmen.
Benutzeravatar
sparrow
User
Beiträge: 4509
Registriert: Freitag 17. April 2009, 10:28

Eine GUI hat eine Hauptschleife, die dafür sorgt, dass die Oberfläche ständig neu gezeichnet wird.
Wenn du dein Programm in eine Schleife schickst, dann wird die Oberfläche erst wieder aktualisiert, wenn der Programmfluss in die Hauptschleife zurückkehrt. Unter Windows bekommst du zudem noch die Info "Das Programm reagiert nicht mehr".
Deshalb gehört alles, was weniger als wenige Millisekunden dauert, dort nicht hinein.

Da hört GUI-Programmierung auf trivial zu sein und man muss entsprechend Threads einsetzen.
Die dürfen aber wiederum die GUI nicht beeinflussen sondern müssen ihre Information in die Hauptschleife schicken, damit hier die Änderungen an der GUI vorgenommen werden.
Einen üblichen Weg hat Sirius3 genannt: Das was dauert muss in einen Thread, der füttert eine Queue in der Hauptschleife und dort wird mittels "after" nachgeschaut, ob dort Informationen eingetroffen sind.
Sirius3
User
Beiträge: 18224
Registriert: Sonntag 21. Oktober 2012, 17:20

Dann hast Du ja schon eine Queue. Also ist after die Methode der Wahl, und dazu gibt es hier im Forum schon tausende Beispiele.
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Danke das mit dem Threading war ziemlich Simple, mir hatte das Verständnis für die GUI und deren Hauptschleife gefehlt. Habe nun das gewüsnchte Ergebnis mit diesem Code.

Code: Alles auswählen

def run_test():
    global command_list

    run_test_window = tkinter.Toplevel(mainwindow)
    run_test_window.geometry("500x700")
    run_test_window.resizable(0,0)

    text_run = tkinter.Listbox(run_test_window,state="normal")
    text_run.pack(fill="both",expand=1) 


    def can_thread():
        can_com.start_BUS()
        can_com.set_bus_filter(0x680,0x710)

        for item in range(len(command_list)):
            ident   = command_list[item].CANID
            node    = int("0x" + command_list[item].Node, 16)
            sid     = command_list[item].SID
            did     = command_list[item].DID

            data    = sid + did
            data_l  = int(len(data)/2)
            
            response = can_com.talk_with_device(ident,node,data_l,data,0)
            text_for_line = data + " => " + response
            text_run.insert(item,text_for_line)
            
       can_com.stop_CAN()
    
    def start():
        can     = threading.Thread(target=can_thread)
        can.start()
       



    button_start    = tkinter.Button(run_test_window,text="START",command=start)
    button_start.pack()
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

tryanderror hat geschrieben: Donnerstag 12. März 2020, 12:15 Danke das mit dem Threading war ziemlich Simple
:lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol: :lol:

Famous first words.

Threading ist nicht simpel, es ist schwer. Und natuerlich hast du das auch prompt verbockt. Du rufst im neuen Thread Methoden eines GUI-Objektes auf (insert) - und das geht. Bis es kracht. GUIs und Threads sind NICHT trivial zu mischen, weshalb man die hier schon mehrfach erwaehnten Mittel wie Queues und after nutzen muss, um die Kommunikation zwischen einem Hintergrund-Thread und dem GUI-Thread zu ermoeglichen.

Um mal eine kleine Analogie zu bemuehen: du laeufst hier gerade mit verbundenen Augen ueber die Strasse. Das geht, und je weniger Verkehr ist, desto besser. Aber irgendwann geht's eben nicht mehr.
Benutzeravatar
__blackjack__
User
Beiträge: 13927
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@tryanderror: Das ist halt zu simpel und falsch weil Du aus dem Thread heraus ein GUI-Element veränderst was man nicht machen darf weil das nicht „thread safe“ ist.

Ausserdem ist ``global`` und das verschachteln von Funktionen falsch. Bei GUIs kommt man nicht um objektorientierte Programmierung herum.
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
Sirius3
User
Beiträge: 18224
Registriert: Sonntag 21. Oktober 2012, 17:20

`command_list` sollte ein Argument der Funktion sein und keine globale Variable. Datentypen haben in Namen nichts verloren, `commands` drückt schon gut genug aus, dass es mehrere davon in diesem Objekt gibt. `mainwindow` kommt aus dem nichts und sollte auch ein Argument sein. Keine Funktionen verschachteln, das macht das Lesen und Testen unnötig kompliziert.
`item` ist der falsche Name für einen Index; Du solltest aber tatsächlich über die Items iterieren, statt eines Indexes zu benutzen. Um von der GUI mit einem Thread zu kommunizieren brauchst Du eine Queue und `after`.
Sirius3
User
Beiträge: 18224
Registriert: Sonntag 21. Oktober 2012, 17:20

Das ganze mal ein bißchen aufgeräumt (ungetestet):

Code: Alles auswählen

from queue import Queue
from functools import partial

def can_thread(queue, commands):
    can_com.start_BUS()
    can_com.set_bus_filter(0x680,0x710)

    for index, item in enumerate(commands):
        ident = item.CANID
        node = int(item.Node, 16)
        data = item.SID + item.DID
        response = can_com.talk_with_device(ident, node, len(data) // 2, data, 0)
        queue.put((index, f"{data} => {response}"))
    can_com.stop_CAN()

def update(queue, text_run)
    while not queue.empty()
        text_run.insert(*queue.get())
    text_run.after(10, update, queue, text_run)

def start(text_run, commands):
    queue = Queue()
    can = threading.Thread(target=partial(can_thread, queue, commands)
    can.start()
    update(queue, text_run)

def run_test(commands, mainwindow):
    run_test_window = tkinter.Toplevel(mainwindow)
    run_test_window.geometry("500x700")
    run_test_window.resizable(0,0)
    text_run = tkinter.Listbox(run_test_window,state="normal")
    text_run.pack(fill="both",expand=1) 
    tkinter.Button(run_test_window,text="START",command=partial(start, text_run, commands)).pack()
    return run_test_window
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Danke fürs Feedback, ich will das ja auch ohne global haben, da das sonst unsauber ist. Kann mir einer eine Seite oder ein Tutorial bezüglich GUI Programmierung mit Threads und Queues in Python empfehlen. Soweit ich das Verstanden habe sollte ich meine Daten im Mainwindow in einer Queue bearbeiten lassen bzw. da auch abholen nun weiß ich aber nicht wo ich Anfangen soll.
Sirius3
User
Beiträge: 18224
Registriert: Sonntag 21. Oktober 2012, 17:20

Neben dem Beispiel, das ich hier gepostet habe, gibt es hier im Forum noch etlich weitere Beispiele.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wie gesagt, die Stichworte sind Queue, Thread und after, und das wurde wirklich schon oft hier diskutiert, und auch mit Beispielcode praesentiert. Ein bisschen suchen wirst du muessen, die Forums-Suche ist leider nicht so geil - ich benutze eigentlich immer google's site-search feature.
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Hallo,

ich weiß es ist schon wieder ein Weilchen her, aber ich habe mich erstmal mit GUI Programmierung an sich bisschen beschäftigt und wollte nun sicher gehen, dass ich es richtig verstanden habe. Ich habe gesehen, dass es wohl besser ist für die einzelnen GUI Fenster Klassen anzulegen, damit kann man auch super alle notwendigen Variablen verarbeiten ohne global zu nutzen.

Nun bin ich wieder bei dem Thema wo Ihr mir zuletz schon geholfen habt und hier steh ich ein bisschen auf dem Schlauch.

Code: Alles auswählen

class TestGui:
    def __init__(self, master):
        self.master = master

        self.test_window = tkinter.Toplevel(master.master)
        self.test_window.geometry("500x700")
        self.test_window.resizable(0,0)
        self.test_window.protocol("WM_DELETE_WINDOW",self.close_window)

        self.listbox_test = tkinter.Listbox(self.test_window, state="normal")
        self.listbox_test.pack(fill="both",expand=1)

        self.button_start = tkinter.Button(self.test_window,text="START", command=self.start_test)
        self.button_check = tkinter.Button(self.test_window,text="CHECK",state="disabled")

        self.button_start.pack()
        self.button_check.pack()

    def can_thread(self, queue, commands):
        can_com.start_BUS()
        can_com.set_bus_filter(0x680, 0x710)

        for item in range( len(commands) ):
            ident   = commands[item].CANID
            node    = int(commands[item].Node, 16)
            data    = commands[item].SID + commands[item].DID
            response    = can_com.talk_with_device(ident, node, int(len(data)/2), data, 0)
            queue.put((item, f"{data} => {response}"))
        can_com.stop_CAN()

    def update(self, queue, listbox):
        while not queue.empty():
            listbox.insert("end", *queue.get() )
        listbox.after(10, partial( self.update, queue, self.listbox_test) )

    def start_test(self):
        queue = Queue()
        can = threading.Thread( target=partial( self.can_thread, queue, self.master.commands ) )
        can.start()
        self.update(queue, self.listbox_test)

    def close_window(self):
    	self.master.refresh_listboxes()
    	self.master.set_mainwindow_inputs(True)
    	self.test_window.destroy()
Also wenn ich das richtig verstanden habe, dann wird beim Drücken des Start Buttons eine Queue erstellt und ein neuer Thread. Dieser Thread sendet die Daten, welche ich kommunizieren möchte und packt die Empfangenen Daten in die Queue welche ich dem Thread übergeben habe. Die Kommunikation Funktioniert auf jeden Fall, da ich diese mit einem anderen Tool sehen kann. Im IDE kann ich auch sehen, dass der Thread startet und nach Ende der Kommunikation sich schließt.

Nur diese Update-Funktion nutze ich wohl noch nicht richtig, da diese wohl nur einmal startet... Ich habe die Update Funktion auch mit einem einfach ein Print versehen, aber das konnte ich in der Konsole nicht wahrnehmen. Also wollte ich mal gucken was genau bei "Update" passiert. Hab dort bei der While und bei dem Listbox.after jeweils ein Breakpoint gesetzt um festzustellen, dass die Funktion nur einmal gecalled wird. Daher denke ich mal, dass mein Aufruf dieser Funktion und die Verwendung von After falsch sein könnte aber ich versteh nicht warum... es könnte natürlich auch sein, dass ich die Queue falsch bepacke und diese dadurch stetig empty ist?

Im Allgemeinen muss ich auch noch sagen, dass ich mit dem Umgang von "self" noch unsicher bin. Was Kritik angeht könnte ihr euch gerne austoben, so lern ich am meisten daraus.
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Wie es scheint lag der Fehler darin, dass die Queue nicht richitg bekannt war... meine Funktionen zum senden printen mir eine Info über die aktuellen Daten in den Konsole, da meine todo Liste aber ca. 150 Befehle hatte habe ich übersehen, dass beim ersten Aufruf ein Error in der Udatefunktion war... ich habe die Queue nun in der ganzen Klasse bekannt gemacht, wodurch ich der Update Funktion diese nicht mehr übergeben muss, sondern die Queue direkt über self abfragen kann.

Code: Alles auswählen

def make_que(self):
        self.queue = Queue()

def update(self):
        listbox = self.listbox_test
        queue   = self.queue
        while not queue.empty():
            string = queue.get()
            listbox.insert("end", string[1] )
            listbox.see("end")
        listbox.after(10, self.update )

def start_test(self):
        self.make_que()
        queue = self.queue
        can = threading.Thread( target=partial( self.can_thread, queue, self.master.commands ) )
        can.start()
        self.update()
So funktioniert das nun, ist das in Ordnung oder ist Form oder Ablauf nicht konform?
Benutzeravatar
__blackjack__
User
Beiträge: 13927
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@tryanderror: Anmerkungen zum Code:

Das `test_` bei `test_window` ist nicht wirklich nötig. Es ist das einzige Fenster in dem Namensraum und `Test` ist bereits Bestandteil des Namens der Klasse.

Objekthierarchien wie man sie bei GUIs verwendet sollten in der Regel nur in eine Richtig traversiert werden: vom Meister zu den Kindern. Wenn Kinder auf die Meister oder gar auf den Meister vom Meister zugreifen wird das unübersichtlich und unflexibel und damit fehleranfälliger und schwerer zu verändern. Kinder haben nichts bei den Meistern herum zu pfuschen sondern bieten die Möglichkeit Rückruffunktionen für Informationen in die Gegenrichtung. Ich würde das auch nur bei GUI-Widgets machen, denn bei einem Objekt in einem `tkinter` benutzenden Programm bedeutet für die meisten Leser ein `master`-Attribut, dass es sich um ein `Widget`-Objekt handelt.

Python hat einen Typ für Wahrheitswerte (`bool`) mit den Werten `True` und `False`. Dazu sollte man nicht die Zahlen 1 und 0 missbrauchen.

`tkinter` hat Konstanten für Zeichenketten die für Tk eine besondere Bedeutung haben wie "end", "normal" oder "disabled". Dann weiss der Leser das es sich nicht um eine beliebige Zeichenkette handelt und man bekommt von Python einen `NameError` wenn man sich vertippt bevor der falsche Wert an Tk übergeben wird.

Die beiden `Button`-Attribute hat anscheinend Yoda benannt. 😉

Ein Objekt sollte nach Ablauf der `__init__()`-Methode vollständig initialisiert sein, also insbesondere alle Attribute sollten existieren. Die `Queue` gehört also auch in die `__init__()`.

Der Start-Knopf sollte nach dem Start deaktiviert werden sonst kann der Benutzer den mehrfach betätigen und immer neue Threads starten.

Dem `Thread` würde ich noch das Argument ``deamon=True`` mitgeben, sonst endet das Programm erst wenn dieser Thread auch beendet ist.

`can_thread` ist keine Methode sondern eine normale Funktion die in die Klasse gesteckt wurde. Warum? Falls das Absicht und kein Versehen war, sollte man da wenigstens eine `staticmethod()` draus machen, damit der Leser sieht, dass es Absicht war.

Die ``for``-Schleife über einen Laufindex um den dann für den Zugriff auf die einzelnen Elemente einer Sequenz zu verwenden ist in Python ein „anti pattern“. Man *direkt* über die Elemente von Sequenzen iterieren, ohne den Umweg über einen Index. Falls man *zusätzlich* eine laufende Zahl benötigt, gibt es die `enumerate()`-Funktion. Allerdings wird der Index hier gar nicht wirklich benötigt. Der wird zwar mit in die Queue gesteckt, aber an der Stelle wo aus der Queue gelesen wird, ignoriert der Code den Index einfach.

`item` ist kein guter Name für einen Laufindex.

Statt etwas durch einen gannzahligen Wert zu teilen und das Ergebnis in `int()` umzuwandeln, böte sich ganzzahlige Teilung an.

Falls der `stop_CAN()`-Aufruf wichtig ist, sollte man den vielleicht in einem ``finally``-Block stecken.

`string` in der `update()`-Methode stimmt als Name nicht, weil da ein Tupel dran gebunden wird und keine Zeichenkette.

Code: Alles auswählen

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

import can_com

...


def can_thread(queue, commands):
    can_com.start_BUS()
    can_com.set_bus_filter(0x680, 0x710)
    for command in commands:
        data = command.SID + command.DID
        response = can_com.talk_with_device(
            command.CANID, int(command.Node, 16), len(data) // 2, data, 0
        )
        queue.put(f"{data} => {response}")
    #
    # TODO If this call is important, maybe ensure it with a ``finally`` block.
    #
    can_com.stop_CAN()


class TestGui:
    def __init__(self, master, commands, on_close):
        self.commands = commands
        self.on_close = on_close
        self.queue = Queue()

        self.window = tk.Toplevel(master)
        self.window.geometry("500x700")
        self.window.resizable(False, False)
        self.window.protocol("WM_DELETE_WINDOW", self.close_window)

        self.listbox = tk.Listbox(self.window, state=tk.NORMAL)
        self.listbox.pack(fill=tk.BOTH, expand=True)

        self.start_button = tk.Button(
            self.window, text="START", command=self.start_test
        )
        self.start_button.pack()
        self.check_button = tk.Button(
            self.window, text="CHECK", state=tk.DISABLED
        )
        self.check_button.pack()

    def update(self):
        while not self.queue.empty():
            self.listbox.insert(tk.END, self.queue.get())
            self.listbox.see(tk.END)
        self.listbox.after(10, self.update)

    def start_test(self):
        #
        # TODO Re-Enable the button after the thread did its work.
        #
        self.start_button["state"] = tk.DISABLED
        threading.Thread(
            target=partial(can_thread, self.queue, self.commands), daemon=True
        ).start()
        self.update()

    def close_window(self):
        self.on_close()
        self.window.destroy()
“Java is a DSL to transform big Xml documents into long exception stack traces.”
— Scott Bellware
tryanderror
User
Beiträge: 24
Registriert: Mittwoch 19. Februar 2020, 08:30

Hallo,

danke für das schöne Feedback und das du dir Zeit genommen hast und so Detailiert beschrieben hast, was nicht so toll ist und warum es nicht so toll ist.
Die beiden `Button`-Attribute hat anscheinend Yoda benannt.
Ja das hab ich so gemacht, weil ich meiner Meinung nach bessere Übersicht über alle Objekte habe, wenn ich zum Beispiel einen Button konfigurieren möchte und nicht den genauen Namen auf anhieb im Kopf habe geb ich Button ein und der IDE zeigt alle Buttons in der Klasse :D

Die Buttons hatte ich beim rumtesten nacher auch so wie du meintest deaktiviert, damit man nicht mehr als einen Thread starten kann.

Das mit dem Thread deamon=True versteh ich nun nicht direkt, was mir das bringt bzw. wie das gehandhabt wird. Ich habe jetzt mit einer weiteren Variable ausm Init gearbeitet um sicher zu stellen, dass der Thread beendet wird. Das sollte von der Logik genauso sicher sein, werde mich aber nochmal genauer mit Threads beschäftigen.

Code: Alles auswählen

def __init__(self, master):
	self.thread_state = False
	
def thread(self, commads)
    try:
        for - schleife:
            if self.thread_state:
                Funktion()
    finaly:
        can_com.stop_CAN()
        self.thread_state = False

def close_window(self):
    self.thread_state = False
Ich hab nur das nötigste beim obrigen Quellcode erwähnt, da die aktuelle Größe des Threads aufgrund von Fehlerabfangen etc. größer geworden ist und für die Allgemeinheit keinen Mehrwert hat.

Danke für den Hinweis mit finally! Ich habe Python immer nur im learning by doing Verfahren gelernt und garnicht gewusst, dass es sowas tolles wie finally gibt. Das konnte ich gleich bei mehren Problemen im ganzen Program verwenden, hat mir viele Zeilen gespart :D

Nochmal ein dickes Danke für die Hinweise und Tipps!
Antworten