web-dev-qa-db-fra.com

Pyqt5 qthread + signal ne fonctionne pas + gui gel

J'essaie de faire un vérificateur de boîte aux lettres avec imap lib, cela fonctionne très bien avec python, file d'attente et multithread sans interface graphique.

Mais quand j'essaye de mettre un gui, chaque fonction que j'ai faite, fait geler le gui jusqu'à la fin.

J'ai essayé beaucoup de choses à partir de divers doc (ajout de qthread, signal, cursorr etcc) et aucun tutoriel n'a fonctionné pour moi.

Quelqu'un peut-il m'aider à comprendre comment définir ou ajouter un texte à un QtextEdit lors de l'exécution d'une fonction car il ne fonctionne que lorsque vous avez terminé.

Voici mon code:

class Checker(QtCore.QThread):
    signal = QtCore.pyqtSignal(object)

    def __init__(self, lignesmailtocheck):
        QtCore.QThread.__init__(self)
        self.lignesmailtocheck = lignesmailtocheck

    def run(self):
            lignemailtocheck = self.lignesmailtocheck.strip()                        
            maillo, passo = lignemailtocheck.split(":",1)
            debmail, finmail = maillo.split("@",1)
            setimap =["oultook.com:imap-mail.Outlook.com", "gmail.com:imap.gmail.com"]
            for lignesimaptocheck in sorted(setimap):
                    ligneimaptocheck = lignesimaptocheck.strip()
                    fai, imap = ligneimaptocheck.split(":",1)                                
                    if finmail == fai:
                            passo0 = passo.rstrip()
                            try :
                                    mail = imaplib.IMAP4_SSL(imap)
                                    mail.login(maillo, passo)
                                    mailboxok = open("MailBoxOk.txt", "a+", encoding='utf-8', errors='ignore')
                                    mailboxok.write(maillo+":"+passo+"\n")
                                    mailboxok.close()
                                    totaly = maillo+":"+passo0+":"+imap                                
                                    print(maillo+":"+passo+"\n")

                                    self.send_text.emit(totaly)
                                    time.sleep(1)
                            except imaplib.IMAP4.error:                          
                                           print ("LOGIN FAILED!!! ")
class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(400, 300)

        self.pushButton = QtWidgets.QPushButton(Form)
        self.pushButton.setGeometry(QtCore.QRect(150, 210, 75, 23))
        self.pushButton.setObjectName("pushButton")
        self.pushButton.clicked.connect(self.gogogo)

        self.openliste = QtWidgets.QToolButton(Form)
        self.openliste.setGeometry(QtCore.QRect(40, 110, 71, 21))
        self.openliste.setObjectName("openliste")

        self.textEdit = QtWidgets.QTextEdit(Form)
        self.textEdit.setGeometry(QtCore.QRect(170, 50, 201, 121))
        self.textEdit.setObjectName("textEdit")

        self.progressBar = QtWidgets.QProgressBar(Form)
        self.progressBar.setGeometry(QtCore.QRect(10, 260, 381, 23))
        self.progressBar.setValue(0)
        self.progressBar.setObjectName("progressBar")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.pushButton.setText(_translate("Form", "PushButton"))
        self.openliste.setText(_translate("Form", "..."))

    def gogogo(self):

        mailtocheck = open('File/toCheck.txt', 'r', encoding='utf-8', errors='ignore').readlines()        
        setmailtocheck = set(mailtocheck)
        for lignesmailtocheck in sorted(setmailtocheck):
            checker = Checker(lignesmailtocheck)

            thread = QThread()
            checker.moveToThread(thread)
            # connections after move so cross-thread:
            thread.started.connect(checker.run)
            checker.signal.connect(self.checkedok)
            thread.start()

    def checkedok(self, data):
        print(data)
        self.textEdit.append(data)
if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    Form = QtWidgets.QWidget()
    ui = Ui_Form()
    ui.setupUi(Form)
    Form.show()
    sys.exit(app.exec_())
13
kenjii himura

Puisqu'il y a souvent des questions sur l'utilisation de QThread dans PyQt, comme la vôtre, voici un exemple qui montre comment utiliser correctement les threads dans PyQt. J'espère que cela peut être utile en tant que réponse pour des questions similaires, j'ai donc passé un peu plus de temps que d'habitude à préparer cela.

L'exemple crée un certain nombre d'objets de travail qui s'exécutent dans des threads non principaux et communiquent avec le thread principal (c'est-à-dire GUI) via les signaux asynchrones de Qt.

import time
import sys

from PyQt5.QtCore import QObject, QThread, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QApplication, QPushButton, QTextEdit, QVBoxLayout, QWidget


def trap_exc_during_debug(*args):
    # when app raises uncaught exception, print info
    print(args)


