why can't i close a QThreadPool?

57 Views Asked by At

i am trying to create a loading screen using PyQt6 for my functions (which sometimes take too long of a time), and i am using a QThreadPool to move those functions into a different thread, here is my code so far:

from src.gui.loading import Ui_Form
from PyQt6.QtWidgets import QWidget
from PyQt6.QtCore import QThreadPool, QRunnable, QTimer, QThread
from typing import Callable


class Loading(Ui_Form, QWidget):
    def __init__(self,
                 parent: QWidget | None,
                 next_widget: QWidget | None,
                 action: str,
                 time: int,
                 task: Callable,
                 task_len: int,
                 initial_task: str):
        super().__init__()
        self.setupUi(self)
        self.setParent(parent)
        self.parent = parent
        self.next_widget = next_widget
        self.time = time
        self.task = TaskRunner(self, task)
        self.current_time = 0
        self.tasks_done = 0
        self.all_tasks = task_len

        self.Task.setText(action)
        self.Estimation.setText(f"estimated time: {self.int_to_time(time)}")
        self.progressBar.setValue(0)
        self.TimeLeft.setText("")
        self.Current.setText("")
        self.Task.setText("")

        self.thread_pool = QThreadPool(self)
        self.thread_pool.destroyed.connect(self.quit)
        self.run_tasks()
        self.task_done(initial_task)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_time)
        self.timer.start(1000)

    @staticmethod
    def int_to_time(time: int) -> str:
        if time >= 3600:
            return f"{time / 3600} hours"
        elif time >= 60:
            return f"{time / 60} minutes"
        else:
            return f"{time} seconds"

    def update_time(self):
        self.current_time += 1
        self.TimeLeft.setText(self.int_to_time(self.current_time))

    def task_done(self, next_task: str = None):
        self.tasks_done += 1
        if not next_task:
            self.Current.setText("finished all tasks, closing window")
            self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}")
        elif self.tasks_done != self.all_tasks:
            self.Current.setText(f"currently: {next_task}")
            self.Tasks.setText(f"{self.tasks_done} out of {self.all_tasks}")

    def quit(self):
        self.close()

    def closeEvent(self, a0):
        self.timer.stop()
        self.thread_pool.waitForDone(-1)
        self.close()

    def run_tasks(self): self.thread_pool.start(self.task)


class TaskRunner(QRunnable):
    def __init__(self, parent: Loading | None, task: Callable):
        super().__init__()
        self.parent = parent
        self.task = task

    def run(self):
        self.task(self.parent)
        if self.parent:
            self.parent.task_done(None)

while this code mostly works, i can't seem to close the window no matter what (by closing the window, i mean inside the code, using the function self.close() doesn't work). i tried debugging a little bit and i found out that self.thread_pool doesn't seem to close even if all tasks are finished. i am coming from rust, and in rust thread pools can just be closed if no functions are running, is it possible to do the same thing in PyQt6? and if not, then why?

btw to anyone who wants to try this code, here is Ui_Form:

# Form implementation generated from reading ui file 'loading.ui'
#
# Created by: PyQt6 UI code generator 6.6.0
#
# WARNING: Any manual changes made to this file will be lost when pyuic6 is
# run again.  Do not edit this file unless you know what you are doing.


from PyQt6 import QtCore, QtGui, QtWidgets


