PyQt5 unable to stop/kill/exit from QThread
Asked Answered
C

1

1

Borrowing code from : Progress Bar Does not Render Until Job is Complete , I tried to to find way to quit/kill a Qthread while it is working, here my code, you can quit the main window while progress bar is working stopping files to be copied:

import os
import sys
import shutil
from PyQt5 import QtCore, QtWidgets


class myProgressDialog(QtWidgets.QProgressDialog):
    def __init__(self, parent=None):
        super(myProgressDialog, self).__init__(parent=parent)

    def closeEvent(self, event):
       """Get the name of active window about to close
       """
       print('cant close')
       
       event.ignore()


class MainWindow(QtWidgets.QMainWindow):
    startMoveFilesSignal = QtCore.pyqtSignal(str, str)

    def __init__(self):
        super(MainWindow, self).__init__()
        # srcdir = "/media/zachlab/Windows/LinuxStorage/old/embryos"
        # dstdir = "/media/zachlab/Windows/LinuxStorage/old/out"
        
        srcdir = "in"
        dstdir = "out"
        
        self.le_src = QtWidgets.QLineEdit(srcdir)
        self.le_dst = QtWidgets.QLineEdit(dstdir)
        self.button = QtWidgets.QPushButton("Copy")
        # self.button.clicked.connect(self.archiveEntry)
        
        self.button.clicked.connect(self.archiveEntry2)

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        lay = QtWidgets.QFormLayout(central_widget)
        lay.addRow("From: ", self.le_src)
        lay.addRow("To: ", self.le_dst)
        lay.addRow(self.button)
        
        print('self,thread :', self.thread)


    def archiveEntry2(self):
        
        print('connected')
        self.progressbar = myProgressDialog(self)
        
        
        # RIMUOVO Cancel Button
        self.progressbar.setCancelButton(None)
        
        self.progressbar.hide()

        self.thread = QtCore.QThread(self)
        self.thread.start()
        self.helper = MoveFileHelper()
        self.startMoveFilesSignal.connect(self.helper.moveFilesWithProgress)
        self.helper.progressChanged.connect(self.progressbar.setValue)
        self.helper.finished.connect(self.on_finished)
        self.helper.started.connect(self.progressbar.show)
        self.helper.errorOccurred.connect(self.on_errorOcurred)
        self.helper.moveToThread(self.thread)
        
        self.archiveEntry()
    
    
    
    ## Questo funziona
    def closeEvent(self, event):
        """Get the name of active window about to close
        """
        print('killing thread')
       
        try:
            if self.thread.isRunning():
               
               
                print('killing running thread', self.thread.isRunning())
               
                # self.thread.terminate()  ## ---------> error Qt has caught an exception thrown from an event handler.
               
                self.thread.quit()  ###  funziona ma non in SPYDER
                

        except Exception as Exceptionz:
            print('Exception :', Exceptionz)
            
            
        try: 
            print('killing running thread after quit :', self.thread.isRunning())
                    
        except:
            print('quitted')
        event.accept()
    
    @QtCore.pyqtSlot()
    def archiveEntry(self):
        self.startMoveFilesSignal.emit(self.le_src.text(), self.le_dst.text())
        self.progressbar.hide()

    @QtCore.pyqtSlot()
    def on_finished(self):
        self.button.setText('Finished')

    @QtCore.pyqtSlot(str)
    def on_errorOcurred(self, msg):
        QtWidgets.QMessageBox.critical(self, "Error Ocurred", msg)
        


