PyQt4: How to pause a Thread until a signal is emitted?
Asked Answered
C

4

11

I have the following 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()

And another thread script performing the actual measurement:

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')

What this code does now is that after the Start button is pressed, the function run in the thread will be executed. However, actually in the function run, there are two phases of the measurement. Right now I used an time delay.

But what I would like to implement actually is that after the 'phase1' measurement is done. A message box will be popped up, and at the same time, the thread will be paused/held. Until the user closed the message box, then the thread function will be resumed.

Crenulate answered 9/12, 2015 at 8:55 Comment(0)
S
2

You can't display a QDialog from within a QThread. All GUI related stuff must be done in the GUI thread (the one that created the QApplication object). What you could do is to use 2 QThread:

  • 1st: perform phase1. You can connect the finished signal of this QThread to a slot in the QMainWindow that will display the popup (using QDialog.exec_() so it will be modal).
  • 2nd: perform phase2. You create the QThread after the popup shown here above has been closed.
Soave answered 11/12, 2015 at 10:1 Comment(0)
P
11

Use a QWaitCondition from the QtCore module. Using a mutex lock, you set the background thread to wait/sleep until the foreground thread wakes it back up. Then it will continue doing its work from there.

#!/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()

And:

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

Your timing is a little bit off in all this though. You create the app and start the thread before you even show your window. Thus, the message box will pop up before the main window even pops up. To get the right sequence of events, you should start your thread as part of the run method of your MainWindow, after you have already made the main window visible. If you want the wait condition to be separate from the setting of the messages, you may need a separate signal and slot to deal with that.

Prevision answered 18/12, 2015 at 2:3 Comment(0)
S
2

You can't display a QDialog from within a QThread. All GUI related stuff must be done in the GUI thread (the one that created the QApplication object). What you could do is to use 2 QThread:

  • 1st: perform phase1. You can connect the finished signal of this QThread to a slot in the QMainWindow that will display the popup (using QDialog.exec_() so it will be modal).
  • 2nd: perform phase2. You create the QThread after the popup shown here above has been closed.
Soave answered 11/12, 2015 at 10:1 Comment(0)
R
1

Your thread can emit a signal to the main window to show the dialog. If you don't want to close the thread while the dialog is open, the thread could enter a while loop for waiting. In the while loop it can continuously check a variable which the main thread can set to true after the dialog is finished. This might not be the cleanest solution, but it should work.

To clarify my answer a bit, I added some pseudo code. What you have to care about is how you share the dialog_closed variable. You could e.g. use a member variable of the thread class.

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

MainThread:
def SignalRecieved():
   open_dialog
   dialog_closed = True
Rentroll answered 13/12, 2015 at 21:33 Comment(1)
Surely the whole point of slots and signals is to avoid the necessity to loop indefinitely waiting for something to happen? Every CPU cycle spent doing this is wasted.Tamelatameless
T
0

I recently had to solve pretty much this problem, did a little research and discovered an elegant technique that seems to work reliably. I didn't need the full complexity detailed there, so here's an outline of the steps I took.

My GUI class defines, as class attributes, two signals.

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

Inside the method that initialises the GUI components this signal is connected to the GUI instance's signal handler.

    self.oyn_sig.connect(self.operator_yes_no)

Here's the code for the handler method of the 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)

As usual the GUI is running in the main thread, and so it needs to be signalled from the thread doing the work in the background. In turn, once it's received the operator's response it raises a response signal to the originating thread.

The worker thread uses the following function to get an operator response.

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

This creates a LoopSpinner object and starts executing its event loop, thereby suspend the current thread's event loop until the "inner thread" terminates. Most of the smarts are hidden inside the LoopSpinner class, which should probably have been better named. Here's its definition.

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

A LoopSpinner instance connects the response signal to its get_answer method and emits the question signal. When the signal is received the answer is stored as an attribute value and the loop quits. The loop is still referenced by its caller, which can safely access the result attribute before the instance is garbage collected.

Tamelatameless answered 18/4, 2019 at 16:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.