class Ui_Form(object):
    def setupUi(self, Form):
        Form.setObjectName("Form")
        Form.resize(836, 302)
        Form.setStyleSheet("#centralwidget {background-color:rgba(20, 20, 20, 250);}\n"
"#Form {background-color:rgba(20, 20, 20, 250);}\n"
"QMainWindow{background-color:rgba(20, 20, 20, 250);}\n"
"\n"
"QWidget{\n"
"    color:rgba(249, 249, 249, 240);\n"
"}\n"
"\n"
"QMenuBar{background-color:rgba(20, 20, 20, 250);}\n"
"QMenu{background-color:rgba(20, 20, 20, 250);}\n"
"QMenu::item:selected{background-color:rgba(229, 229, 229, 100);}\n"
"QMenuBar::item:selected{background-color:rgba(229, 229, 229, 100);}\n"
"QListWidget{background-color:rgba(20, 20, 20, 250);}\n"
"QSpinBox{background-color:rgba(20, 20, 20, 250);}\n"
"\n"
"QMenu::separator{height:5px; background-color:rgba(191, 191, 191, 100);}\n"
"\n"
"QComboBox {\n"
"    color:rgba(193, 193, 193, 250);\n"
"    background-color:rgba(29, 29, 29, 250);\n"
"    border:none;\n"
"}\n"
"QComboBox QAbstractItemView{\n"
"    color:rgba(193, 193, 193, 250);\n"
"    background-color:rgba(29, 29, 29, 250);\n"
"    border:none;\n"
"}\n"
"QLineEdit {\n"
"    color:rgba(193, 193, 193, 250);\n"
"    background-color:rgba(29, 29, 29, 250);\n"
"    border:none;\n"
"}\n"
"QTextEdit {\n"
"    color:rgba(193, 193, 193, 250);\n"
"    background-color:rgba(29, 29, 29, 250);\n"
"    border:none;\n"
"}\n"
"QProgressBar {\n"
"     border: 0px solid grey;\n"
"     border-radius: 0px;\n"
"     background-color:rgba(255, 255, 255, 0);\n"
" }\n"
"QTableWidget {\n"
"    background-color: rgba(29, 29, 29, 250);\n"
"    color: rgba(193, 193, 193, 250);\n"
"}\n"
"\n"
"QTableWidget::item {\n"
"    padding: 4px;\n"
"}\n"
"\n"
"QTableWidget::item:selected {\n"
"    background-color: rgba(50, 50, 50, 250);\n"
"}\n"
"\n"
"QTableWidget::item:focus {\n"
"    background-color: rgba(70, 70, 70, 250);\n"
"    outline: none;\n"
"}\n"
"\n"
"QHeaderView::section {\n"
"    background-color: rgba(50, 50, 50, 250);\n"
"    color: rgba(193, 193, 193, 250);\n"
"    padding: 4px;\n"
"    border: none;\n"
"}\n"
"\n"
"QHeaderView {\n"
"    background-color: rgba(50, 50, 50, 250);\n"
"    color: rgba(193, 193, 193, 250);\n"
"    padding: 4px;\n"
"    border: none;\n"
"}\n"
"\n"
"QHeaderView::section:checked {\n"
"    background-color: rgba(70, 70, 70, 250);\n"
"}\n"
" QTableView QTableCornerButton::section {\n"
"    background-color: rgba(50, 50, 50, 250);\n"
" }\n"
"\n"
"QPushButton{background-color:rgba(59, 59, 59, 250);}\n"
"QPushButton:hover{background-color:rgba(107, 107, 107, 250);}")
        self.verticalLayout = QtWidgets.QVBoxLayout(Form)
        self.verticalLayout.setObjectName("verticalLayout")
        self.Task = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.Task.setFont(font)
        self.Task.setText("")
        self.Task.setObjectName("Task")
        self.verticalLayout.addWidget(self.Task, 0, QtCore.Qt.AlignmentFlag.AlignHCenter)
        self.horizontalLayout = QtWidgets.QHBoxLayout()
        self.horizontalLayout.setObjectName("horizontalLayout")
        self.Estimation = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.Estimation.setFont(font)
        self.Estimation.setObjectName("Estimation")
        self.horizontalLayout.addWidget(self.Estimation)
        self.TimeLeft = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.TimeLeft.setFont(font)
        self.TimeLeft.setText("")
        self.TimeLeft.setObjectName("TimeLeft")
        self.horizontalLayout.addWidget(self.TimeLeft, 0, QtCore.Qt.AlignmentFlag.AlignRight)
        self.verticalLayout.addLayout(self.horizontalLayout)
        self.horizontalLayout_2 = QtWidgets.QHBoxLayout()
        self.horizontalLayout_2.setObjectName("horizontalLayout_2")
        self.Current = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.Current.setFont(font)
        self.Current.setObjectName("Current")
        self.horizontalLayout_2.addWidget(self.Current)
        self.Tasks = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.Tasks.setFont(font)
        self.Tasks.setObjectName("Tasks")
        self.horizontalLayout_2.addWidget(self.Tasks)
        self.verticalLayout.addLayout(self.horizontalLayout_2)
        self.progressBar = QtWidgets.QProgressBar(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.progressBar.setFont(font)
        self.progressBar.setMinimum(0)
        self.progressBar.setProperty("value", 0)
        self.progressBar.setObjectName("progressBar")
        self.verticalLayout.addWidget(self.progressBar)
        self.Error = QtWidgets.QLabel(parent=Form)
        font = QtGui.QFont()
        font.setFamily("Secular One")
        font.setPointSize(20)
        self.Error.setFont(font)
        self.Error.setStyleSheet("color: red;")
        self.Error.setText("")
        self.Error.setObjectName("Error")
        self.verticalLayout.addWidget(self.Error)

        self.retranslateUi(Form)
        QtCore.QMetaObject.connectSlotsByName(Form)

    def retranslateUi(self, Form):
        _translate = QtCore.QCoreApplication.translate
        Form.setWindowTitle(_translate("Form", "Form"))
        self.Estimation.setText(_translate("Form", "estimated time:"))
        self.Current.setText(_translate("Form", "current task:"))
        self.Tasks.setText(_translate("Form", "task x out of y"))

edit: here is the test code that i am using to run the code:

class TestLoading(TestCase):
    def test_task(self):
        def foo(loading_page: Loading):
            time.sleep(5)

        app = QApplication([])
        self.loader = Loading(None, None,"doing something", 2, foo, 2, "testing")
        self.loader.show()
        app.exec()
1

There are 1 best solutions below

1
On

using a QRunnable in my case was a mistake. the idea of a QRunnable is that it is lighter and simpler than QThread, making it easier to use in projects where a lot of threads need to run. since i only want to run a single side thread, it is much simpler (and solves the issue) to just use a QThread instead. i just deleted self.thread_pool and turned class TaskRunner(QRunnable) into class TaskRunner(QThread), and instead of running self.thread_pool.start(self.task) i can just run self.task.start. now i can delete the thread at any point, but the code stays (almost) the same.