# install exception hook: without this, uncaught exception would cause application to exit
sys.excepthook = trap_exc_during_debug


class Worker(QObject):
    """
    Must derive from QObject in order to emit signals, connect slots to other signals, and operate in a QThread.
    """

    sig_step = pyqtSignal(int, str)  # worker id, step description: emitted every step through work() loop
    sig_done = pyqtSignal(int)  # worker id: emitted at end of work()
    sig_msg = pyqtSignal(str)  # message to be shown to user

    def __init__(self, id: int):
        super().__init__()
        self.__id = id
        self.__abort = False

    @pyqtSlot()
    def work(self):
        """
        Pretend this worker method does work that takes a long time. During this time, the thread's
        event loop is blocked, except if the application's processEvents() is called: this gives every
        thread (incl. main) a chance to process events, which in this sample means processing signals
        received from GUI (such as abort).
        """
        thread_name = QThread.currentThread().objectName()
        thread_id = int(QThread.currentThreadId())  # cast to int() is necessary
        self.sig_msg.emit('Running worker #{} from thread "{}" (#{})'.format(self.__id, thread_name, thread_id))

        for step in range(100):
            time.sleep(0.1)
            self.sig_step.emit(self.__id, 'step ' + str(step))

            # check if we need to abort the loop; need to process events to receive signals;
            app.processEvents()  # this could cause change to self.__abort
            if self.__abort:
                # note that "step" value will not necessarily be same for every thread
                self.sig_msg.emit('Worker #{} aborting work at step {}'.format(self.__id, step))
                break

        self.sig_done.emit(self.__id)

    def abort(self):
        self.sig_msg.emit('Worker #{} notified to abort'.format(self.__id))
        self.__abort = True


class MyWidget(QWidget):
    NUM_THREADS = 5

    # sig_start = pyqtSignal()  # needed only due to PyCharm debugger bug (!)
    sig_abort_workers = pyqtSignal()

    def __init__(self):
        super().__init__()

        self.setWindowTitle("Thread Example")
        form_layout = QVBoxLayout()
        self.setLayout(form_layout)
        self.resize(400, 800)

        self.button_start_threads = QPushButton()
        self.button_start_threads.clicked.connect(self.start_threads)
        self.button_start_threads.setText("Start {} threads".format(self.NUM_THREADS))
        form_layout.addWidget(self.button_start_threads)

        self.button_stop_threads = QPushButton()
        self.button_stop_threads.clicked.connect(self.abort_workers)
        self.button_stop_threads.setText("Stop threads")
        self.button_stop_threads.setDisabled(True)
        form_layout.addWidget(self.button_stop_threads)

        self.log = QTextEdit()
        form_layout.addWidget(self.log)

        self.progress = QTextEdit()
        form_layout.addWidget(self.progress)

        QThread.currentThread().setObjectName('main')  # threads can be named, useful for log output
        self.__workers_done = None
        self.__threads = None

    def start_threads(self):
        self.log.append('starting {} threads'.format(self.NUM_THREADS))
        self.button_start_threads.setDisabled(True)
        self.button_stop_threads.setEnabled(True)

        self.__workers_done = 0
        self.__threads = []
        for idx in range(self.NUM_THREADS):
            worker = Worker(idx)
            thread = QThread()
            thread.setObjectName('thread_' + str(idx))
            self.__threads.append((thread, worker))  # need to store worker too otherwise will be gc'd
            worker.moveToThread(thread)

            # get progress messages from worker:
            worker.sig_step.connect(self.on_worker_step)
            worker.sig_done.connect(self.on_worker_done)
            worker.sig_msg.connect(self.log.append)

            # control worker:
            self.sig_abort_workers.connect(worker.abort)

            # get read to start worker:
            # self.sig_start.connect(worker.work)  # needed due to PyCharm debugger bug (!); comment out next line
            thread.started.connect(worker.work)
            thread.start()  # this will emit 'started' and start thread's event loop

        # self.sig_start.emit()  # needed due to PyCharm debugger bug (!)

    @pyqtSlot(int, str)
    def on_worker_step(self, worker_id: int, data: str):
        self.log.append('Worker #{}: {}'.format(worker_id, data))
        self.progress.append('{}: {}'.format(worker_id, data))

    @pyqtSlot(int)
    def on_worker_done(self, worker_id):
        self.log.append('worker #{} done'.format(worker_id))
        self.progress.append('-- Worker {} DONE'.format(worker_id))
        self.__workers_done += 1
        if self.__workers_done == self.NUM_THREADS:
            self.log.append('No more workers active')
            self.button_start_threads.setEnabled(True)
            self.button_stop_threads.setDisabled(True)
            # self.__threads = None

    @pyqtSlot()
    def abort_workers(self):
        self.sig_abort_workers.emit()
        self.log.append('Asking each worker to abort')
        for thread, worker in self.__threads:  # note Nice unpacking by Python, avoids indexing
            thread.quit()  # this will quit **as soon as thread event loop unblocks**
            thread.wait()  # <- so you need to wait for it to *actually* quit

        # even though threads have exited, there may still be messages on the main thread's
        # queue (messages that threads emitted before the abort):
        self.log.append('All threads exited')


