PyQt4: Как приостановить поток до тех пор, пока не выйдет сигнал?

У меня есть следующий pyqtmain.py:

#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        self.qt_app = QApplication(sys.argv)
        QMainWindow.__init__(self, parent)

        buttonWidget = QWidget()
        rsltLabel = QLabel("Result:")
        self.rsltFiled = QLineEdit()
        self.buttonStart = QPushButton("Start")

        verticalLayout = QVBoxLayout(buttonWidget)
        verticalLayout.addWidget(rsltLabel)
        verticalLayout.addWidget(self.rsltFiled)
        verticalLayout.addWidget(self.buttonStart)

        butDW = QDockWidget("Control", self)
        butDW.setWidget(buttonWidget)
        self.addDockWidget(Qt.LeftDockWidgetArea, butDW)

        self.mthread = QThread()  # New thread to run the Measurement Engine
        self.worker = MeasurementEngine()  # Measurement Engine Object

        self.worker.moveToThread(self.mthread)
        self.mthread.finished.connect(self.worker.deleteLater)  # Cleanup after thread finished

        self.worker.measure_msg.connect(self.showRslt)

        self.buttonStart.clicked.connect(self.worker.run)

        # Everything configured, start the worker thread.
        self.mthread.start()

    def run(self):
        """ Show the window and start the event loop """
        self.show()
        self.qt_app.exec_()  # Start event loop

    @pyqtSlot(str)
    def showRslt(self, mystr):
        self.rsltFiled.setText(mystr)


def main():
    win = MainWindow()
    win.run()


if __name__ == '__main__':
    main()

И еще один поток script выполняет фактическое измерение:

from PyQt4.QtCore import *
import time

class MeasurementEngine(QObject):
    measure_msg = pyqtSignal(str)
    def __init__(self):
        QObject.__init__(self)  # Don't forget to call base class constructor

    @pyqtSlot()
    def run(self):
        self.measure_msg.emit('phase1')
        time.sleep(2) # here I would like to make it as an interrupt
        self.measure_msg.emit('phase2')

Что теперь делает этот код, так как после нажатия кнопки "Пуск" запускается функция, выполняемая в потоке. Однако на самом деле в прогоне функции есть две фазы измерения. Прямо сейчас я использовал задержку времени.

Но то, что я хотел бы реализовать на самом деле, заключается в том, что после измерения "phase1". Появится окно с сообщением, и в то же время поток будет приостановлен/удерживаться. Пока пользователь не закрыл окно сообщения, функция потока будет возобновлена.

Ответ 1

Вы не можете отобразить QDialog в пределах QThread. Все связанные с GUI вещи должны быть выполнены в потоке графического интерфейса (тот, который создал объект QApplication). Вы можете использовать 2 QThread:

  • 1st: выполнить phase1. Вы можете подключить сигнал finished этого QThread к слоту в QMainWindow, который отобразит всплывающее окно (используя QDialog.exec_(), чтобы он был модальным).
  • 2nd: выполнить phase2. Вы создаете QThread после того, как всплывающее окно, показанное здесь выше, было закрыто.

Ответ 2

Используйте QWaitCondition из модуля QtCore. Используя блокировку мьютекса, вы устанавливаете фоновый поток на ожидание/спящий режим до тех пор, пока поток переднего плана не пробудит его. Затем он продолжит выполнять свою работу оттуда.

#!/usr/bin/python3
import sys
from PyQt4.QtCore import *
from PyQt4.QtGui import *
from pyqtMeasThread import *


class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        self.qt_app = QApplication(sys.argv)
        QMainWindow.__init__(self, parent)

        buttonWidget = QWidget()
        rsltLabel = QLabel("Result:")
        self.rsltFiled = QLineEdit()
        self.buttonStart = QPushButton("Start")

        verticalLayout = QVBoxLayout(buttonWidget)
        verticalLayout.addWidget(rsltLabel)
        verticalLayout.addWidget(self.rsltFiled)
        verticalLayout.addWidget(self.buttonStart)

        butDW = QDockWidget("Control", self)
        butDW.setWidget(buttonWidget)
        self.addDockWidget(Qt.LeftDockWidgetArea, butDW)

        self.mutex = QMutex()
        self.cond = QWaitCondition()
        self.mthread = QThread()  # New thread to run the Measurement Engine
        self.worker = MeasurementEngine(self.mutex, self.cond)  # Measurement Engine Object

        self.worker.moveToThread(self.mthread)
        self.mthread.finished.connect(self.worker.deleteLater)  # Cleanup after thread finished

        self.worker.measure_msg.connect(self.showRslt)

        self.buttonStart.clicked.connect(self.worker.run)

        # Everything configured, start the worker thread.
        self.mthread.start()

    def run(self):
        """ Show the window and start the event loop """
        self.show()
        self.qt_app.exec_()  # Start event loop

    # since this is a slot, it will always get run in the event loop in the main thread
    @pyqtSlot(str)
    def showRslt(self, mystr):
        self.rsltFiled.setText(mystr)
        msgBox = QMessageBox(parent=self)
        msgBox.setText("Close this dialog to continue to Phase 2.")
        msgBox.exec_()
        self.cond.wakeAll()


