web-dev-qa-db-ja.com

Pyqt5 qthread +シグナルが機能しない+ GUIフリーズ

imap libを使用してメールボックスチェッカーを作成しようとしていますが、Python、キュー、およびGUIなしのマルチスレッドでかなり正常に動作します。

しかし、GUIを作成しようとすると、作成したすべての機能が終了するまでGUIをフリーズさせます。

私はさまざまなドキュメント(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_())
11
kenjii himura

PyQtでQThreadを使用することに関する質問はよくあるので、あなたのものと同様に、PyQtでスレッドを正しく使用する方法を示す例を次に示します。似たような質問に対する後藤回答として役立つことを期待しているので、これを準備する通常よりも少し時間を費やしました。

この例では、非メインスレッドで実行され、Qtの非同期信号を介してメイン(つまりGUI)スレッドと通信する多数のワーカーオブジェクトを作成します。

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からの信号に基づいてアクションのコースを変更する必要がある場合(ループや待機を中断するなど)、QApplicationインスタンスでprocessEvents()を呼び出す必要があります。これにより、QThreadがイベントを処理し、GUIからの非同期信号に応じてスロットを呼び出すことができます。 QApplication.instance().processEvents()はすべてのスレッドでprocessEvents()を呼び出すようです。これが望ましくない場合は、QThread.currentThread().processEvents()が有効な代替手段です。
  • QThread.quit()を呼び出しても、そのイベントループはすぐには終了しません。現在実行中のスロット(存在する場合)が戻るまで待機する必要があります。したがって、スレッドが終了するように指示されたら、そのスレッドでwait()する必要があります。そのため、ワーカースレッドを中止するには、通常、カスタムシグナルを介してシグナルを送信して、実行中の処理を停止します。これには、GUIオブジェクトのカスタムシグナル、ワーカースロットへのシグナルの接続、およびワーカーのワークメソッドがスレッドのprocessEvents()を呼び出す必要があります作業中に放出信号がスロットに到達できるようにします。
38
Oliver

システムでsetimapが使用できないため、テストできません。 CheckerThreadCheckerに名前を変更しました。これは、スレッドではなくなったためです(スレッド内で「生きている」だけです)。

_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で装飾することをお勧めします。したがって、runcheckedokの両方を装飾する必要があります。

Qtスレッドに関するSOの答え は、詳細を思い出すのに非常に便利です(ただし、古いスタイルの接続を使用していることに注意してください-C++ connect( sender, SIGNAL(sig), receiver, SLOT(slot));をPyQt5 sender.sig.connect(receiver.slot))。

2
Oliver

遅い回答で申し訳ありませんが、同様の問題を解決できる手法です。

問題は明らかです。スレッドは別のジョブを実行する必要があるため、GUIはフリーズします。 (PyQtポイントから)抽象化されたソリューションを以下に示します。

  1. ワーカーになるthreading.Threadを継承するクラスを作成します。
  2. 通信の手段としてqueue(queue.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.timer.timeout.connectは、コールバック関数を登録します。

0
Vaggos Phl