Pyqt5 qthread + сигнал не работает + замораживание gui

Я пытаюсь сделать проверку почтового ящика с imap lib, он отлично работает с python, queue и multithread без gui.

но когда я пытаюсь поставить gui, каждый fonction я сделал, сделать gui заморозить до конца .

Я пробовал много вещей из разных doc (добавить qthread, signal, cursorr etcc) и учебники, которые не работали для меня .

может кто-нибудь помочь мне понять, как установить или добавить текст в QtextEdit при запуске функции, потому что она работает только после финиша .

вот мой код :

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_())

3 ответов


так как часто возникают вопросы об использовании QThread в PyQt, аналогично вашему, вот пример, который показывает, как правильно использовать потоки в PyQt. Я надеюсь, что это может быть полезно в качестве Гото-ответа на подобные вопросы, поэтому я потратил немного больше времени, чем обычно, готовя это.

в примере создается ряд рабочих объектов, которые выполняются в неосновных потоках и взаимодействуют с основным потоком (т. е. GUI) через асинхронные сигналы 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_())

главная понятия, необходимые для понимания многопоточного программирования в PyQt, следующие:

  • потоки Qt имеют свой собственный цикл событий (специфичный для каждого потока). Основной поток, он же поток GUI, также является QThread, и его цикл событий управляется этим потоком.
  • сигналы между потоками передаются (асинхронно) через цикл событий принимающего потока. Следовательно, отзывчивость GUI или любой поток = способность обрабатывать события. Е. Г., если поток занят в цикл функции, он не может обрабатывать события, поэтому он не будет отвечать на сигналы от GUI, пока функция не вернется.
  • если рабочий объект (метод) в потоке может изменить свой курс действия на основе сигналов от GUI (скажем, прервать цикл или ожидание), он должен вызвать processEvents() на QApplication экземпляра. Это позволит QThread обрабатывать события и, следовательно, вызывать слоты в ответ на асинхронные сигналы от GUI. Обратите внимание, что QApplication.instance().processEvents() кажется, назвал processEvents() на каждый нить, если это не желательно, то QThread.currentThread().processEvents() является достойной альтернативой.
  • вызов QThread.quit() не сразу выходит из цикла событий: он должен ждать возврата текущего исполняемого слота (если таковой имеется). Следовательно, как только потоку говорят выйти, вы должны ждать() на нем. Таким образом, прерывание рабочего потока обычно включает в себя сигнализацию (через пользовательский сигнал), чтобы остановить все, что он делает: для этого требуется пользовательский сигнал на объекте GUI, соединение этого сигнала с рабочим слотом и рабочий метод работы необходимо позвонить позволить испущенному сигналу достигнуть слот пока делающ работу.

Я не могу проверить, потому что setimap недоступен в моей системе. Я переименовал CheckerThread to Checker поскольку это больше не поток (он просто "живет" в потоке):

class Checker(QtCore.QObject):

тогда просто замените содержимое цикла в gogogo(self) С этого:

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)

это почти всегда хорошая идея, чтобы украсить слоты с pyqtSlot так как run и checkedok должно быть таким образом украшено.

на Итак, ответьте о потоках Qt вполне удобно напомните себе о деталях (обратите внимание, однако, что он использует соединения старого стиля - вы должны перевести C++ connect( sender, SIGNAL(sig), receiver, SLOT(slot)); на PyQt5 sender.sig.connect(receiver.slot)).


извините за поздний ответ, но это метод, который может решить подобные проблемы.

проблема понятна. GUI зависает, потому что его поток должен выполнить другую работу. Ниже приведено абстрактное (из точки PyQt) решение:

  1. создайте класс, наследующий от threading.Поток, который будет рабочим.
  2. передайте конструктору очередь (queue.Очередь) как средство связи.
  3. вы можете запустить рабочий поток из GUI поток и передача сообщений с помощью очереди.
  4. чтобы поток GUI читал сообщения, создайте QTimer с интервалом по вашему выбору и зарегистрируйте функцию обратного вызова. В функции обратного вызова прочитайте очередь.

Пример Кода:

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.таймер.перерыв.подключить регистрирует функцию обратного вызова.