Seite 1 von 1

GUI friert ein, Probleme mit Threading...

Verfasst: Samstag 2. April 2022, 13:34
von Ares
Hallo Python Gemeinde,

ich beschäftige mich seit einiger Zeit mit Python und wollte, hauptsächlich zu Übungszwecken, ein kleines Programm schreiben bei dem die GUI und die Progammlogik in verschiedenen Klassen liegen.
Das Programm sollte eigentlich via openpyxl Dateien kopieren, formatieren etc. und zwischen den einzelnen Schritten einen Status in eine Textbox schreiben.
Das alles funktioniert auch soweit, wie ich mir das vorgestellt habe, abgesehen davon das die GUI einfriert, und alle Einträge in die Textbox erst nach Ende der Funktion erscheinen/geschrieben werden.
Ich habe versuch das Problem mit threading zu lösen aber bisher keinen Erfolg.

Ich hab das Programm mal heruntergebrochen auf das "minimum" um es hier zu posten und würde mich freuen wenn mir jmd helfen oder mich in die richtige Richtung schubsen kann.
Ich habe schon einiges suchen/lesen und ausprobieren hinter mir und verstehe einfach nicht wo der Fehler liegt.
Oder habe ich evtl. einen prinzipiellen Fehler beim Aufbau mit den 2 Klassen?

Vielen dank schonmal im vorraus!

Code: Alles auswählen

#!/usr/bin/env python3.9

import tkinter as tk
from tkinter import ttk

from tkinter import filedialog as fd
from tkinter import messagebox

import os
import openpyxl
from openpyxl import load_workbook

import threading

class MainFrame():

    def __init__(self, root, *args, **kwargs):
        super().__init__()

        self.root = root

        self.root.rowconfigure(0, weight=1)
        self.root.columnconfigure(0, weight=1)

        self.lgc = Logic()

        self.main_gui()

    def main_gui(self):
        frame1 = tk.Frame(
            self.root,
            bd=2,
            relief='groove'
        )

        frame1.grid(
            row=0, rowspan=1, column=0, columnspan=1,
            padx=5, pady=5, sticky='NESW')

        frame1.columnconfigure(0, weight=1)
        frame1.rowconfigure(0, weight=0)
        frame1.rowconfigure(1, weight=1)

        bttn_choose_file = ttk.Button(
            frame1,
            text='Choose File',
            width=15,
            command=lambda: self.action()
        )

        bttn_choose_file.grid(
            row=0, rowspan=1,
            column=0, columnspan=1,
            padx=10, pady=5,
            sticky='w'
        )

        self.txt_output = tk.Text(
            frame1, bg='white',
            bd=0,
            width=40,
            height=10,
        )

        self.txt_output.grid(
            row=1, rowspan=1,
            column=0, columnspan=1,
            padx=10, pady=5,
            sticky='NESW'
        )

    def action(self):
        file = fd.askopenfilename()
        if len(file) == 0:
            return
        self.txt_output.insert('end', 'Start ... \n')

        self.txt_output.insert('end', 'Load file first time... \n')
        wb_1 = self.lgc.load_vorlage(file)

        self.txt_output.insert('end', 'Load file second time... \n')
        wb_2 = self.lgc.load_vorlage_2(file)

        self.txt_output.insert('end', 'Done \n')
        messagebox.showinfo(
            title='Done',
            message=' Job done'
        )


class Logic():

    def load_vorlage(self, path):
        x = load_workbook(path)
        return x

    def load_vorlage_2(self, data):
        x = load_workbook(data, data_only=True, read_only=False)
        return x


def main():
    root = tk.Tk()
    MainFrame(root)
    root.mainloop()


if __name__ == '__main__':
    main()


Re: GUI friert ein, Probleme mit Threading...

Verfasst: Samstag 2. April 2022, 17:00
von __blackjack__
@Ares: Anmerkungen zum Quelltext: `os`, `threading`, und `openpyxl` werden importiert, aber nirgends verwendet.

Man sollte Namen nicht kryptisch Abkürzen. Es gibt ein paar verbreitete Ausnahmen wie `tk` für `tkinter`, also in der Regel wenn man sehr viele Namen aus einem Modul verwendet, aber auf `tkinter.fildedialog` trifft das hier nicht zu. Entweder `askopenfilename()` direkt importieren, oder über den nicht abgekürzten Modulnamen darauf zugreifen.

