10

I have a thread class "MyThread" and my main application which is simply called "Gui". I want to create a few objects from the thread class but for this example I created only one object. The thread class does some work, then emits a signal to the Gui class, indicating that a user input is needed (this indication for now is simply changing the text of a button). Then the thread should wait for a user input (in this case a button click) and then continue doing what it is doing...

from PyQt4 import QtGui, QtCore
class MyTrhead(QtCore.QThread):
    trigger = QtCore.pyqtSignal(str)

    def run(self):
        print(self.currentThreadId())
        for i in range(0,10):
            print("working ")
            self.trigger.emit("3 + {} = ?".format(i))
            #### WAIT FOR RESULT
            time.sleep(1)


class Gui(QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(Gui, self).__init__(parent)
        self.setupUi(self)
        self.pushButton.clicked.connect(self.btn)

        self.t1 = MyTrhead()
        self.t1.trigger.connect(self.dispaly_message)
        self.t1.start()
        print("thread: {}".format(self.t1.isRunning()))


    @QtCore.pyqtSlot(str)
    def dispaly_message(self, mystr):
        self.pushButton.setText(mystr)

    def btn(self):
        print("Return result to corresponding thread")



if "__main__" == __name__:
    import sys
    app = QtGui.QApplication(sys.argv)
    m = Gui()
    m.show()
    sys.exit(app.exec_())

How can I wait in (multiple) threads for a user input?

Maecky
  • 1,988
  • 2
  • 27
  • 41

1 Answers1

15

By default, a QThread has an event loop that can process signals and slots. In your current implementation, you have unfortunately removed this behaviour by overriding QThread.run. If you restore it, you can get the behaviour you desire.

So if you can't override QThread.run(), how do you do threading in Qt? An alternative approach to threading is to put your code in a subclass of QObject and move that object to a standard QThread instance. You can then connect signals and slots together between the main thread and the QThread to communicate in both directions. This will allow you to implement your desired behaviour.

In the example below, I've started a worker thread which prints to the terminal, waits 2 seconds, prints again and then waits for user input. When the button is clicked, a second separate function in the worker thread runs, and prints to the terminal in the same pattern as the first time. Please note the order in which I use moveToThread() and connect the signals (as per this).

Code:

from PyQt4.QtCore import *
from PyQt4.QtGui import *
import time

class MyWorker(QObject):

    wait_for_input = pyqtSignal()
    done = pyqtSignal()


    @pyqtSlot()
    def firstWork(self):
        print 'doing first work'
        time.sleep(2)
        print 'first work done'
        self.wait_for_input.emit()

    @pyqtSlot()
    def secondWork(self):
        print 'doing second work'
        time.sleep(2)
        print 'second work done'
        self.done.emit()


class Window(QWidget):
    def __init__(self, parent = None):
        super(Window, self).__init__()

        self.initUi()
        self.setupThread()

    def initUi(self):
        layout = QVBoxLayout()
        self.button = QPushButton('User input')
        self.button.setEnabled(False)
        layout.addWidget(self.button)
        self.setLayout(layout)
        self.show()

    @pyqtSlot()
    def enableButton(self):
        self.button.setEnabled(True)

    @pyqtSlot()    
    def done(self):
        self.button.setEnabled(False)

    def setupThread(self):
        self.thread = QThread()
        self.worker = MyWorker()

        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.firstWork)
        self.button.clicked.connect(self.worker.secondWork)
        self.worker.wait_for_input.connect(self.enableButton)
        self.worker.done.connect(self.done)

        # Start thread
        self.thread.start()    

if __name__ == "__main__":
    app = QApplication([])
    w = Window()
    app.exec_()
Community
  • 1
  • 1
three_pineapples
  • 11,579
  • 5
  • 38
  • 75
  • Thanks for your help, one more question: How can I pass arguments from the function firstWork to the GUI and pass the result back to secondWork in case I have more than one of these worker threads? Think of, the function firstWork asks a question, passes this to the user and second work gets the answer? – Maecky Feb 21 '16 at 13:42
  • 1
    Change the slot decorator to `@pyqtSlot( )` followed by the same signature of the function that represents the slot `def firstwork(self, )`. For example `@pyqtSlot(int, str)` means that the slot requires 2 arguments (one of `int` and another of `str` type). The function that the slot decorator belongs to should look like `def firstWork(self, x, y)` with `x` being the `int` and `y` being the `str` pieces of data that the signal transports. You also have to change the signal received by that slot too: `mySignal = pyqtSignal(int, str)`. – rbaleksandar Feb 21 '16 at 16:40
  • @three_pineapples don't forget to connect `QThread.finished` signal to `self.worker.deleteLater` slot. – rbaleksandar Feb 21 '16 at 16:42
  • 1
    @rbaleksandar There isn't much point making that connection unless your thread is actually finishing (which requires you explicitly call `QThread.quit()` to end the event loop in this example) – three_pineapples Feb 22 '16 at 00:13
  • 1
    @Maecky You can absolutely have multiple worker threads. Just create them in the same way as I've demonstrated, and connect the signals/slots as you wish. You can have as many `QThread` instances and `QObject` subclasses as you like, in any combination, with connections between all of them. Just be careful as making 3+ event loops consistent and interoperable can be challenging if you don't plan it out well. – three_pineapples Feb 22 '16 at 00:15
  • 2
    @three_pineapples Not sure how it's in PyQt and if internally it gets take care of but in Qt C++ when you don't clean up your objects and also invoke quitting the thread you get a warning once you stop your application that a thread was destroyed prematurely. I'm also not sure whether moving a dynamically allocated `QObject` (even though it's PyQt underneath we have C++) to a `QThread` also makes the `QThread` parent to that object. If that's not the case we get a memory leak (again - underlying C++ talking here). – rbaleksandar Feb 22 '16 at 00:28