v1.5.0 - Release for restic v0.14.0 with compression and migration tool
PyQt5 - QThread
-
Eines der Geheimnisse, was mich schon was länger beschäftigt ist, wie man parallele Prozesse mit Python verwaltet. Dazu findet man viele Beispiele und man rätselt und rätselt.....
Mal gehen die Beispiele nicht, mal versteht man sie nicht. Dabei habe ich nur was gesucht, was mir möglichst einfach darstellt, wie man so was grundsätzlich aufbaut. Der Rest kommt dann mit der Zeit, wenn ich lange genug damit herumgespielt habe
Ok, warum das Ganze? Ich habe in meinem Restic UI Prozesse, die was länger dauern. Ich möchte dem Anwender das signalisieren, mittels einem waitingspinnerwidget. Ok, so weit hatte ich das auch hinbekommen, nur was mache ich mit einem Ergebnis in diesem Prozess? Das muss ja irgendwie zurück
Ich habe dann diese Code hier gefunden und zum Laufen bekommen. YEAH
#!/usr/bin/env python3 from PyQt5.QtCore import QObject, QThread from PyQt5.QtCore import pyqtSlot, pyqtSignal from PyQt5.QtWidgets import QMainWindow import sys import time from PyQt5.QtWidgets import QApplication class MainWindow(QMainWindow): def __init__(self): super().__init__() print("Base init") self.thread = QThread() self.w = Worker() self.w.finished[str].connect(self.onFinished) self.w.moveToThread(self.thread) self.thread.started.connect(self.w.work) self.thread.start() @pyqtSlot(str) def onFinished(self, i): #print("Base caught finished, {}".format(i)) print("Worker end", i) class Worker(QObject): finished = pyqtSignal(str) def __init__(self): print("Worker init") super().__init__() def work(self): print("Worker work") time.sleep(5) self.finished.emit('TEXT') if __name__ == "__main__": app = QApplication(sys.argv) w = MainWindow() w.show() sys.exit(app.exec_())
Das ist jetzt nicht so verdammt schwer, ein wenig hat man ja schon gelernt. Es geht ein Fenster auf, der Hauptprozess startet den Worker als eigenen Prozess. Der macht nicht viel, als 5 Sekunden zu warten und dann wenn er beendet wird, sendet er ein Signal an den Hauptprozess def onFinished(self, i): welcher dann eine Meldung in die Konsole ausgibt. Nix besonderes, aber für mich ein Ansatz meine Aufgabe zu lösen.
Quelle: https://stackoverflow.com/questions/29243692/pyqt5-how-to-make-qthread-return-data-to-main-thread
-
Das Thema muss sitzen, also versuche ich das etwas zu vertiefen. Gestern habe ich viel Stunden damit verbracht, mein Restic UI auf das waitingspinnerwidget umzubauen. Bis auf eine Funktion, die heute noch dran kommt, habe ich alles hinbekommen Aber, wie immer habe ich danach Fragen. Ok, fangen wir mal an.
@pyqtSlot(str) def onFinished(self, i): #print("Base caught finished, {}".format(i)) print("Worker end", i)
Wenn wir uns den Code anschauen, kommt die Frage auf, was macht @pyqtSlot(str) ? Ich probiere viel aus, also hatte ich das in meinem Code auskommentiert und mich gewundert, warum das immer noch funktioniert!? Ok, also Dokumentation lesen.
Connecting a signal to a decorated Python method also has the advantage of reducing the amount of memory used and is slightly faster.
Das klärt meine Frage aber immer noch nicht komplett. Auf stackoverflow.com, das folgende gefunden.
For your case consider it as a good practice - denoting a method as a slot for some signal.
function of pyqtSlot
I just read a tutorial about pyqt5 button from here. And the code is as below. There is a question about button.clicked.connect(self.on_click) and @pyqtSlot(). If I delete @pyqtSlot() from the code...
Stack Overflow (stackoverflow.com)
Also muss ich mir jetzt merken, das es weniger Speicher verbraucht und etwas schneller ist. Gut, und es ordentlicher Programmierstil(?), wenn ich die Funktionen als abhängig von Signalen kennzeichne!? Ich denke, so passt es ganz gut.
Sehr gute Anleitungen zum Thema findet man auch immer wieder auf realpython.com. Hier z.B. zum Thema QThread. Anhand des Beispieles habe ich meinen ersten QThread heute folgendermaßen dokumentiert.
"First Thread with documentation" # Worker for restic_stats # Create a QThread object self.thread = QThread() # Create a worker object self.worker1 = Worker() # Move worker to the thread self.worker1.moveToThread(self.thread) # Connect signals and slots self.thread.started.connect(self.worker1.run) self.worker1.stats_finished[str].connect(self.restic_stats_finished) self.worker1.stats_error[str].connect(self.restic_stats_error) "First thread end"
Dann stand ich sehr schnell vor dem Problem, was mache ich bei der nächsten Funktion, die einen QThread braucht?
# Worker for restic_backup self.thread2 = QThread() self.worker2 = Worker2() self.worker2.moveToThread(self.thread2) self.thread2.started.connect(self.worker2.run) self.worker2.backup_finished[str].connect(self.restic_backup_finished) self.worker2.backup_error[str].connect(self.restic_backup_error)
Ok, das war jetzt nicht so wirklich schwierig.
-
Und hier mal ein komplettes Beispiel.
class Worker
Wir legen den Worker an, das ist der Prozess der die Arbeit macht und etwas länger braucht.
class Worker(QObject): """ Worker Class for Rest function stats""" stats_finished = pyqtSignal(str) stats_error = pyqtSignal(str) def __init__(self): super().__init__() def run(self): # Restic function try: # long running task except Exception: # Process don't successful, send signal self.stats_error.emit(result.stderr) else: # Process successful, send signal self.stats_finished.emit(result.stdout) finally: pass
in class MainWindow
class MainWindow(QMainWindow): def __init__(self): super().__init__() "First Thread with documentation" # Worker for restic_stats # Create a QThread object self.thread = QThread() # Create a worker object self.worker1 = Worker() # Move worker to the thread self.worker1.moveToThread(self.thread) # Connect signals and slots self.thread.started.connect(self.worker1.run) self.worker1.stats_finished[str].connect(self.restic_stats_finished) self.worker1.stats_error[str].connect(self.restic_stats_error) "First thread end" ############################################### # Process for restic_stats is finished ############################################### @pyqtSlot(str) def restic_stats_finished(self, i): # Signal from worker thread without an error Spinner.stop(self) self.thread.quit() ############################################### # Process for restic_stats when get an error ############################################### @pyqtSlot(str) def restic_stats_error(self, i): # Signal from worker thread with an error! Spinner.stop(self)
Funktion restic_stats
def restic_stats(self): # we start the worker thread self.thread.start() # we start waitingspinnerwidget Spinner.start(self)
Ich wollte gerade schreiben, das folgendes sehr wichtig ist
self.thread.quit()
da fällt mir ein Fehler auf. Kurz ausprobiert und bingo, wenn der Prozess einen Error triggert, muss natürlich auch der Prozess beendet werden. Wenn man das nicht macht, macht das Programm nicht das was es soll. Der Grund ist, das der Prozess einfach immer weiter läuft. Er MUSS beendet werden. Ich gehe dann mal in meinem Programm alles ändern
Ok, jetzt geht die Funktion auch zweimal hintereinander und gibt auch ordentlich den Fehler aus.
Fazit
Ich habe wieder sehr viel gelernt und hoffe das ich es auch richtig verstanden habe Hoffe das es dem ein oder anderen Anfänger hilft. Und falls hier ein Profi mitliest und hier Blödsinn steht bitte ich um einen Kommentar, damit ich das ändern kann. Es steht schon genug Blödsinn im Netz
-
-
-
Restic UI - Changelog
Angeheftet Restic UI -
-
-
-
-
Python und GUI
Verschoben Python3