1

I'm currently trying to make an API call when my button is clicked without pausing my GUI for the duration of the call. I was using threads to set the text of my Qlable to the response of the API call. This worked however it was unsafe as I was accessing GUI elements from a separate thread.

Currently, I'm attempting to use QThreads to make the API call and then emit the response to the GUI thread however, when I create the Qthread object my program finishes with exit code 3. I've simplified the problem for more clarity.

class MainWindow(QtWidgets.QWidget):

    def __init__(self):
        super(MainWindow, self).__init__()
        self.setWindowTitle("TestWindow")
        self.setFixedSize(300,75)

        self.main_layout = QtWidgets.QGridLayout()
        self.setLayout(self.main_layout)
        self.txt_0 = QtWidgets.QLabel()
        self.btn_0 = QtWidgets.QPushButton('Press Me')
        self.btn_0.clicked.connect(self.btn_0_clicked)

        self.main_layout.addWidget(self.txt_0, 0, 0)
        self.main_layout.addWidget(self.btn_0, 1, 0)

        self.show()

    def btn_0_clicked(self):
        temp_thread = StringThread("name")
        temp_thread.start()


class StringThread(QtCore.QThread):

    str_signal = QtCore.pyqtSignal(str)
    _name = ''

    def __init__(self, name):
        QtCore.QThread.__init__(self)
        self._name = name
        print("Thread Created")

    def run(self):
        self.str_signal.emit('Emitted message from StringThread. Name = ' + self._name)
        print("Done run")

My intention is to set the text of my Qlable to the message emitted from the pyqtSignal in the StringThread class however, as soon as I click the button my program finishes with exit code 3.

edit:

I made the following changes to the btn_0_clicked method

def btn_0_clicked(self):
        self.temp_thread = StringThread("hello")
        self.temp_thread.str_signal.connect(self.txt_0.setText)
        self.temp_thread.start()

It's working now.

Ozymandias
  • 839
  • 1
  • 8
  • 13
  • change `temp_thread` to `self.temp_thread` – eyllanesc Jan 09 '19 at 21:54
  • emp_thread is a local variable so it will be deleted when the btn_0_clicked function is finished, so the thread will be destroyed when it does not emit the signal causing an abrupt exit, so the solution is to extend the life cycle of the variable as per example to do it attribute of the class, another method is to pass it a parent: `temp_thread = StringThread("name", self)` ... `def __init__(self, name, parent=None): super(StringThread, self).__init__(parent)` – eyllanesc Jan 09 '19 at 21:57
  • There are several methodologies to use QThread: inherit from QThread (the method you tried to use), use QObjects as workers, workers + controllers, use lambda methods, etc. The use of one or another methodology in general is arbitrary but in some cases one method can be implemented and in others it can not. It is good that you review the docs: http://doc.qt.io/qt-5/qthread.html – eyllanesc Jan 09 '19 at 22:07
  • IMHO in python I prefer to use `threading.Thread()` since the use is simple. – eyllanesc Jan 09 '19 at 22:08
  • @eyllanesc I would like to keep everything in PyQt5. Also I need pyqtSignal for cross thread communication – Ozymandias Jan 09 '19 at 22:12
  • The need for signals always exists in PyQt5, and in the transmission of data from the threads to the GUI is mandatory; review the following example: https://stackoverflow.com/questions/54041323/how-i-can-make-thread-for-progress-bar-with-pafy :-) – eyllanesc Jan 09 '19 at 22:15

1 Answers1

0

You should probably read up on the Qt C++ documentation to better understand what is happening:

  1. http://doc.qt.io/qt-5/qobject.html
  2. http://doc.qt.io/qt-5/qthread.html
  3. https://wiki.qt.io/QThreads_general_usage

In general you have two problems (one of which is worth listing twice, to examine it from different angles):

  1. Your QThread is owned/associated by/with the thread that created it for the purpose of emitting events. So all code trying to emit events is actually trying to access the event loop of your main thread (UI). Read up on QObject's thread() and moveToThread() methods to understand this more fully.
  2. You overrode the run() method of QThread which is what is invoked on the actual thread represented by QThread (i.e. the code that winds up getting called eventually once you call start() on the thread). So you are effectively trying to access the main event loop in an unsafe manner from a different thread.
  3. You overrode the run() method of QThread. The default implementation of QThread sets up a QEventLoop, runs it and that is what gives QObject's associated/owned with/by that thread the ability to use signal/slots freely. You probably did not intend to lose that default implementation.

Basically what you want to do instead is this:

  1. Do not subclass QThread. Instead write a custom QObject worker subclass with appropriate events
  2. Construct a QThread with an eventloop (the default IIRC, but check the docs)
  3. Construct an instance of your custom worker object.
  4. Move your worker object to the newly created QThread with moveToThread().
  5. Wire up the signals and slots, make sure you are using the QueuedConnection type (the default is DirectConnection which is not suitable for communicating across thread boundaries).
  6. Start your thread which should just run its own QEventLoop.
  7. Emit the signal to kickstart your worker object logic.
  8. Receive notification of the results via signals on your main thread (eventloop), where it is safe to update the UI.
  9. Figure out how you want to terminate the QThread and clean up associated resources. Typically you would fire a custom event wired up to the destroyLater() slot to get rid of your worker object. You may want to cache/reuse the QThread for subsequent button clicks or you may want to tear it down as well once your worker object has been cleaned up.
user268396
  • 11,576
  • 2
  • 31
  • 26