7

I know how to send signals from worker threads back to the main GUI thread, but how can I send signals from the main thread to the worker thread?

Here's some sample code which includes a signal and slot. Here I'm sending signals back to the main thread, but how can I go in the opposite direction?

The goal here being to send a signal to change the value of self.do to 0 when I want the thread to stop.

Here's the main file and I'll put the UI file below

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
from progressUI import Ui_Form

import sys
import time

class ProgressBar(QObject):
    progress = pyqtSignal(int)
    kill = pyqtSignal()

    def __init__(self, timer, parent=None):
        super(ProgressBar, self).__init__(parent)
        self.time = timer
        self.do = 1

    def work(self):

        while self.do == 1:
            y = 0
            for _ in range(100):
                time.sleep(.1)
                y += 1
                self.progress.emit(y)
            break

        self.kill.emit()

    @pyqtSlot(str)
    def worker_slot(self, sentence):
        print(sentence)

class Go(QMainWindow, Ui_Form, QObject):
    custom_signal = pyqtSignal(str)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.progressBar.setValue(0)
        self.startThread()

    @pyqtSlot(int)
    def updateProgress(self, val):
        self.progressBar.setValue(val)
        self.custom_signal.emit('hi from main thread')

    def startThread(self):
        self.progressThread = ProgressBar(60)
        self.thread = QThread()
        self.progressThread.moveToThread(self.thread)

        self.progressThread.progress.connect(self.updateProgress)
        self.progressThread.kill.connect(self.thread.quit)
        self.custom_signal.connect(self.progressThread.worker_slot)
        self.thread.started.connect(self.progressThread.work)

        self.thread.start()



if __name__ == '__main__':
     app = QtWidgets.QApplication(sys.argv)
     MainApp = Go()
     MainApp.show()
     sys.exit(app.exec_())

Here's the UI file.

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(658, 118)
        self.progressBar = QtWidgets.QProgressBar(Form)
        self.progressBar.setGeometry(QtCore.QRect(30, 40, 601, 23))
        self.progressBar.setProperty("value", 24)
        self.progressBar.setObjectName("progressBar")
        self.label = QtWidgets.QLabel(Form)
        self.label.setGeometry(QtCore.QRect(45, 75, 581, 26))
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setObjectName("label")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "TextLabel"))
squirtgun
  • 629
  • 9
  • 12
  • Your code is not runnable. There are typos and we do not have access to your UI. – ImportanceOfBeingErnest Dec 07 '16 at 19:55
  • No it's not running code. It's a sample for you to look at. – squirtgun Dec 07 '16 at 20:13
  • 1
    Ok, so I look at it and I see... that its not working. Anyways, what is preventing you from sending a signal to the worker? – ImportanceOfBeingErnest Dec 07 '16 at 20:19
  • The lack of knowledge. I've had trouble finding any examples. If you could give a quick example, maybe with my nonworking code that I could look at, I would really appreciate it. – squirtgun Dec 07 '16 at 20:21
  • I won't make my hands dirty on some code that I cannot test. But what you'd need to do is the same you already did for connecting from the worker, just in the opposite direction: Create a signal in `Go`, create a slot method in `ProgressBar` and connect the signal to the slot. Btw. as it is now you're not running the `ProgressBar` in the `QThread`, so this signal/slot thing does not make much sense. – ImportanceOfBeingErnest Dec 07 '16 at 20:33
  • 2
    See http://stackoverflow.com/a/35534047/1994235 but note that you can't have a function in the worker that loops forever as this will block the worker from processing signals. Replace the loop with something else (like a `QTimer`, but make sure it exists in the correct thread - there are some subtleties there) – three_pineapples Dec 07 '16 at 20:39
  • three_pineapples, thanks for the link. I don't think it's what I'm looking for however. I'm looking to emit signals from the main thread to the worker thread. And I didn't know about the loop blocking. Thanks for that. I appreciate it... @ImportanceOfBeingErnest -- So I just do the opposite of the normal signals/slots... gee thanks. I don't think you can answer the question... – squirtgun Dec 07 '16 at 21:01
  • You should anyways look at the link that @three_pineapples posted, since it shows you how to use threading correctly. As I said, at the moment, your worker **is not** living in the thread. – ImportanceOfBeingErnest Dec 07 '16 at 21:36
  • 1
    @squirtgun I'm not sure why the link is not what you are looking for. The signal in the main thread is the `clicked` signal that comes with a `QPushButton`. Feel free to use any pre-existing signal that exists in the main thread, or define a new one inside a `QObject` that lives in the main thread. The slot is a method in the worker function. Thus the signal is emitted from the main thread and calls a slot in the worker function. Is this not what you are looking for? The example shows bi-directional communication between main thread and worker. Not sure what more you could want? – three_pineapples Dec 08 '16 at 05:31
  • "So I just do the opposite of the normal signals/slots" I would more describe it as exactly doing the same. Signals and slots work between threads as long as all the threads are processing their event queues from time to time. It doesn't matter which one is the GUI thread and which one is the worker thread. – NoDataDumpNoContribution Dec 08 '16 at 08:11
  • @three_pineapples Bi-directional communication is what I'm looking for. And that's a great example! And your last explanation helps clarify. I guess what I am looking for is an example of how to define a new signal inside a QObject in the main thread, like you said, and then use it. In my head I am imagining using emit() to send arguments to a slot in the worker thread. Do you have an example of that? I appreciate your patience. For some reason it's not quite clicking in my head I guess. – squirtgun Dec 08 '16 at 08:49
  • Ok I got it. I appreciate all the help. :) I'll post a working code sample in a bit answering what I was attempting to ask. – squirtgun Dec 08 '16 at 09:24

