Kombination von TKinter GUI, TCPIP-Kommunikation und Anwendung sauber programmieren

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
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Hallo zusammen,
ich komme aus der "C"-Programmierung, HTML und PHP. Also alles ablaufgesteuert. Nun soll ich für eine Anwendung ein erstes, objektorientiertes Python-Programm schreiben. Diese soll auf einem Raspberry Pi mit Touchscreen laufen und grob gesagt Eingaben auf Sinnhaftigkeit überprüfen, die Eingaben über Netzwerk an einen PC schicken und von einem PC Befehle entgegennehmen und dementsprechend reagieren.
Auf dem Touchscreen befinden sich Buttons, die wiederum auch Aktionen wie z.B das Senden von Daten über Ethernet oder das Beenden des Programms auslösen.

Das klappt eigentlich ganz gut, allerdings ist es eher ablaufgesteuerter Code ohne Objektorientierung. Die Nutzung von globalen Variablen ist wohl ein weiteres Indiz dafür, dass es wesentlich schöner geht.
Das ich unterschiedliche Threads für die graphische Benutzeroberfläche von TKinter benötige und auch einen separaten Thread für die TCPIP-Kommunikation ist mir auch klar.

Bevor ich nun in die falsche Richtung laufe, benötige ich Tipps zur Optimierung und Strukturierung des Programms:

Welche Klassen würdet ihr anlegen? Getrennte Klassen für TKinter, Kommunikation und eigentlicher Anwendung? Geht diese Trennung? oder muss die Kommunikation in die GUI-Klasse integriert werden, dass sich das Programm nicht aufhängt? Wie kommunizieren die Klassen untereinander

Läuft die TKinter-GUI automatisch als Thread? Wie synchronisiere ich die GUI und den vorhandenen Kommunikations-Thread? Das läuft momentan eigentlich erstaunlich gut ohne Hänger, nur beim Abbruch des Programms mittels F12 oder QUT-Befehl bekomme ich den Fehler " threading.py ... in shutdown t.join" und _wait_for_tstate_lock(). Wie beende ich dieses Programm richtig?

Und sicher sind da noch ganz viele andere Dinge, die den erfahrenen Python-Programmierer die Haare zu Berge stehen lassen.
Schon mal danke für eure Hilfe und den Schubs in die richtige Richtung.

Hier der vermutlich stark verbesserungswürdige Code:

Code: Alles auswählen

import sys
import tkinter as tk
import threading
import time
import socket

HEIGHT = 480
WIDTH = 800

HOST = ''
#HOST = '192.168.xxx.xx'
PORT = 5555

#Globals
SerialValid = False
SerNumber = "0"
SerialLength = 10   # Standard value for serial lenth

#THREAD1
def thr_Communication():

    global SerNumber        #use these global variables
    global SerialValid      #use these global variables
    global SerialLength     #use these global variables

    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

    try:
        s.bind((HOST, PORT))
    except socket.error:
        label2.config(text="Connection failed", bg='white', fg='#00cc99')
        time.sleep(5)
        sys.exit(0)

    s.listen(1) #1 connection allowed
    clientsocket, address = s.accept()
    clientsocket.send(bytes("Connected", "utf-8"))
    label2.config(text="Connected", bg='white', fg='#00cc99')
    label4.config(text="Waiting")

    while True:

        msg = clientsocket.recv(1024)
   
        if msg.decode("utf-8") == 'QUT':
            label2.config(text=msg.decode("utf-8"), bg='white', fg='#00cc99')
            clientsocket.send(bytes('BYE', "utf-8"))
            clientsocket.close()
            ExitScreen()
            break
        elif msg.decode("utf-8") == 'GET':
                #mach dies
                clientsocket.send(bytes("GET_OK", "utf-8"))
                label4.config(text="blabla")
        elif msg.decode("utf-8") == 'GOT':
            clientsocket.send(bytes("GOT_OK", "utf-8"))
            label2.config(text=msg.decode("utf-8"), bg='white', fg='#00cc99')
        else:
            clientsocket.send(bytes("Unknown command", "utf-8"))
  
def Funktion_machwas():
    global SerNumber
    global SerialValid
    global SerialLength
    entryField.delete(0, 100)
    if SerialLength == 10:
        SerNumber = "1"
    elif SerialLength == 11:
        SerNumber = "2"
    else:
        SerNumber = "0"

    SerialValid = True
    label2.config(text="Test sample Mode", bg='white', fg='orange')
    label4.config(text=SerNumber, bg='white', fg='#00cc99')

def Funktion_machwasanderes():
    global SerNumber
    global SerialValid
    SerNumber = "0"
    SerialValid = False
    entryField.delete(0, 100)
    label2.config(text="BlaBla", bg='white', fg='#00cc99')
    label4.config(text="BlaBla2", bg='white', fg='#00cc99')


