Search code examples
python-3.xpyqt5python-multithreading

Launch a QMessageBox when a thread is finished in Python


I am using Python 3.5.2 with PyQt 5.5.1. When a button is pushed a function is started as a thread, this function contains a while loop which can be terminated either from a specific event inside the thread or by pushing the button again. If the loop is terminated from inside, I want to launch a QMessageBox explaining the reason.

MWE:

from PyQt5 import QtCore, QtGui, QtWidgets, uic
import sys, threading, random

qtCreatorMain = "CloseThread.ui"
Ui_MainWindow, QtBaseClass = uic.loadUiType(qtCreatorMain)

myKillReason = 20

class OperatorGUI(QtWidgets.QMainWindow, Ui_MainWindow):
    def __init__(self, parent=None):
        super(OperatorGUI, self).__init__(parent)
        self.setupUi(self)

        self.running = False
        self.kill_reason = None
        self.lock = threading.Lock()

        self.runBtn.clicked.connect(self.run_pushed)

    def run_pushed(self):
        self.running = not self.running
        if self.running:
            self.thread_1 = threading.Thread(target=self.myThread).start()
        else:
            self.label.setText("Not Running")

    def myThread(self):
        self.kill_reason = None
        self.label.setText("Running")
        while self.running:
            self.lock.acquire()
            num = random.random()

            if num > 0.99999999:
                self.kill_reason = myKillReason
                self.running = False

            self.lock.release()

        if self.kill_reason == myKillReason:
            self.label.setText("Not Running")
            QtWidgets.QMessageBox.information(None, "Error", "Random value above limit",
                                              QtWidgets.QMessageBox.Ok)


if __name__ == "__main__":
    sys.settrace
    app = QtWidgets.QApplication(sys.argv)
    window = OperatorGUI()
    window.show()
    sys.exit(app.exec_())

.ui file

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Form</class>
 <widget class="QWidget" name="Form">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>400</width>
    <height>154</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Form</string>
  </property>
  <widget class="QPushButton" name="runBtn">
   <property name="geometry">
    <rect>
     <x>150</x>
     <y>110</y>
     <width>85</width>
     <height>27</height>
    </rect>
   </property>
   <property name="text">
    <string>Run</string>
   </property>
  </widget>
  <widget class="QLabel" name="label">
   <property name="geometry">
    <rect>
     <x>10</x>
     <y>20</y>
     <width>381</width>
     <height>81</height>
    </rect>
   </property>
   <property name="font">
    <font>
     <pointsize>24</pointsize>
    </font>
   </property>
   <property name="text">
    <string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p align=&quot;center&quot;&gt;&lt;span style=&quot; font-size:24pt;&quot;&gt;Not running&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
   </property>
   <property name="alignment">
    <set>Qt::AlignCenter</set>
   </property>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>

Doing this I get an error stating: QApplication: Object event filter cannot be in a different thread.

Is there a way to have the thread send a SIGNAL when it closes? If so I know how to use that to launch the QMessageBox. If not, is there another way to achieve the same thing?


Solution

  • In Qt you can only update the GUI from the main thread, if you want to update any element of the GUI before an event that occurs in another thread you must do it using signals.

    class OperatorGUI(QtWidgets.QMainWindow, Ui_MainWindow):
        finished = QtCore.pyqtSignal()
    
        def __init__(self, parent=None):
            super(OperatorGUI, self).__init__(parent)
            self.setupUi(self)
    
            self.running = False
            self.kill_reason = None
            self.lock = threading.Lock()
    
            self.runBtn.clicked.connect(self.run_pushed)
            self.finished.connect(self.on_finished, QtCore.Qt.QueuedConnection)
    
        def run_pushed(self):
            self.running = not self.running
            if self.running:
                self.thread_1 = threading.Thread(target=self.myThread).start()
                self.label.setText("Running")
            else:
                self.label.setText("Not Running")
    
        def on_finished(self):
            self.label.setText("Not Running")
            QtWidgets.QMessageBox.information(None, "Error", "Random value above limit",
                                                  QtWidgets.QMessageBox.Ok)
        def myThread(self):
            self.kill_reason = None
            while self.running:
                self.lock.acquire()
                num = random.random()
                if num > 0.99:
                    self.kill_reason = myKillReason
                    self.running = False
                self.lock.release()
    
            if self.kill_reason == myKillReason:
                self.finished.emit()