2 Answers2

1

How do you send a signal to a worker thread? Exactly the same way as sending a signal from a worker thread to the GUI. I expected it to be more different.

@three-pineapples linked to an excellent example of bi-directional communication between the main thread and a worker thread.

If you want to create a custom signal in the main GUI thread, you need to make sure you inherit QObject and then you'll be able to create custom signals.

I updated my code in the original post to include the UI file so you can run it, and I included an example of a custom signal in the GUI thread that sends a signal to the worker.

However you will not see the output of the print statement until the for loop has finished as it blocks the worker from processing signals, as @three-pineapples also stated.

So, although it's not the best example, hopefully if someone is having the same trouble understanding the concept, maybe this will help.

squirtgun
  • 629
  • 9
  • 12
1

Hi encountered the same proble on another type of project, see PyQt5 unable to stop/kill QThread,

found very informative solution/explanation here : Stopping an infinite loop in a worker thread in PyQt5 the simplest way and tried to use the second proposed solution : Solution 2: Passing a mutable as a control variable, choose this one because my project doesnt have an infinite loop, but a for loop running until a directory is emptied of all is subolders/files.

To try to understand how it works see my result, applied to code above:

UI file progressUI_2.py :

from PyQt5 import QtCore, QtGui, QtWidgets

class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(658, 218)
        self.progressBar = QtWidgets.QProgressBar(Form)
        self.progressBar.setGeometry(QtCore.QRect(30, 40, 581, 23))
        self.progressBar.setProperty("value", 24)
        self.progressBar.setObjectName("progressBar")
        self.label = QtWidgets.QLabel(Form)
        self.label.setGeometry(QtCore.QRect(45, 75, 581, 26))
        self.label.setAlignment(QtCore.Qt.AlignCenter)
        self.label.setObjectName("label")
        
        self.button = QtWidgets.QPushButton('press here', Form)
        self.button.setGeometry(QtCore.QRect(30, 115, 581, 26))
        
        self.button.setObjectName("button")

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.label.setText(_translate("Form", "TextLabel"))

Main script file main.py :

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import QMainWindow
from PyQt5.QtCore import QThread, QObject, pyqtSignal, pyqtSlot
from progressUI_2 import Ui_Form

import sys
import time

class ProgressBar(QObject):
    progress = pyqtSignal(int)
    kill = pyqtSignal()

    def __init__(self, timer, ctrl, parent=None):
        super(ProgressBar, self).__init__(parent)
        self.time = timer
        self.do = 1
        
        self.flag = 'off'
        
        self.ctrl = ctrl # dict with your control var
        


    def work(self):
        
        print('Entered run in worker thread')
        print('id of ctrl in worker:', id(self.ctrl))
        self.ctrl['break'] = False

        while self.do == 1:
            y = 0
            for _ in range(100):
                time.sleep(.1)
                y += 1
                self.progress.emit(y)
                
                if self.flag == 'on':
                    break
                # print('flag : ', self.flag, 'self.do : ', self.do)
                
                if self.ctrl['break'] == True :
                    break
                
            break

        # self.kill.emit()
        
    @pyqtSlot(str)
    def worker_slot2(self, sentence2):
        self.flag = 'on'
        self.do = 0
        print('from worker !!!!', sentence2, 'self.flag : ', self.flag,'self.do : ', self.do)   
        

    @pyqtSlot(str)
    def worker_slot(self, sentence):
        print(sentence)

class Go(QMainWindow, Ui_Form, QObject):
    custom_signal = pyqtSignal(str)
    button_signal = pyqtSignal(str)

    def __init__(self, parent=None):
        QMainWindow.__init__(self, parent)
        self.setupUi(self)
        self.progressBar.setValue(0)
        
        
        
        self.button.clicked.connect(self.clicked)
        
        self.ctrl = {'break': False} # dict with your control variable
        print('id of ctrl in main:', id(self.ctrl))
        
        self.startThread()
        
    def clicked(self):
        print('clicked')
        self.button_signal.emit('emitted from go ')
        self.flag = 'on'
        self.do = 0
        # self.ctrl['break'] = True

    @pyqtSlot(int)
    def updateProgress(self, val):
        self.progressBar.setValue(val)
        self.custom_signal.emit('hi from main thread')

    def startThread(self):
        self.progressThread = ProgressBar(60, self.ctrl)
        self.thread = QThread()
        self.progressThread.moveToThread(self.thread)

        self.progressThread.progress.connect(self.updateProgress)
        self.progressThread.kill.connect(self.thread.quit)
        self.custom_signal.connect(self.progressThread.worker_slot)
        
        self.thread.started.connect(self.progressThread.work)
        
        self.button_signal.connect(self.progressThread.worker_slot2)
        
        
        self.thread.start()



if __name__ == '__main__':
     app = QtWidgets.QApplication(sys.argv)
     MainApp = Go()
     MainApp.show()
     sys.exit(app.exec_())

try to run the main.py

commenting out and uncommenting self.ctrl['break'] = True

in :

 def clicked(self):
        print('clicked')
        self.button_signal.emit('emitted from go ')
        self.flag = 'on'
        self.do = 0
        # self.ctrl['break'] = True

and see if you are able to stop the progress bar pushing the QBButton,

notice how trying to change self.do and self.flag in various part of the code prove to be unsuccessful, not sure if it just because I should have passed them to the worker

pippo1980
  • 2,181
  • 3
  • 14
  • 30