1

Creating a QMainWindow >> pushing start button >> that connects a long-running function with a QLabel as arg >> updating the label while running the long function.

I want to update the status of the long-running function in the GUI. But as soon as the long-running function starts the whole window freezes

import sys
import time
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
    QApplication,
    QLabel,
    QMainWindow,
    QPushButton,
    QVBoxLayout,
    QWidget,
)

def runLongTask(label):
    label.setText('<1> sleeping for 10s ...')
    time.sleep(10)
    label.setText('<2> sleeping for 10s ...')
    time.sleep(10)
    label.setText('End')

class Window(QMainWindow):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setupUi()

    def setupUi(self):
        self.setWindowTitle("GUI freeze FIX")
        self.resize(350, 250)
        self.centralWidget = QWidget()
        self.setCentralWidget(self.centralWidget)
        self.label = QLabel()
        self.label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
        self.label.setText('No Update')
        countBtn = QPushButton("Start")
        countBtn.clicked.connect(lambda: runLongTask(self.label))
        layout = QVBoxLayout()
        layout.addWidget(self.label)
        layout.addWidget(countBtn)
        self.centralWidget.setLayout(layout)

app = QApplication(sys.argv)
window = Window()
window.show()
sys.exit(app.exec())
imankalyan
  • 155
  • 1
  • 8

2 Answers2

8

You should use PyQt5's threads to start your long function in a different thread. That way, the main UI thread won't be busy itself, and will even be able to receive signals from the other thread and therefore update the UI.

This article is a good introduction to QThread usage.

Here is an example of a long task executed when clicking a button. The long task uses dummy time.sleep(x) to make it long, but notice how the update_ui function is passed, like a callback, to update the UI.

import sys
import time
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5.QtWidgets import (
    QApplication,
    QMainWindow,
    QProgressBar,
    QPushButton,
    QVBoxLayout,
    QWidget,
)


def long_running_function(update_ui):
    # Doing something
    time.sleep(1)
    update_ui(percent=25)

    # Doing something else
    time.sleep(1)
    update_ui(percent=50)

    # Another long thing
    time.sleep(1)
    update_ui(percent=75)

    # Almost done
    time.sleep(1)
    update_ui(percent=100)


class Worker(QObject):
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def run(self):
        # Here we pass the update_progress (uncalled!)
        # function to the long_running_function:
        long_running_function(self.update_progress)
        self.finished.emit()

    def update_progress(self, percent):
        self.progress.emit(percent)


class MainWindow(QMainWindow):
    def __init__(self):
        super(MainWindow, self).__init__()
        layout = QVBoxLayout()
        self.progress = QProgressBar()
        self.button = QPushButton("Start")
        layout.addWidget(self.progress)
        layout.addWidget(self.button)

        self.button.clicked.connect(self.execute)

        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()

    def execute(self):
        self.update_progress(0)
        self.thread = QThread()
        self.worker = Worker()
        self.worker.moveToThread(self.thread)

        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.worker.progress.connect(self.update_progress)

        self.thread.start()
        self.button.setEnabled(False)

    def update_progress(self, progress):
        self.progress.setValue(progress)
        self.button.setEnabled(progress == 100)


if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    app.exec_()
Munshine
  • 402
  • 3
  • 14
  • 1
    Thanks. It worked. One typo, in update_ui(progress=25) it should be percent in place of progress. – imankalyan Jan 19 '21 at 04:40
  • @imankalyan If that answer was correct, [could you accept it](https://stackoverflow.com/help/someone-answers) ? :) – Munshine Feb 01 '21 at 10:54
0

You may manually call processEvents:

def runLongTask(label):
    label.setText('<1> sleeping for 10s ...')
    QApplication.processEvents()
    time.sleep(10)
    label.setText('<2> sleeping for 10s ...')
    QApplication.processEvents()
    time.sleep(10)
    label.setText('End')
    label.update()    

For more info, you should search for "qt Keeping the GUI Responsive", or the QThread/QtConcurrent answer here: How to make Qt work when main thread is busy?

Demi-Lune
  • 1,868
  • 2
  • 15
  • 26
  • 4
    No. processEvents should only be used in specific situations that potentially allow minimal event queue processing or *actually* require "clearing" the queue. With blocks that take *this* much time (10 seconds is a lot) this is *not* a good choice, most importantly because it could cause lots of issue in input interaction. – musicamante Jan 18 '21 at 12:15
  • Though it's updating the label, the GUI still freezes while sleeping. – imankalyan Jan 19 '21 at 06:22