def ExitScreen(arg=None):
    root.destroy()
    sys.exit(0)



### GUI ###
root = tk.Tk()
root.config(cursor='none')

canvas = tk.Canvas(root, height=HEIGHT, width=WIDTH)
canvas.pack()

root.attributes('-fullscreen', True)

frame = tk.Frame(root, bg='#00cc99')
frame.place(anchor='nw', height=HEIGHT, width=WIDTH)

label = tk.Label(frame, text="Title Window", bg='white', fg='#00cc99')
label.place(relx=0, rely=0, relwidth=1, relheight=0.10)

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

# Configure buttons
button1 = tk.Button(frame, text="Repeat Scan", font=('Arial', 16), bg='#00cc99', fg='white', activebackground='#00cc99', activeforeground='white', command=Funktion_machwas)
button2 = tk.Button(frame, text="Sample", font=('Arial', 16), bg='#00cc99', fg='white', activebackground='#00cc99', activeforeground='white', command=Funktion_machwasanderes)
button1.place(relx=0.00, rely=0.10, relwidth=0.3, relheight=0.45)
button2.place(relx=0.00, rely=0.55, relwidth=0.3, relheight=0.45)
label2 = tk.Label(frame, text="Search connection...", font=('Arial', 24), bg='white', fg='#00cc99',
                  highlightbackground='black', borderwidth=1, relief='solid', wraplength=500)
label2.place(relx=0.3, rely=0.10, relwidth=0.7, relheight=0.45)
label4 = tk.Label(frame, text="Waiting", font=('Arial', 24), bg='white', fg='#00cc99', highlightbackground='black',
                  borderwidth=1, relief='solid', wraplength=500)
label4.place(relx=0.3, rely=0.55, relwidth=0.7, relheight=0.45)

x = threading.Thread(target=thr_Communication)
x.start()

root.mainloop()
Sirius3
User
Beiträge: 17747
Registriert: Sonntag 21. Oktober 2012, 17:20

Ein Grundsatz ist, Anwendungslogik und GUI zu trennen. Also erst einmal die ganze Logik zu programmieren, und wenn die funktioniert und getestet ist, die GUI-Klassen zu programmieren, und auf geeignete Weise mit der Anwendungslogik zu kommunizieren.
Das ist bei asynchroner Anwendungslogik natürlich schon um einiges komplexer. Da kommst man kaum um einen Thread für die Socketkommunikation herum.
Zu Deinem Code: Variablennamen und Funktionen schreibt man nach Konvention komplett klein. Konstanten, wie Du ja schon richtigerweise machst, komplett GROSS.
Sobald Du `global` verwendest, machst du etwas falsch.
Anwendungslogik und GUI sind wild gemischt. Das darf man spätestens dann nicht mehr machen, wenn man mit Threads arbeitet, denn die GUI darf nur vom Hauptthread aus verändert werden.
Die Socketkommunikation ist (wie 99.99999% aller Beispiele im Netz) kaputt. recv muß damit umgehen können, dass jeweils nur 1 Byte empfangen wird, oder dass gleich mehrere Befehle aneinandergeklebt werden, also GOTQUT. Du brauchst also ein Protokoll, das die Nachrichten trennen kann. Gesendet wird immer mit sendall, weil sonst auch nur Teile der Nachricht gesendet werden, oder man programmiert das send halt richtig.
Warum verwendest Du mal bytes.decode zum Decodieren, aber bytes als Funktion zum Encodieren?
sys.exit hat in einem sauberen Programm nichts (oder nur in der main-Funktion) zu suchen. In Threads erst recht nichts.
Benutze keine Abkürzungen. SerNumber und SerialValid? Warum wird das einmal ausgeschrieben und einmal nicht?
Alles ab ##GUI## gehört auch in eine Funktion, die man üblicherweise main nennt, dann kommt man erst gar nicht in Verlegenheit wild globale Variablen zu benutzen.
Kommunikation zwischen Threads wird mit Queues gemacht. Für GUIs braucht man Klassen.
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Erst mal danke für die Denkanstöße.
So, jetzt hab ich mir erst mal ein bisschen Literatur in Buchform bestellt und werde diese unter mein Kopfkissen legen.
Hochmotiviert habe ich eine Beispiel-GUI in einer Klasse gesucht und experimentiere nun mit dem Code herum, was ziemlich frustrierend ist.

class GUI(Frame) erbt also von Frame, das habe ich nu schon heraus gefunden. Die Buttons in der GUI , die einfach nur eine Methode mit einem print("Halli Hallo") aufrufen, funktionieren ganz gut.
Nun möchte ich aber einer Funktion noch einen Parameter übergeben:

Funktion:

Code: Alles auswählen

 def servus(self, message):
        print(message)
Aufruf:

Code: Alles auswählen

