Search code examples
pythonpython-3.xmultithreadingpyqt5

Child thread won't close when main GUI window is closed in pyqt5 / pythonx3


I tried this example from StackOverflow:

How to get a child thread to close when main GUI window is closed in pyqt5 / python 3?

I would like this child thread to terminate when I close the main application. In this example, the child thread is a simple counter. When I close the main GUI, the counter still keeps going. How can I get the thread to end when the GUI window is closed?

My code is here:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Created on Sat Apr 11 12:41:26 2020

@author: Pietro
"""

from PyQt5 import QtWidgets
from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                             QVBoxLayout)
from PyQt5.QtCore import QThread
import time, threading, sys





class testScriptApp(QtWidgets.QWidget):

    def __init__(self, parent=None):
        # initialize the widget
        QtWidgets.QWidget.__init__(self, parent)
        # set the window title
        self.setWindowTitle("Scripting")
        # manage the layout
        self.center()
        self.resize(400,400)
        self.mainGrid = QVBoxLayout()
        self.button = QPushButton('Start')
        self.button.clicked.connect(self.on_click)
        self.mainGrid.addWidget(self.button)
        self.setLayout(self.mainGrid)



    def center(self):
        qr = self.frameGeometry()
        cp = QtWidgets.QDesktopWidget().availableGeometry().center()
        qr.moveCenter(cp)
        self.move(qr.topLeft())

    def on_click(self):
        global running
        print('global running : ' , running)
        self.worker = Worker()
        self.worker.run()

    def closeEvent(self, event):
        global running
        running = False
        print('Closing')
#       self.worker.terminate()
        event.accept()

class Worker(QThread):

    def __init__(self):
        QThread.__init__(self)

    def run(self):
        count=1
        global running
        while count>0 and not running:
#        while count>0:
            print('counter on: ',count, running)
            time.sleep(2)
            count+=1
        else:
            print('out of loop')
            return

if __name__ == "__main__":
    running = False
    app = QtWidgets.QApplication(sys.argv)
    myapp = testScriptApp()
    myapp.show()
    app.exec_()

I started trying to learn Python3 weeks ago so please be gentle. What did I do wrong here?


Solution

  • From : QThread : run() Vs start()

    From the docs

    QThread begins executing in run(). To create your own threads, subclass QThread and reimplement run() Use the start() method to begin execution. Execution ends when you return from run(), just as an application does when it leaves main() To create your own thread you'll always need to subclass QThread and reimplement run(). To start the thread you'll need to call start().

    I hope it's clear now.

    So like alec said

    You should call

    .worker.start() instead of run().

    If you set self.worker.setTerminationEnabled(True)

    then in the closeEvent

    self.worker.terminate() will terminate the thread. –

    Changed code:

    from PyQt5 import QtWidgets
    from PyQt5.QtWidgets import (QWidget, QApplication,QPushButton, 
                                 QVBoxLayout)
    from PyQt5.QtCore import QThread
    import time, threading, sys
    
    
    
    
    
    class testScriptApp(QtWidgets.QWidget):
    
        def __init__(self, parent=None):
            # initialize the widget
            QtWidgets.QWidget.__init__(self, parent)
            # set the window title
            self.setWindowTitle("Scripting")
            # manage the layout
            self.center()
            self.resize(400,400)
            self.mainGrid = QVBoxLayout()
            self.button = QPushButton('Start')
            self.button.clicked.connect(self.on_click)
            self.mainGrid.addWidget(self.button)
            self.setLayout(self.mainGrid)
    
    
    
        def center(self):
            qr = self.frameGeometry()
            cp = QtWidgets.QDesktopWidget().availableGeometry().center()
            qr.moveCenter(cp)
            self.move(qr.topLeft())
    
        def on_click(self):
            global running
            print('global running : ' , running)
            self.worker = Worker()
            
            self.worker.setTerminationEnabled(True)
            self.worker.start()
    
        def closeEvent(self, event):
            global running
            
            # running = False ## doesnt print 'out of loop' line
            running = True ## does print 'out of loop' line
            
            # self.worker.terminate() # if not commented out doesnt print the 'out of loop' line
            
            ### self.worker Execution ends when you return from run() 
            
            time.sleep(5)
            
            print('Closing')
    
            event.accept()
    
    class Worker(QThread):
    
        def __init__(self):
            QThread.__init__(self)
    
        def run(self):
            count=1
            global running
            while count>0 and not running:
                print('counter on: ',count, running)
                time.sleep(2)
                count+=1
            else:
                print('out of loop')
                return
    
    if __name__ == "__main__":
        running = False
        app = QtWidgets.QApplication(sys.argv)
        myapp = testScriptApp()
        myapp.show()
        app.exec_()
    
    

    output:

    global running :  False
    counter on:  1 False
    counter on:  2 False
    out of loop
    Closing
    

    uncommenting self.worker.terminate() , output:

    global running :  False
    counter on:  1 False
    counter on:  2 False
    counter on:  3 False
    counter on:  4 False
    counter on:  5 False
    Closing
    
    

    Note that I added time.sleep(5) in closeEvent to allow the Qthread to print the out of loop line.

    Remember as in the link above self.worker Execution ends when you return from run() allowing terminate with self.worker.setTerminationEnabled(True) will allow to call self.worker.terminate() at any point.