def main():
    win = MainWindow()
    win.run()


if __name__ == '__main__':
    main()

и

from PyQt4.QtCore import *
import time

class MeasurementEngine(QObject):
    measure_msg = pyqtSignal(str)
    def __init__(self, mutex, cond):
        QObject.__init__(self)  # Don't forget to call base class constructor
        self.mtx = mutex
        self.cond = cond

    @pyqtSlot()
    def run(self):
        # NOTE: do work for phase 1 here
        self.measure_msg.emit('phase1')
        self.mtx.lock()
        try:
            self.cond.wait(self.mtx)
            # NOTE: do work for phase 2 here
            self.measure_msg.emit('phase2')
        finally:
            self.mtx.unlock()

В то же время ваше время немного сбито. Вы создаете приложение и запускаете поток, прежде чем вы даже покажете свое окно. Таким образом, окно сообщения появится до, даже если всплывет главное окно. Чтобы получить правильную последовательность событий, вы должны начать свой поток как часть метода run вашего MainWindow, после, которое вы уже сделали основным окном. Если вы хотите, чтобы условие ожидания было отделено от настроек сообщений, вам может понадобиться отдельный сигнал и слот, чтобы справиться с этим.

Ответ 3

Ваш поток может выдать сигнал в главное окно, чтобы отобразить диалог. Если вы не хотите закрывать поток, пока диалог открыт, поток может входить в цикл while для ожидания. В цикле while он может непрерывно проверять переменную, которую основной поток может установить в true после завершения диалога. Это может быть не самое чистое решение, но оно должно работать.

Чтобы немного прояснить мой ответ, я добавил несколько псевдокодов. Что вам нужно, так это то, как вы делитесь переменной dialog_closed. Вы можете, например, используйте переменную-член класса потока.

Thread:
emit_signal
dialog_closed = False
while not dialog_closed:
   pass
go_on_with_processing

MainThread:
def SignalRecieved():
   open_dialog
   dialog_closed = True

Ответ 4

Недавно мне пришлось решить эту проблему, я провел небольшое исследование и обнаружил элегантную технику, которая, кажется, работает надежно. Мне не нужно было подробно описывать всю сложность, поэтому вот краткое описание шагов, которые я предпринял.

Мой класс графического интерфейса определяет, как атрибуты класса, два сигнала.

oyn_sig = pyqtSignal(str)       # Request for operator yes/no
ryn_sig = pyqtSignal(bool)      # Response to yes/no request

Внутри метода, который инициализирует компоненты GUI, этот сигнал связан с обработчиком сигнала экземпляра GUI.

    self.oyn_sig.connect(self.operator_yes_no)

Вот код для метода-обработчика GUI:

@pyqtSlot(str)
def operator_yes_no(self, msg):
    "Asks the user a 'yes/no question on receipt of a signal then signal a bool answer.'"
    answer = QMessageBox.question(None,
                                   "Confirm Test Sucess",
                                   msg,
                                   QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
    # Signal the caller that the result was received.
    self.ryn_sig.emit(answer==QMessageBox.Yes)

Как обычно, графический интерфейс работает в основном потоке, и поэтому он должен сигнализироваться из потока, выполняющего работу в фоновом режиме. В свою очередь, как только он получил ответ оператора, он генерирует ответный сигнал в исходящий поток.

Рабочий поток использует следующую функцию для получения ответа оператора.

def operator_yes_no(self, msg):
    loop = LoopSpinner(self.gui, msg)
    loop.exec_()
    return loop.result

Это создает объект LoopSpinner и начинает выполнять его цикл событий, тем самым приостанавливая цикл событий текущего потока, пока не завершится "внутренний поток". Большинство LoopSpinner скрыто внутри класса LoopSpinner, который, вероятно, должен был быть назван лучше. Вот его определение.

class LoopSpinner(QEventLoop):

    def __init__(self, gui, msg):
        "Ask for an answer and communicate the result."
        QEventLoop.__init__(self)
        gui.ryn_sig.connect(self.get_answer)
        gui.oyn_sig.emit(msg)

    @pyqtSlot(bool)
    def get_answer(self, result):
        self.result = result
        self.quit()

Экземпляр LoopSpinner соединяет ответный сигнал со своим методом get_answer и излучает сигнал вопроса. Когда сигнал получен, ответ сохраняется как значение атрибута и цикл завершается. На этот цикл все еще ссылается вызывающая сторона, которая может безопасно получить доступ к атрибуту результата до того, как экземпляр будет собран.