self.hiButton = Button(self, text="BOOM", bg="RED", fg="white", command=self.servus("bye"))
Alles, was ich versucht habe, klappt nicht. Das liegt vermutlich an dem Konstruktor dieser Klasse. Entweder fehlt dem Compiler ein Argument, es ist eins zuviel oder diverse andere Fehler.
Wenn ich an so einem einfachen Problem schon scheitere, dann sinkt die Motivation schon recht schnell. Das kann doch nicht so wild sein. Klassen verstehe ich eigentlich schon, aber in Verbindungb mit der GUI komme ich da wieder nicht mit. Die letzten vier Zeilen des Codes mit den wilden Zuordnungen verstehe ich z.B nicht so richtig.

Hier mal der gesamte Code. Wieso wird der Parameter nicht richtig an die Funktion übergeben?

Code: Alles auswählen

from tkinter import *

class GUI(Frame):

    def __init__(self, master, message):
        Frame.__init__(self, master)
        self.master.title("GUI")
        self.pack()
        self.widgetsAnlegen()
        self.message = message

    def hallo(self):
        print("Halli Hallo")

    def servus(self, message):
        print(message)

    def widgetsAnlegen(self):
        self.schliessenButton = Button(self, text= "Schließen", bg="green", fg="red", command = self.quit)
        self.schliessenButton.pack(side=LEFT)
        self.hiButton = Button(self, text = "Hallo", bg = "yellow", fg="blue", command =self.hallo)
        self.hiButton.pack(side=LEFT)
        self.hiButton = Button(self, text="BOOM", bg="RED", fg="white", command=self.servus("bye"))
        self.hiButton.pack(side=LEFT)
        self.myLabel = Label(self, text="NANA")
        self.myLabel.pack(side=LEFT)


root = Tk()
app = GUI(master=root)
app.mainloop()
root.destroy()
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

OMG, ich habe es durch viel Googeln herausgefunden: Man kann mit der Button-Funktion von TKinter nicht einfach die Parameter übergeben wie in einer normalen Funktion.
Hierzu muss man die Lanbda-Funktion benutzen, warum auch immer. Jetzt muss ich doch noch mal schauen, was Lambda genau ist. Und die vier letzten Zeilen habe ich editiert, weiss aber immer noch nicht, warum man das so machen muss, z.B.

Code: Alles auswählen

app = GUI(master=root, message=root)
.

Hier der gesamte Code:

Code: Alles auswählen

from tkinter import *
from functools import partial


class GUI(Frame):

    def __init__(self, master, message):
        Frame.__init__(self, master)
        self.master.title("GUI")
        self.pack()
        self.widgetsAnlegen()
        self.message = message

    def hallo(self):
        print("Halli Hallo")

    def servus(self, message):
        print(message)

    def widgetsAnlegen(self):
        self.schliessenButton = Button(self, text= "Schließen", bg="green", fg="red", command = self.quit)
        self.schliessenButton.pack(side=LEFT)
        self.hiButton = Button(self, text = "Hallo", bg = "yellow", fg="blue", command =self.hallo)
        self.hiButton.pack(side=LEFT)
        self.hiButton = Button(self, text="BOOM", bg="RED", fg="white", command = lambda: self.servus("GO"))
        self.hiButton.pack(side=LEFT)
        self.myLabel = Label(self, text="NANA")
        self.myLabel.pack(side=LEFT)


root = Tk()
app = GUI(master=root, message=root)
app.mainloop()
root.destroy()
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@blutec: Ich weiss jetzt nicht was Du mit „wie in einer normalen Funktion“ meinst, denn was Du da erwartet hast ist ja gerade nicht das wie Funktionsaufrufe funktionieren. Wenn Du ``some_function(argument=do_something(1, 2, 3))`` schreibst, dann wird *erst* `do_something()` mit den Argumenten 1, 2, und 3 aufgerufen und der Rückgabewert dieses Aufrufs dann an `some_function()` übergeben. Genau das passiert in Python *immer* in dieser Ausführungsreihenfolge. Auch wenn das Argument `command` heisst ändert sich das nicht auf magische Weise. Das `command`-Argument von `Button.__init__()` erwartet ein aufrufbares Objekt das genau 0 Argumente erwartet. Bei ``Button(…, command=self.servus("bye"))`` wird erst ``self.servus("bye")`` ausgeführt und der Rückgabewert von diesem Aufruf dann für das `command`-Argument verwendet. Die Methode gibt nichts explizit zurück, also wird dort `None` als `command` übergeben.

Man kann einen ``lambda``-Ausdruck verwenden, aber ich würde `functools.partial()` vorziehen. Bei ``lambda``-Ausdrücken wird man sonst irgendwann mal darüber stolpern das die freien Variablen erst zur Aufrufzeit nachgeschlagen werden („late binding“), während `partial()` die Werte direkt beim Aufruf bindet.