if __name__ == "__main__":
    app = QApplication([])

    form = MyWidget()
    form.show()

    sys.exit(app.exec_())

Les principaux concepts nécessaires pour comprendre la programmation multi-thread dans PyQt sont les suivants:

  • Les threads Qt ont leur propre boucle d'événements (spécifique à chaque thread). Le thread principal, alias le thread GUI, est également un QThread, et sa boucle d'événements est gérée par ce thread.
  • Les signaux entre les threads sont transmis (de manière asynchrone) via la boucle d'événements du thread récepteur. D'où la réactivité de l'interface graphique ou de tout thread = capacité à traiter les événements. Par exemple, si un thread est occupé dans une boucle de fonction, il ne peut pas traiter les événements, il ne répondra donc pas aux signaux de l'interface graphique tant que la fonction ne reviendra pas.
  • Si un objet de travail (méthode) dans un thread peut avoir à changer son cours d'action en fonction des signaux de l'interface graphique (par exemple, pour interrompre une boucle ou une attente), il doit appeler processEvents() sur le QApplication instance. Cela permettra au QThread de traiter les événements, et donc d'appeler des créneaux en réponse aux signaux asynchrones de l'interface graphique. Notez que QApplication.instance().processEvents() semble appeler processEvents() sur chaque thread, si ce n'est pas souhaité, alors QThread.currentThread().processEvents() est une alternative valide.
  • Un appel à QThread.quit() ne quitte pas immédiatement sa boucle d'événement: il doit attendre que le slot en cours d'exécution (le cas échéant) revienne. Par conséquent, une fois qu'un thread doit quitter, vous devez attendre (). Ainsi, l'abandon d'un thread de travail implique généralement de le signaler (via un signal personnalisé) pour arrêter ce qu'il fait: cela nécessite un signal personnalisé sur un objet GUI, une connexion de ce signal à un slot de travail, et la méthode de travail du travailleur doit appeler le thread processEvents() pour permettre au signal émis d'atteindre la fente pendant le travail.
38
Oliver

Je ne peux pas tester car setimap n'est pas disponible sur mon système. J'ai renommé CheckerThread en Checker car ce n'est plus un thread (il "vit" simplement dans un thread):

class Checker(QtCore.QObject):

Remplacez ensuite simplement le contenu de la boucle dans gogogo(self) par ceci:

for lignesmailtocheck in sorted(setmailtocheck):
    checker = Checker(lignesmailtocheck)

    thread = QThread()
    checker.moveToThread(thread)
    # connections after move so cross-thread:
    thread.started.connect(checker.run)
    checker.signal.connect(self.checkedok)
    thread.start()

    self.threads.append(thread)

C'est presque toujours une bonne idée de décorer les emplacements avec pyqtSlot afin que run et checkedok soient ainsi décorés.

Le réponse SO à propos des threads Qt est assez pratique pour se souvenir des détails (notez cependant qu'il utilise des connexions à l'ancienne - vous devez traduire C++ connect( sender, SIGNAL(sig), receiver, SLOT(slot)); en PyQt5 sender.sig.connect(receiver.slot)).

2
Oliver

Désolé pour une réponse tardive mais c'est une technique qui peut résoudre des problèmes similaires.

Le problème est clair. L'interface graphique se bloque car son thread doit effectuer un autre travail. Une solution abstraite (à partir du point PyQt) est donnée ci-dessous:

  1. Créez une classe héritant de threading.Thread qui sera l'ouvrier.
  2. Passez au constructeur une file d'attente (queue.Queue) comme moyen de communication.
  3. Vous pouvez démarrer le thread de travail à partir du thread GUI et transmettre des messages à l'aide de la file d'attente.
  4. Pour que le thread GUI lise les messages, créez un QTimer avec l'intervalle de votre choix et enregistrez une fonction de rappel. Dans la fonction de rappel, lisez la file d'attente.

Exemple de code:

class Worker(threading.Thread):

    def __init__(self, queue):
        super().init()
        self.queue = queue

    def run(self):
         # Your code that uses self.queue.put(object)

class Gui:

    def __init__(self):
        self.timer = Qtimer()
        self.timer.setInterval(milliseconds)
        self.timer.timeout.connect(self.read_data)


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

        thr = Worker(self.queue)

        thr.start()


    def read_data(self):
        data = self.queue.get()

self.timer.timeout.connect enregistre la fonction de rappel.

0
Vaggos Phl