class MoveFileHelper(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()
    errorOccurred = QtCore.pyqtSignal(str)

    def calculateAndUpdate(self, done, total):
        progress = int(round((done / float(total)) * 100))
        self.progressChanged.emit(progress)

    @staticmethod
    def countFiles(directory):
        count = 0
        if os.path.isdir(directory):
            for path, dirs, filenames in os.walk(directory):
                count += len(filenames)
        return count

    @staticmethod
    def makedirs(dest):
        if not os.path.exists(dest):
            os.makedirs(dest)

    @QtCore.pyqtSlot(str, str)
    def moveFilesWithProgress(self, src, dest):
        numFiles = MoveFileHelper.countFiles(src)
        # if os.path.exists(dest):
        #     self.errorOccurred.emit("Dest exist")
        #     return 
        if numFiles > 0:
            self.started.emit()
            MoveFileHelper.makedirs(dest)
            numCopied = 0
            for path, dirs, filenames in os.walk(src):
                for directory in dirs:
                    destDir = path.replace(src, dest)
                    MoveFileHelper.makedirs(os.path.join(destDir, directory))

                for sfile in filenames:
                    srcFile = os.path.join(path, sfile)
                    destFile = os.path.join(path.replace(src, dest), sfile)
                    shutil.copy(srcFile, destFile)
                    numCopied += 1
                    self.calculateAndUpdate(numCopied, numFiles)
                    for i in range(100000):
                        i = i*10
           
            self.finished.emit()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ex = MainWindow()
    ex.resize(640, ex.sizeHint().height())
    ex.show()
    sys.exit(app.exec_())

Not sure if it is the right way to kill a Qthread but seems to work.

Even if after stopping the Qthread: self.thread.quit() I stop the copying of files

but the self.thread.isRunning() still returns True.

When trying to split the code and add another window using:

main.py
import os
import sys
import shutil
from PyQt5 import QtCore, QtWidgets

from mod007b_import import Windowz, MoveFileHelper, myProgressDialog

class MainWindow(QtWidgets.QMainWindow):
    
    def __init__(self):
        super(MainWindow, self).__init__()
        QtWidgets.QMainWindow.__init__(self)
        self.layout = QtWidgets.QHBoxLayout()
        self.lineEdit = QtWidgets.QLineEdit()
        self.lineEdit.setText("Just to fill up the dialog")
        self.layout.addWidget(self.lineEdit)
        self.button = QtWidgets.QPushButton('pppppp')
        self.layout.addWidget(self.button)
    
        self.widget = QtWidgets.QWidget()
        self.widget.setLayout(self.layout)
    
        self.setCentralWidget(self.widget)
        self.setWindowTitle('Simple')

        
        self.button.clicked.connect(self.newWindow)
        
        self.listz = []
         
    def newWindow(self):
        
        print('newwindow')
        
        self.pippo = Windowz()   ########## RIVEDERE PARENT CHILD RELATIONSHIP
        
        self.pippo.show()
        
        # self.listz.append(self.pippo)
        
        pw = self.pippo.parentWidget()
        
        print('list : ', self.listz)
        
        print(pw)
        if pw is not None:
            print('self :', self)
            print('pw : ', pw, pw.layout)
            print('pippo :', self.pippo)
        

        # print(' central_widget :', central_widget, type( central_widget))

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ex = MainWindow()
    # ex.setWindowTitle('Simple**************')
    ex.resize(640, ex.sizeHint().height())
    ex.show()
    sys.exit(app.exec_())

and

mod007b_import.py
import os
import sys
import shutil
from PyQt5 import QtCore, QtWidgets


class myProgressDialog(QtWidgets.QProgressDialog):
    def __init__(self, parent=None):
        super(myProgressDialog, self).__init__(parent=parent)

    def closeEvent(self, event):
       """Get the name of active window about to close
       """
       print('cant close')
       
       event.ignore()


class Windowz(QtWidgets.QWidget):
# class Windowz(QtWidgets.QMainWindow):
    startMoveFilesSignal = QtCore.pyqtSignal(str, str)

    # def __init__(self,parent=None):
    #     # super(Windowz, self).__init__(parent=parent)
    #     super(Windowz, self).__init__(parent=parent)
    def __init__(self):
        # super(Windowz, self).__init__(parent=parent)
        super().__init__()
        # srcdir = "/media/zachlab/Windows/LinuxStorage/old/embryos"
        # dstdir = "/media/zachlab/Windows/LinuxStorage/old/out"
        
        srcdir = "in"
        dstdir = "out"
        
        self.le_src = QtWidgets.QLineEdit(srcdir)
        self.le_dst = QtWidgets.QLineEdit(dstdir)
        self.button = QtWidgets.QPushButton("Copy")
        # self.button.clicked.connect(self.archiveEntry)
        
        self.button.clicked.connect(self.archiveEntry2)

        
        ### spostati in Main
        # central_widget2 = QtWidgets.QWidget()
        # self.setCentralWidget(central_widget2)
        # lay = QtWidgets.QFormLayout(central_widget2)
        self.lay = QtWidgets.QFormLayout(self)
        self.lay.addRow("From: ", self.le_src)
        self.lay.addRow("To: ", self.le_dst)
        self.lay.addRow(self.button)
        
        

        print('self,thread :', self.thread)
        
        # self.show()

        

    def archiveEntry2(self):
        
        print('connected')
        self.progressbar = myProgressDialog(self)
        
        
        # RIMUOVO Cancel Button
        self.progressbar.setCancelButton(None)
        
        self.progressbar.hide()

        self.thread = QtCore.QThread(self)
        self.thread.start()
        self.helper = MoveFileHelper()
        self.startMoveFilesSignal.connect(self.helper.moveFilesWithProgress)
        self.helper.progressChanged.connect(self.progressbar.setValue)
        self.helper.finished.connect(self.on_finished)
        self.helper.started.connect(self.progressbar.show)
        self.helper.errorOccurred.connect(self.on_errorOcurred)
        self.helper.moveToThread(self.thread)
        
        self.archiveEntry()
    
    
    ## Questo funziona
    def closeEvent(self, event):
        """Get the name of active window about to close
        """
        print('killing thread')
       
        try:
            if self.thread.isRunning():
               
               
                print('killing running thread', self.thread.isRunning())
               
                # self.thread.terminate()  ## ---------> error Qt has caught an exception thrown from an event handler.
               
                self.thread.quit()  ###  doesnt work
                
                # self.progressbar.hide() ### hides the bar 
                
                # self.progressbar.close() ### doesnt work
                
                try: 
                    print('killing running thread after quit :', self.thread.isRunning())
                
                except:
                    print('quitted')
               
        except Exception as Exceptionz:
            print('Exception :', Exceptionz)
        event.accept()
    
    @QtCore.pyqtSlot()
    def archiveEntry(self):
        self.startMoveFilesSignal.emit(self.le_src.text(), self.le_dst.text())
        self.progressbar.hide()

    @QtCore.pyqtSlot()
    def on_finished(self):
        self.button.setText('Finished')

    @QtCore.pyqtSlot(str)
    def on_errorOcurred(self, msg):
        QtWidgets.QMessageBox.critical(self, "Error Ocurred", msg)
        


class MoveFileHelper(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()
    errorOccurred = QtCore.pyqtSignal(str)

    def calculateAndUpdate(self, done, total):
        progress = int(round((done / float(total)) * 100))
        self.progressChanged.emit(progress)

    @staticmethod
    def countFiles(directory):
        count = 0
        if os.path.isdir(directory):
            for path, dirs, filenames in os.walk(directory):
                count += len(filenames)
        return count

    @staticmethod
    def makedirs(dest):
        if not os.path.exists(dest):
            os.makedirs(dest)

    @QtCore.pyqtSlot(str, str)
    def moveFilesWithProgress(self, src, dest):
        numFiles = MoveFileHelper.countFiles(src)
        # if os.path.exists(dest):
        #     self.errorOccurred.emit("Dest exist")
        #     return 
        if numFiles > 0:
            self.started.emit()
            MoveFileHelper.makedirs(dest)
            numCopied = 0
            for path, dirs, filenames in os.walk(src):
                for directory in dirs:
                    destDir = path.replace(src, dest)
                    MoveFileHelper.makedirs(os.path.join(destDir, directory))

                for sfile in filenames:
                    srcFile = os.path.join(path, sfile)
                    destFile = os.path.join(path.replace(src, dest), sfile)
                    shutil.copy(srcFile, destFile)
                    numCopied += 1
                    self.calculateAndUpdate(numCopied, numFiles)
                    for i in range(100000):
                        i = i*10
           
            self.finished.emit()

I get a first window, pressing the 'pppppp' button it goes to a second one that is the same as the single file script above: press 'copy' button to start the copying/Qthread, but when I close this window even if the QThread seems to be stopped, progress bar doesnt disappear, I can hide the progress bar but cant close it and in any case the copying process reach completion.

Any idea what is going on ?

PS

in order to have the script working and a having a visible progress bar files need to be in a directory toghether with a 'in' folder with enough files to have a slow process.

Chyack answered 10/3, 2022 at 10:36 Comment(10)
Calling a thread's quit doesn't automatically stop it, nor its running function. You need to set a flag on the worker and check it periodically (for instance, at every iteration of the for loops) and eventually break or return, and connect the button to a function that will set that flag.Wyndham
Why does it seem to work with the first script ???Chyack
I've not tested that code, but I suppose that it "works" just because you're quitting the program: you're not stopping the thread, you're killing it. A proper thread closure calls quit() and wait() (which blocks until the thread actually stops).Wyndham
@Wyndham according to this #41026532 if I am not wrong it wont be possible to link the close eveny of my window to any signal that could interrupt a loop. I mean if printing doesnt work would setting a variable from False to True work ? Maybe I am missing somethingChyack
What do you mean by "if printing doesn't work"? The print statement correctly returns True to isRunning() because setting the flag will not instantly stop the thread: the function needs to return first, meaning that you have to wait for the thread to get control and let the function evaluate the flag and finally return control to the main thread by actually quitting the thread. And, in any case, I don't see any reference to what you're saying in that post: if you're not overriding the QThread's run() (thus ignoring its event loop), a signal will be processed as expected.Wyndham
Setting the flag for the worker means that it will evaluate it as soon as its thread "allows it", then the function will eventually exit, and the thread manager (QThread) will be able to properly quit the thread. After that, wait() is mandatory (otherwise it would be similar to terminate() - which is also discouraged - if you're going to quit before waiting the correct exit of the thread). In any case, remember that QThread (similarly to python Thread) is an interface to the system thread: it causes it's run() (and whatever is called from it) to be executed in a separate thread.Wyndham
Sorry @Wyndham I was talking about the print in the in question 4102603 where in a comment they say: 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)Chyack
And was wondering if the flag change would eventually be seen from inside a loopChyack
In the accepredcanswer of 4102603 is stated : 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.Chyack
That's exactly my point: if you have a while loop (that never releases control from the thread), the QThread is not able to process its event loop, including any signal. The flag change, instead, can be "seen" from the while loop, as soon as the thread is active at that moment and reaches the point in which the flag is evaluated. But it can only be "seen" from the internal function and only from there until that function returns, meanwhile it's blocking. When the function finally returns, the QThread can resume its event loop and finally evaluate the flag on its own, if required.Wyndham
C
0

OK thanks to @musicamante and to Stopping an infinite loop in a worker thread in PyQt5 the simplest way

I figured out what was wrong in the first of my two codes, here the first one re-written with a flag set to terminate the copying loop when main window is closed, important

commenting out or not :

# self.thread.quit()
                    
# self.thread.wait()

in Mainwindow def closeEvent(self, event): just after the setting of the flag to True (self.ctrl['break'] = True) will end up having the QThread running / not running before the script terminates anyway. For the flag see the Solution 2: Passing a mutable as a control variable in Stopping an infinite loop in a worker thread in PyQt5 the simplest way

import os
import sys
import shutil
from PyQt5 import QtCore, QtWidgets


class myProgressDialog(QtWidgets.QProgressDialog):
    def __init__(self, parent=None):
        super(myProgressDialog, self).__init__(parent=parent)
       

    def closeEvent(self, event):
       """Get the name of active window about to close
       """
       print('cant close')
       
       event.ignore()


class MainWindow(QtWidgets.QMainWindow):
    startMoveFilesSignal = QtCore.pyqtSignal(str, str)


    def __init__(self):
        super(MainWindow, self).__init__()
        # srcdir = "/media/zachlab/Windows/LinuxStorage/old/embryos"
        # dstdir = "/media/zachlab/Windows/LinuxStorage/old/out"
        
        srcdir = "in"
        dstdir = "out"
        
        self.le_src = QtWidgets.QLineEdit(srcdir)
        self.le_dst = QtWidgets.QLineEdit(dstdir)
        self.button = QtWidgets.QPushButton("Copy")
        # self.button.clicked.connect(self.archiveEntry)
        
        self.button.clicked.connect(self.archiveEntry2)

        central_widget = QtWidgets.QWidget()
        self.setCentralWidget(central_widget)
        lay = QtWidgets.QFormLayout(central_widget)
        lay.addRow("From: ", self.le_src)
        lay.addRow("To: ", self.le_dst)
        lay.addRow(self.button)
        
        
        self.ctrl = {'break': False} # dict with your control variable
        print('id of ctrl in MainWindow:', id(self.ctrl))


    def archiveEntry2(self):
        
        print('connected')
        self.progressbar = myProgressDialog(self)
        self.progressbar.setWindowTitle('coopying files')
        
        
        # RIMUOVO Cancel Button
        self.progressbar.setCancelButton(None)
        
        self.progressbar.hide()

        self.thread = QtCore.QThread(self)
        self.thread.start()
        self.helper = MoveFileHelper(self.ctrl)
        self.startMoveFilesSignal.connect(self.helper.moveFilesWithProgress)
        
        
        self.helper.progressChanged.connect(self.progressbar.setValue)
        self.helper.finished.connect(self.on_finished)
        self.helper.started.connect(self.progressbar.show)
        self.helper.errorOccurred.connect(self.on_errorOcurred)
        self.helper.moveToThread(self.thread)
        
        self.archiveEntry()
    
    
    
    ## Questo funziona
    def closeEvent(self, event):
            """Get the name of active window about to close
            """
        
            try:
            
                print('killing thread')
                
                print('self.thread.isRunning() before quit :', self.thread.isRunning())
                
                if self.thread.isRunning():
            
                    
                    print("quitted   ----> self.ctrl['break'] = True")
                    
                    self.ctrl['break'] = True
                    
                    self.thread.quit()
                    
                    self.thread.wait()
                    
            
            except Exception as Exceptionz:
                print('Exception :', Exceptionz)
            
        
       
    
            try: 
                print('self.thread.isRunning() after quit :', self.thread.isRunning())
                    
            except:
                print('quitted')
            
            
            # event.accept() # not needed implicit
        
            # event.ignore()
        

    
    
    @QtCore.pyqtSlot()
    def archiveEntry(self):
        self.startMoveFilesSignal.emit(self.le_src.text(), self.le_dst.text())
        self.progressbar.hide()

    @QtCore.pyqtSlot()
    def on_finished(self):
        print('on_finished self.ctrl inside worker : ', self.ctrl)
        print('self.thread.isRunning() after quit on_finished :', self.thread.isRunning())
       
        
 
        
        self.button.setText('Finished')

    @QtCore.pyqtSlot(str)
    def on_errorOcurred(self, msg):
        QtWidgets.QMessageBox.critical(self, "Error Ocurred", msg)
        


class MoveFileHelper(QtCore.QObject):
    progressChanged = QtCore.pyqtSignal(int)
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()
    errorOccurred = QtCore.pyqtSignal(str)
    
    def __init__(self, ctrl):
       
        super().__init__()
    
        self.ctrl = ctrl # dict with your control var
        
        print('self.ctrl inside worker MoveFileHelper : ', self.ctrl)
        
        print('Entered run in worker thread')
        print('id of ctrl in worker:', id(self.ctrl))
        self.ctrl['break'] = False
    
    def calculateAndUpdate(self, done, total):
        progress = int(round((done / float(total)) * 100))
        self.progressChanged.emit(progress)

    @staticmethod
    def countFiles(directory):
        count = 0
        if os.path.isdir(directory):
            for path, dirs, filenames in os.walk(directory):
                count += len(filenames)
        return count

    @staticmethod
    def makedirs(dest):
        if not os.path.exists(dest):
            os.makedirs(dest)
            



    @QtCore.pyqtSlot(str, str)
    def moveFilesWithProgress(self, src, dest):
        numFiles = MoveFileHelper.countFiles(src)
        # if os.path.exists(dest):
        #     self.errorOccurred.emit("Dest exist")
        #     return 
        if numFiles > 0:
            self.started.emit()
            MoveFileHelper.makedirs(dest)
            numCopied = 0
            for path, dirs, filenames in os.walk(src):
                

                    for directory in dirs:
                        destDir = path.replace(src, dest)
                        MoveFileHelper.makedirs(os.path.join(destDir, directory))
    
                    for sfile in filenames:
                        

                        if self.ctrl['break'] :   # == True : is implicit
                            
                            self.finished.emit()
                            return
                        
                        else:
                            
                            srcFile = os.path.join(path, sfile)
                            destFile = os.path.join(path.replace(src, dest), sfile)
                            shutil.copy(srcFile, destFile)
                            numCopied += 1
                            self.calculateAndUpdate(numCopied, numFiles)
                            for i in range(100000):
                                i = i*10

           
            self.finished.emit()

if __name__ == "__main__":
    app = QtWidgets.QApplication(sys.argv)
    ex = MainWindow()
    ex.resize(640, ex.sizeHint().height())
    ex.show()
    sys.exit(app.exec_())
Chyack answered 13/3, 2022 at 23:26 Comment(2)
Note: as long as you are sure about types, there's no point in if something == True:: just do if something:. Also, a Close event is implicitly accepted, so you normally only need to do event.ignore().Wyndham
@Wyndham , thanks indeed. Two mor question , I struggled a lot with this trying to use a simple variable as FLAG (before finding the right post that showed me to use a dict) why that doesnt work ? (I assume answer is in Python inners). Second I tried to pass the value as a signal too but that didnt work either, but that probably is just because the signal was calling a function that changed my flag and that was running after my looping function ended, Is that possible ?Chyack

© 2022 - 2024 — McMap. All rights reserved.