PyQt4 Wait in thread for user input from GUI
Asked Answered
S

1

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?

Strapless answered 20/2, 2016 at 18:41 Comment(0)
F
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_()
Fredel answered 21/2, 2016 at 8:40 Comment(6)
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?Strapless
Change the slot decorator to @pyqtSlot( <arguments>) followed by the same signature of the function that represents the slot def firstwork(self, <argument variables>). 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).Playtime
@Fredel don't forget to connect QThread.finished signal to self.worker.deleteLater slot.Playtime
@Playtime 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)Fredel
@Strapless 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.Fredel
@Fredel 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).Playtime

© 2022 - 2024 — McMap. All rights reserved.