Search code examples
pythonpyqtpython-3.5pyqt5qthread

Why do external functions cause the PyQt5 window to freeze?


Here is some sample code that breaks:

import sys
import time
from PyQt5.QtWidgets import (QApplication, QDialog,
                             QProgressBar)

class Actions(QDialog):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.progress = QProgressBar(self)
        self.progress.setGeometry(0, 0, 300, 25)
        self.show()

        self.count = 0

        while self.count < 100:
            self.count += 1
            time.sleep(1) # Example external function
            self.progress.setValue(self.count)

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Actions()
    sys.exit(app.exec_())

Running this will cause it to freeze and become unresponsive particularly in windows environments. Replacing the time.sleep function with any non-PyQt5 function will yield the same results.

From what I understand this has to do with the function not being called in a separate thread using QThread. I used this answer as a reference and came up with a partial solution.

import sys
import time

from PyQt5.QtCore import QThread
from PyQt5.QtWidgets import (QApplication, QDialog,
                             QProgressBar)

class External(QThread):

    def run(self):
        count = 0

        while count < 100:
            count += 1
            print(count)
            time.sleep(1)

class Actions(QDialog):
    def __init__(self):
        super().__init__()
        self.initUI()

    def initUI(self):
        self.progress = QProgressBar(self)
        self.progress.setGeometry(0, 0, 300, 25)
        self.show()

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = Actions()
    calc = External()
    calc.finished.connect(app.exit)
    calc.start()
    sys.exit(app.exec_())

This will run time.sleep in the background and keep the main window responsive. But, I don't know how to update the values using self.progress.setValue since it's not accessible in class External.

So far from what I know, I have to use signals to accomplish this. Most of the documentation out there is for PyQt4 making it harder to find a solution.

Another problem I am faced with is being able to start the External thread from within class Actions.

Answers to this problem will also serve as valuable documentation for PyQt5. Thanks in advance.


Solution

  • You must use the signals to update the values.

    import sys
    import time
    
    from PyQt5.QtCore import QThread, pyqtSignal
    from PyQt5.QtWidgets import (QApplication, QDialog,
                                 QProgressBar)
    
    class External(QThread):
        countChanged = pyqtSignal(int)
        def run(self):
            count = 0
    
            while count < 100:
                count += 1
                self.countChanged.emit(count)
                print(count)
                time.sleep(1)
    
    class Actions(QDialog):
        def __init__(self):
            super().__init__()
            self.initUI()
    
        def initUI(self):
            self.progress = QProgressBar(self)
            self.progress.setGeometry(0, 0, 300, 25)
            self.show()
    
        def onCountChanged(self, value):
            self.progress.setValue(value)
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        window = Actions()
        calc = External()
        calc.countChanged.connect(window.onCountChanged)
        calc.start()
        sys.exit(app.exec_())