`lgc` für das `Logic`-Exemplar ist schlecht. Warum heisst das nicht einfach `logic`? Vokale kosten nicht extra. 😉

`MainFrame` ist ein unpassender Name für etwas das gar kein Frame ist. Und da das Objekt nach dem erstellen auch nicht wirklich für irgend etwas benutzt wird, was komisch aussieht, ist die Frage ob man daraus nicht besser das Hauptfenster macht und dafür von der Klasse für das Hauptfenster erbt. Dann kann man das Objekt nach dem erstellen auch verwenden um darauf die Hauptschleife aufzurufen.

Das erzeugen der Widgets in eine eigene Methode auszulagern die von der minimalen `__init__()` aufgerufen wird, macht keinen Sinn.

Man sollte Namen nicht durchnummerieren sondern sinnvolle Namen verwenden, oder gar keine Einzelnamen, sondern eine Datenstruktur. Oft eine Liste. Wenn man aber wie bei `frame1` nur diesen einen Namen mit der 1 hat, ist das extrem sinnfrei.

Bei Grid-Aufrufen machen `rowspan`- oder `columnspan`-Argumente mit dem Wert 1 keinen Sinn.

Für Argumentwerte für die es im `tkinter`-Modul Konstanten gibt, sollte man verwenden, statt literale Zeichenketten.

Ein ``lambda``-Ausdruck der nichts weiter macht als die Argument 1:1 an einen Aufruf einer Funktion oder Methode durchzureichen ist sinnfrei, weil man da gleich die Funktion oder Methode an der Stelle verwenden kann.

`file` ist ein unpassender Name für einen Dateinamen, denn das ist ein Name bei dem der Leser ein Dateiobjekt erwartet. Das spiegelt sich auch in dem Funktionsnamen `fildedialog.askopenfilename()` wieder. Es gibt auch ein `fildedialog.askopenfile()` welches nicht nur einen Namen, sondern tatsächlich ein Dateiobjekt zurück gibt. Da wäre `file` als Name für das Ergebnis passend.

``if len(filename) == 0:`` ist an der Stelle nicht idiomatisch. Da würde man ``if not filename:`` schreiben.

Die `Logic`-Klasse ist keine Klasse, weil die keinen Zustand kapselt und ausschliesslich zwei Funktionen enthält die als Methoden verkleidet sind. Und was die Funktionen machen muss nicht in zwei Funktionen stecken, im Grunde kann man da gleich die `load_workbook()`-Funktion verwenden.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3.9
import tkinter as tk
from tkinter import messagebox, ttk
from tkinter.filedialog import askopenfilename

from openpyxl import load_workbook



class Window(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        frame = tk.Frame(self, bd=2, relief=tk.GROOVE)
        frame.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=0)
        frame.rowconfigure(1, weight=1)

        ttk.Button(
            frame, text="Choose File", width=15, command=self.action
        ).grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)

        self.txt_output = tk.Text(frame, bg="white", bd=0, width=40, height=10)
        self.txt_output.grid(row=1, column=0, padx=10, pady=5, sticky=tk.NSEW)

    def action(self):
        filename = askopenfilename()
        if filename:
            self.txt_output.insert(tk.END, "Start...\n")

            self.txt_output.insert(tk.END, "Load file first time...\n")
            workbook_a = load_workbook(filename)

            self.txt_output.insert(tk.END, "Load file second time...\n")
            workbook_b = load_workbook(filename, data_only=True)

            self.txt_output.insert(tk.END, "Done\n")
            messagebox.showinfo(title="Done", message="Job done")


def main():
    Window().mainloop()


if __name__ == "__main__":
    main()
Man sieht hier ja nicht was Du mit Threads versucht hast, also kann man da nur die allgemeinen Sachen sagen, die man aber auch schon sehr oft hier im Forum findet: Von einem Thread aus darf die GUI nicht verändert werden, darum kommuniziert man in der Regel über eine Queue (`queue`-Modul) mit dem Hauptthread in dem die GUI läuft, und die per `after()`-Methode regelmässig schaut ob etwas in der Queue steckt.

Re: GUI friert ein, Probleme mit Threading...

Verfasst: Samstag 2. April 2022, 19:28
von Ares
Ok wow, erstmal vielen Dank für deine ausführliche Antwort.
Ich werde sie so gut es geht in meinem Code übernehmen.
Für Argumentwerte für die es im `tkinter`-Modul Konstanten gibt, sollte man verwenden, statt literale Zeichenketten.
Das bezieht sich auf

