PyQt5 Signale und Slots

Python und das Qt-Toolkit, erstellen von GUIs mittels des Qt-Designers.
Antworten
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Moin,
ich versuche mich an einer GUI für einen Dateidownload. Wie zu erwarten friert die GUI wärend des Downloads ein. Habe dann hier im Forum gestöbert und bin in einem Thread auf diesen Link gestoßen https://realpython.com/python-pyqt-qthread/. Ich verstehe die dort geschilderte Funktionsweise mit dem auslagern des 'Long running task' in einen Worker-Thread und die Signale von dort zurück in den Window Thread.

Was mir nicht einleuchtet, wie schicke ich Signale vom Window Thread an den Worker Thread? Ich verstehe das senden ansich schon, nur werden die Slots im Window Thread in Methoden abgearbeitet. Der Worker-Thread hat aber nur die 'run()' Methode, in der der 'Long running task' läuft. Kann ich da auch beliebig neue Methoden erstellen?

Um meine Frage zu veranschaulichen, hier mal der Code aus dem Link oben.
Wenn ich nun in der Worker Klasse das "for i in range(5)" in ein "for i in range(x)" umschreiben möchte und dann das x aus der Window Klasse an die Worker Klasse übergeben möchte, wie mache ich das?

Code: Alles auswählen

import sys
from time import sleep
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

# Step 1: Create a worker class
class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def run(self):
        """Long-running task."""
        for i in range(5):
            sleep(1)
            self.progress.emit(i + 1)
        self.finished.emit()

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.clicksCount = 0
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("Freezing GUI")
        self.resize(300, 150)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        
        # Create and connect widgets
        self.clicksLabel = QLabel("Counting: 0 clicks", self)
        self.clicksLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.stepLabel = QLabel("Long-Running Step: 0")
        self.stepLabel.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.countBtn = QPushButton("Click me!", self)
        self.countBtn.clicked.connect(self.countClicks)
        self.longRunningBtn = QPushButton("Long-Running Task!", self)
        self.longRunningBtn.clicked.connect(self.runLongTask)
        
        # Set the layout
        layout = QVBoxLayout()
        layout.addWidget(self.clicksLabel)
        layout.addWidget(self.countBtn)
        layout.addStretch()
        layout.addWidget(self.stepLabel)
        layout.addWidget(self.longRunningBtn)
        self.centralWidget.setLayout(layout)

    def countClicks(self):
        self.clicksCount += 1
        self.clicksLabel.setText(f"Counting: {self.clicksCount} clicks")

    def reportProgress(self, n):
        self.stepLabel.setText(f"Long-Running Step: {n}")

        # Snip...
    def runLongTask(self):
        # Step 2: Create a QThread object
        self.thread = QThread()

        # Step 3: Create a worker object
        self.worker = Worker()

        # Step 4: Move worker to the thread
        self.worker.moveToThread(self.thread)

        # Step 5: Connect signals and slots
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.reportProgress)
        
        # Step 6: Start the thread
        self.thread.start()

        # Final resets
        self.longRunningBtn.setEnabled(False)
        self.thread.finished.connect(
            lambda: self.longRunningBtn.setEnabled(True)
        )
        self.thread.finished.connect(
            lambda: self.stepLabel.setText("Long-Running Step: 0")
        )

app = QApplication(sys.argv)
win = Window()
win.show()
sys.exit(app.exec())
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Grundsätzlich sind alle Methoden auf dem Worker ja auch slots, und entsprechend können die auch per queued connection aus dem main Thread getriggert werden. Aber damit das funktioniert muss der thread in seinem exec die eigenen Ereignisschleife abarbeiten. Das gut er nach Anlage, aber wenn du dann das run (übrigens ganz beschissen benannt durch den Autor der Seite, denn das ist gleich der run Methode im thread selbst, und die sollte man nicht überladen, und auch den worker so nicht nennen, um Verwirrung zu vermeiden. work wäre passend) via started connection aufgerufen wird, kehrt das ja erst wieder zurück, wenn die Arbeit komplett ist. Mit anderen Worten: der leidet unter dem gleichen Phänomen wie der main Thread, wenn da etwas lange läuft. Erst wenn run beendet ist, kommen folgende slots zum Zug.
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Danke deets, das hilft mir schonmal ein wenig weiter. Muß ich mal schauen ob ich dieses Phänomen irgendwie umschiffen kann.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Was muss denn da umschifft werden, und was ist das Ziel?
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Ich möchte dem Thread Argumente / Parameter beim aufrufen mitgeben.
Ich habe eine GUI um mit pytube Videos zu laden. Wenn ich den Download in den Thread auslagere, muß ich ihm ja auch die URL, den gewählten Stream, das gewählte Downloadverzeichnis usw. mitgeben. Das ist mein Ziel. Gefunden habe ich allerdings nur Anleitungen, in denen Argumente / Signale vom Thread kommen aber keine, wo welche zum Thread hingehen.
__deets__
User
Beiträge: 14493
Registriert: Mittwoch 14. Oktober 2015, 14:29

Haeh? Du schickst doch ein signal zum Thread. Du hast das Signal "started" mit Slot "run" verbunden. started hat keine Argumente, und run auch nicht. Also wird run im Background Thread abgearbeitet.

Wenn du das anders willst, dann definier doch ein Slot "download_youtube_video(self, url, path)", und sende dem ein Signal, dass eben diese 2 Argumente nutzt. Die Verbindung started/run darf dann natuerlich nicht mehr existieren. Denn started kommt ja automatisch.

Nachtrag: das Signal hat natürlich nur 2 Argumente. Aber der Slot braucht das Self.
Welpe
User
Beiträge: 26
Registriert: Mittwoch 30. Dezember 2020, 10:39

Ok, das probiere ich aus. Danke für deine Geduld.
Antworten