Search code examples
python-3.xpyqt5qthreadqprocess

Terminating QThread with multiple QProcess generated with for loop


I recently asked a question about how to prevent QThread from freezing my GUI when running multiple instances of QProcess are generated through using a nested loop. The solution provided worked like a charm! I was able to finish my program, and it runs smoothly! I recently added a feature to allow the user to stop the QThread at any point during the process. The stop feature works in that no new instances of QProcesse are called; however, there are a number of unfinished instances of QProcess still processing. Is there a way to determine when there are no longer any instances of QProcess running and notify the user? I am familiar with the QProcess method waitForFinished(), but I'm not sure how to apply that to multiple instances of QProcess generated using a nested loop.

Here is a sample of code that simulates the data processing aspect of my actual program:

# This Python file uses the following encoding: utf-8
import sys
import os
from PyQt5.QtCore import *
from PyQt5.QtGui import *
from PyQt5.QtWidgets import *
import test
import time

class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.ui=test.Ui_test()
        self.ui.setupUi(self)
        self.ui.pushButton_startThread.clicked.connect(self.startTestThread)
        self.ui.pushButton_Update.clicked.connect(self.stopThread)

    def startTestThread(self):
        self.xValue = self.ui.lineEdit_x.text() #Represents number of batches
        self.yValue = self.ui.lineEdit_y.text() #Represents number of images per batch
        self.runTest = testThread(self.xValue, self.yValue, self)  # Creates an instance of testThread
        self.runTest.start()  # Starts the instance of testThread

    def stopThread(self):
        self.runTest.stop()
        self.ui.lineEdit_x.setText("")
        self.ui.lineEdit_y.setText("")



class testThread(QThread):
    def __init__(self, xValue, yValue, parent=None):
        super().__init__(parent)
        self.xValue = xValue
        self.yValue = yValue

    def __del__(self):
        self.wait()

    def run(self):
        processd = []
        for x in range(int(self.xValue)):  # For loop to iterate througeach batch
            pass
            for y in range(
                int(self.yValue)):  # For loop to iterate through each image in each batch
                time.sleep(.1)
            QProcess.startDetached(os.path.dirname(os.path.realpath(__file__)) + r"\test.bat")  # Runs test.bat

    def stop(self):
        self.terminate()
        self.wait()
        print("\nStopping thread")


if __name__ == "__main__":
    app = QApplication(sys.argv)
    app.setStyle("Fusion")
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

test.bat

@ECHO OFF
ECHO Processing Batched Images.
TIMEOUT /T 15
ECHO Process Finished.

As was already noted, stopping the QThread works; however, I want to be able to determine when all QProcess instances are done. This will allow the program to notify the user that they can restart the process using new parameters. Currently, I'm having to tell my labmates to just wait until no more output files are being written before trying a new set of parameters.

Any help will be much appreciated. Thank you for your time!


Solution

  • While not elegant, I figured out a way to determine when multiple instances of QProcess are finished after terminating a QThread:

    1. I added a batch counter that increments by one each time an instance of QProcess is queued.
    2. Once the thread is canceled, a signal is emitted with the current batch counter.
    3. This signal is connected to a method that will determine when all instances of QProcess have finished processing.
    4. For my program, each instance of QProcess generates a unique output file.
    5. Using a while loop, the moment all instances of QProcess are finished can be determined.

    Here is a sample of the code:

    # This Python file uses the following encoding: utf-8
    import sys
    import os
    from PyQt5.QtCore import *
    from PyQt5.QtGui import *
    from PyQt5.QtWidgets import *
    import test
    import time
    from glob import glob1
    
    class MainWindow(QMainWindow):
        def __init__(self):
            super().__init__()
            self.ui=test.Ui_test()
            self.ui.setupUi(self)
            self.ui.pushButton_startThread.clicked.connect(self.startTestThread)
            self.ui.pushButton_Update.clicked.connect(self.stopThread)
    
        def startTestThread(self):
            self.xValue = self.ui.lineEdit_x.text() #Represents number of batches
            self.yValue = self.ui.lineEdit_y.text() #Represents number of images per batch
            self.runTest = testThread(self.xValue, self.yValue, self)  # Creates an instance of testThread
            self.runTest.signals.batchesSubmitted.connect(self.testThreadFinished)
            self.runTest.start()  # Starts the instance of testThread
    
        def stopThread(self):
            self.runTest.stop()
    
        @pyqtSlot(int)    
        def testThreadFinished(self, batchesSubmitted):
            while len(glob1(outputFilePath + fileName, "*.txt")) < batchesSubmitted: #Compare the number of *.txt files in the output directory to the number of isntances of QProcess called
                time.sleep(1)
            self.ui.lineEdit_x.setText("")
            self.ui.lineEdit_y.setText("")
            print("All instance of QProcess are finished!")
    
    class testThreadSignals(QObject):
        batchesSubmitted = pyqtSignal(int)
    
    class testThread(QThread):
        def __init__(self, xValue, yValue, parent=None):
            super().__init__(parent)
            self.xValue = xValue
            self.yValue = yValue
            self.signals = testThreadSignals()
    
        def run(self):
            self.batchCounter = 0
            for x in range(int(self.xValue)):  # For loop to iterate througeach batch
                pass
                for y in range(
                    int(self.yValue)):  # For loop to iterate through each image in each batch
                    time.sleep(.1)
                QProcess.startDetached(os.path.dirname(os.path.realpath(__file__)) + r"\test.bat")  # Runs test.bat
                self.batchCounter += 1 #Counts the successful number *.txt files written
    
        @pyqtSlot(int)
        def stop(self):
            self.terminate()
            self.threadactive = False
            self.wait()
            self.signals.batchesSubmitted.emit(self.batchCounter)
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        app.setStyle("Fusion")
        window = MainWindow()
        window.show()
        sys.exit(app.exec_())
    

    test.bat:

    @ECHO OFF
    ECHO Processing Batched Images.
    TIMEOUT /T 15
    REM write a unique text file
    ECHO Process Finished.
    

    I was able to adapt the above python code to my program with no problems.