Code: Alles auswählen

self.txt_output.insert(tk.END, "Load file second time...\n")
oder?
Das erzeugen der Widgets in eine eigene Methode auszulagern die von der minimalen `__init__()` aufgerufen wird, macht keinen Sinn
Würde es denn Sinn machen wenn die '__init__()' besonder groß wäre? Ich mache das gerne der Übersicht wegen so, oder gehören die Wdgets eigentlich immer in die '__init__()'?
Und was die Funktionen machen muss nicht in zwei Funktionen stecken, im Grunde kann man da gleich die `load_workbook()`-Funktion verwenden.
Ja da hast du natürlich recht, das ist kein sinnvolles Beispiel für mein Vorhaben... :lol:

Re: GUI friert ein, Probleme mit Threading...

Verfasst: Samstag 2. April 2022, 20:48
von __blackjack__
@Ares: Das mit den Konstanten bezieht sich auf `tk.END`, `tk.GROOVE`, `tk.NSEW`, halt auf alle Argumente wo nicht beliebige Zeichenketten verwendet werden können, sondern nur eine feste Auswahl. Wo andere Programmiersprachen Aufzählungstypen für haben und Tk/Tcl halt nur Zeichenketten, weil da letztlich alles irgendwie Zeichenkette ist.

Ich sehe nicht das eine Mini-__init__()` und dann eine andere Methode die dafür alles enthält, irgendwie übersichtlicher wäre. Der Leser muss dann eine weitere Methode lesen um zu sehen was alles an `self` gebunden wird. Das kann man dann auch leicht mal übersehen, denn in der Regel werden alle Attribute in der `__init__()` eingeführt. Bei GUIs kann so eine `__init__()` dann schon mal länger werden. Allerdings ist das auch eher eine ”Besonderheit” von `tkinter`, denn bei den anderen beiden grossen, Gtk und Qt, ist es üblicher das die Elemente und das Layout mit einem Programm zu erstellen und als Datendatei zu speichern, die zur Laufzeit geladen wird. Die `__init__()` der Klasse enthält dann hauptsächlich den Code der die Ereignisse mit Methoden verbindet.

In Deinem ursprünglichen Code ist die zusätzliche Methode auch der einzige Grund warum `root` an das Objekt gebunden wird. Wäre sonst ja gar nicht notwendig gewesen.

Re: GUI friert ein, Probleme mit Threading...

Verfasst: Sonntag 3. April 2022, 14:58
von Ares
In Deinem ursprünglichen Code ist die zusätzliche Methode auch der einzige Grund warum `root` an das Objekt gebunden wird. Wäre sonst ja gar nicht notwendig gewesen.
Aha ok, verstehe ich, vielen Dank.

Ich hab jetzt hier nochmal den Code überarbeitet und versucht die Methode als einen Thread auszuführen, der mir dann in die Queue schreibt. Das Ergebnis ist leider das gleiche. Die GUI reagiert nicht und ich bekomme zwar die message aus der Queue gedruckt aber erst nachdem allers erledigt ist.

Code: Alles auswählen

#!/usr/bin/env python3.9
import tkinter as tk
from tkinter import messagebox, ttk
from tkinter.filedialog import askopenfilename

from openpyxl import load_workbook
import threading

import queue
shared_queue = queue.Queue()


class Window(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        frame = tk.Frame(self, bd=2, relief=tk.GROOVE)
        frame.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=0)
        frame.rowconfigure(1, weight=1)

        ttk.Button(
            frame, text="Choose File", width=15, command=self.action
        ).grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)

        self.txt_output = tk.Text(
            frame, bg="white", bd=0, width=40, height=10)
        self.txt_output.grid(row=1, column=0, padx=10,
                             pady=5, sticky=tk.NSEW)

    def update_status(self):
        try:
            message = shared_queue.get(block=False)
        except queue.Empty:
            message = None
        print(message)

    def action(self):
        thread = threading.Thread(target=self.test_action())
        thread.deamon = True
        thread.start()
        self.after(500, self.update_status())

    def test_action(self):
        logic = Logic()

        filename = askopenfilename()
        if filename:
            self.txt_output.insert(tk.END, "Start...\n")

            shared_queue.put("Load file first time...\n")

            self.txt_output.insert(tk.END, "Load file first time...\n")
            workbook_a = logic.load_vorlage(filename)

            self.txt_output.insert(tk.END, "Load file second time...\n")
            workbook_b = logic.load_vorlage_2(filename)

            self.txt_output.insert(tk.END, "Done\n")
            messagebox.showinfo(title="Done", message="Job done")

class Logic():

    def load_vorlage(self, path):
        workbook = load_workbook(path)
        return workbook

    def load_vorlage_2(self, data):
        workbook = load_workbook(data, data_only=True, read_only=False)
        return workbook


def main():
    Window().mainloop()


if __name__ == '__main__':
    main()
Wo genau liegt der Fehler?

Danke

Re: GUI friert ein, Probleme mit Threading...

Verfasst: Sonntag 3. April 2022, 15:40
von Sirius3
Man benutzt keine globalen Variablen, also auch keine globale Queue. Die wird genauso in __init__ angelegt, wie die anderen Attribute auch.
Thread hat ein daemon-Argument, das muß man nicht nachträglich mit Schreibfehler setzen.
Thread-Target und after erwarten, wie eigentlich alle Methoden, die ein callback erwarten, die Funktion, und nicht den Rückgabewert der Funktion.
`after` führt die Funktion einmal nach der angegeben Zeit aus, deshalb muß man in der callback-Funktion selbst wieder after aufgerufen werden, denn sonst hat man ja nur einen einmaligen Aufruf und keinen wiederholten.
Im Thread darf es keine GUI-Interaktion geben, also weder askopenfilename, noch irgendeinen Zugriff auf txt_output oder eine messagebox. Das ist ja gerade der Sinn, mit einer Queue zu arbeiten.
Die Klasse `Logic` hat immer noch keinen Sinn, warum ist die immer noch da?

Re: GUI friert ein, Probleme mit Threading...

Verfasst: Sonntag 3. April 2022, 18:56
von Ares
Ui, jetzt klappt es. Vielen Dank!

Hier ist nochmal der Code.
Ist das so besser?

Code: Alles auswählen

#!/usr/bin/env python3.9
import tkinter as tk
from tkinter import messagebox, ttk
from tkinter.filedialog import askopenfilename

from openpyxl import load_workbook
import threading

import queue


class Window(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        frame = tk.Frame(self, bd=2, relief=tk.GROOVE)
        frame.grid(row=0, column=0, padx=5, pady=5, sticky=tk.NSEW)
        frame.columnconfigure(0, weight=1)
        frame.rowconfigure(0, weight=0)
        frame.rowconfigure(1, weight=1)

        ttk.Button(
            frame, text="Choose File", width=15, command=self.action
        ).grid(row=0, column=0, padx=10, pady=5, sticky=tk.W)

        self.txt_output = tk.Text(
            frame, bg="white", bd=0, width=20, height=10)
        self.txt_output.grid(row=1, column=0, padx=10,
                             pady=5, sticky=tk.NSEW)

        self.shared_queue = queue.Queue()

    def update_status(self):
        try:
            message = self.shared_queue.get(block=False)
        except queue.Empty:
            message = None

        if message == "Done...":
            self.txt_output.insert(tk.END, message + "\n")
            messagebox.showinfo(
                title="Done",
                message="Job Done")
        elif message != None:
            self.txt_output.insert(tk.END, message)

        self.after(1000, self.update_status)

    def action(self):
        filename = askopenfilename()
        if filename:
            thread = threading.Thread(
                target=self.test_action, args=([filename]))
            thread.start()
            self.after(1000, self.update_status)

    def test_action(self, filename):
        self.shared_queue.put("Start...\n")

        self.shared_queue.put("Load file first time...\n")
        workbook_a = load_vorlage(filename)

        self.shared_queue.put("Load file second time...\n")
        workbook_b = load_vorlage_2(filename)

        self.shared_queue.put("Done...")


def load_vorlage(path):
    workbook = load_workbook(path)
    return workbook


def load_vorlage_2(data):
    workbook = load_workbook(data, data_only=True, read_only=False)
    return workbook


def main():
    Window().mainloop()


if __name__ == '__main__':
    main()
Dieser Teil

Code: Alles auswählen

        if message == "Done...":
            self.txt_output.insert(tk.END, message + "\n")
            messagebox.showinfo(
                title="Done",
                message="Job Done")
        elif message != None:
            self.txt_output.insert(tk.END, message)
erscheint mir nicht sonderlich "elegant"?

Dankeschön