Sternchen-Importe sind Böse™. Da holt man sich gerade bei `tkinter` fast 200 Namen ins Modul von denen nur ein kleiner Bruchteil verwendet wird. Auch Namen die gar nicht in `tkinter` definiert werden, sondern ihrerseits von woanders importiert werden. Das macht Programme unnötig unübersichtlicher und fehleranfälliger und es besteht die Gefahr von Namenskollisionen.

Namen werden in Python klein_mit_unterstrichen geschrieben. Ausnahmen sind Konstanten (KOMPLETT_GROSS) und Klassen (MixedCase).

`GUI` sollte nichts an seinem `master` rumfummeln. Man schreibt seinem Meister nicht vor welchen Titel er tragen soll. Insbesondere weiss man ja gar nicht ob der Meister überhaupt ein Toplevel-Wigdet ist. Diese Annahme kann/sollte man nicht treffen, denn sie macht das unnötig unflexibel.

Widgets layouten sich nicht selbst. Das ``self.pack()`` gehört da nicht in die Klasse, das gehört dort hin wo der `GUI`-`Frame` erstellt wurde, denn *dort* wird entschieden in welches Container-Widget das gesteckt wird und wie es angeordnet werden soll.

Das erstellen des Inhalts in eine eigene Methode auszulagern macht keinen Sinn, das steht mit in der `__init__()`.

Die Vorsilbe `my` ist sinnfrei. Die hat Null Informationswert. Einfach weglassen.

Edit: Welchen Sinn hat eigentlich das `message`-Attribut? Es wird nicht verwendet und das Hauptfenster ist ja keine Nachricht.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
__blackjack__
User
Beiträge: 13100
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@blutec: Noch ein paar Anmerkungen:

Die Tk-Hauptschleife würde man eher auf `root` aufrufen statt auf dem abgeleiteten Frame.

Das `root.destroy()` am Ende ist sinnlos und kann ersatzlos gestrichen werden.

Da auf die ganzen Widgets in keiner Methode zugegriffen wird, braucht man die auch alle nicht an das Objekt binden. Selbst wenn, dann macht es keinen Sinn beim ersten Button der an `self.hiButton` gebunden wird, denn das wird dann ja durch den nächsten Button wieder überschrieben.

Vielleicht möchte man ja das Label an das Objekt binden und in den Methoden dessen Text setzen, statt mit `print()` auszugeben, denn die Methoden sind ja momentan gar keine Methoden, sondern nur Funktionen die sinnfreierweise in einer Klasse stecken.

Code: Alles auswählen

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


class GUI(tk.Frame):
    def __init__(self, master):
        tk.Frame.__init__(self, master)
        tk.Button(
            self, text="Schließen", bg="green", fg="red", command=self.quit
        ).pack(side=tk.LEFT)
        tk.Button(
            self, text="Hallo", bg="yellow", fg="blue", command=self.hallo
        ).pack(side=tk.LEFT)
        tk.Button(
            self,
            text="BOOM",
            bg="red",
            fg="white",
            command=partial(self.servus, "GO"),
        ).pack(side=tk.LEFT)
        self.label = tk.Label(self, text="NANA")
        self.label.pack(side=tk.LEFT)

    def servus(self, message):
        print(message)
        self.label["text"] = message

    hallo = partialmethod(servus, "Halli Hallo")


def main():
    root = tk.Tk()
    root.title("GUI")
    gui = GUI(root)
    gui.pack()
    root.mainloop()


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Danke schon mal für die ausführlichen Antworten, von denen ich momentan nur die Hälfte verstehe. Ich werde mich weiter in Python einarbeiten und mir deine Antwort öfter durchlesen.
Meinen Sourcecode habe ich zu großen Teilen aus anderen Python-Foren. Ich finde es als Anfänger schwierig, weil jeder andere Dinge sagt, die man falsch macht. So ist ein Beispiel aus dem einen Forum in einem anderen Forumschlecht, und dann wieder anders herum.
Ich hoffe, den richtigen Weg zu finden um mir irgendwann selbst eine Meinung bilden zu können.

Nun geht es an das Analysieren deines Beispielcodes. Danke für deine Mühe und Untestützung.
blutec
User
Beiträge: 16
Registriert: Montag 21. September 2020, 17:05

Noch eine Frage zu deinem Code: Wieso macht man das: gui = GUI(root) ?
Was stellt dieses "root" dar? Wieso benötige ich es?
__deets__
User
Beiträge: 14536
Registriert: Mittwoch 14. Oktober 2015, 14:29

Schau doch welches Objekt root ist, und was die Dokumentation von tkinter dazu zu sagen hat. Du selbst hast das doch auch benutzt in deinem urspruenglichen Code.
Antworten