2

I have an application where I run some process in a second thread and at some point, given a certain condition, another dialog window opens, which halts the process until you confirm something. This causes the following error message:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QApplication(0x1f9c82383d0), parent's thread is QThread(0x1f9c7ade2a0), current thread is QThread(0x1f9c8358800)

Interestingly, if you also move your cursor over the MainWindow while the process is running, and before the new dialog pops up, it also produces this error message a couple of times:

QBasicTimer::stop: Failed. Possibly trying to stop from a different thread

Very strange. Because it only occurs if you move your cursor over the MainWindow.

Now, in my application, I actually load an interface for the new dialog that pops up using PyQt5.uic.loadUi, and this hasn't caused any problems. However, when I was creating the example for this post, another issue occurred, due to the fact that I was setting the layout of the new dialog during its initialization:

QObject::setParent: Cannot set parent, new parent is in a different thread

Which results in the application crashing:

Process finished with exit code -1073741819 (0xC0000005)

I'm obviously doing something wrong here regarding the threading I would guess, but I don't know what. I am especially baffled by the fact that I cannot set the layout of the new dialog during its initialization, while using loadUi is totally fine. Here is my example code:

import sys
import time
import numpy as np

from PyQt5.QtCore import QObject, pyqtSignal, QThread
from PyQt5.QtWidgets import (
    QDialog, QApplication, QPushButton, QGridLayout, QProgressBar, QLabel
)


class SpecialDialog(QDialog):
    def __init__(self):
        super().__init__()
        btn = QPushButton('pass variable')
        btn.clicked.connect(self.accept)
        layout = QGridLayout()
        layout.addWidget(btn)
        # self.setLayout(layout)
        self.variable = np.random.randint(0, 100)


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

    def __init__(self):
        super().__init__()
        self._isRunning = True
        self._success = False

    def run(self):
        result = None
        i = 0
        while i < 100 and self._isRunning:
            if i == 50:
                dialog = SpecialDialog()
                dialog.exec_()
                result = dialog.variable
            time.sleep(0.01)
            i += 1
            self.progress.emit(i)

        if i == 100:
            self._success = True
            self.finished.emit(result)

    def stop(self):
        self._isRunning = False


class MainWindow(QDialog):
    def __init__(self):
        super().__init__()
        btn = QPushButton('test')
        btn.clicked.connect(self.run_test)
        self.pbar = QProgressBar()
        self.resultLabel = QLabel('Result:')
        layout = QGridLayout(self)
        layout.addWidget(btn)
        layout.addWidget(self.pbar)
        layout.addWidget(self.resultLabel)
        self.setLayout(layout)

        self.handler = None
        self.handler_thread = QThread()
        self.result = None

    def run_test(self):
        self.handler = Handler()
        self.handler.moveToThread(self.handler_thread)
        self.handler.progress.connect(self.progress)
        self.handler.finished.connect(self.finisher)
        self.handler_thread.started.connect(self.handler.run)
        self.handler_thread.start()

    def progress(self, val):
        self.pbar.setValue(val)

    def finisher(self, result):
        self.result = result
        self.resultLabel.setText(f'Result: {result}')
        self.pbar.setValue(0)
        self.handler.stop()
        self.handler.progress.disconnect(self.progress)
        self.handler.finished.disconnect(self.finisher)
        self.handler_thread.started.disconnect(self.handler.run)
        self.handler_thread.terminate()
        self.handler = None


if __name__ == '__main__':
    app = QApplication(sys.argv)
    GUI = MainWindow()
    GUI.show()
    sys.exit(app.exec_())

EDIT

I forgot to mention that I already found this post, which may be related to my problem, however, I don't undestand the reasoning of the solution in the top answer, and more importantly, I don't speak what I believe is C++.

mapf
  • 1,906
  • 1
  • 14
  • 40

1 Answers1

3

You cannot create or modify a GUI element from a secondary thread and this is signaled by the error message.

You have to redesign the Handler class, with your requirement you must divide run into 2 methods, the first method will generate progress up to 50% where the GUI will open the dialogue, obtain the result and launch the second method.

import sys
import time
import numpy as np
from functools import partial

from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, QThread, QTimer
from PyQt5.QtWidgets import (
    QDialog,
    QApplication,
    QPushButton,
    QGridLayout,
    QProgressBar,
    QLabel,
)


class SpecialDialog(QDialog):
    def __init__(self):
        super().__init__()
        btn = QPushButton("pass variable")
        btn.clicked.connect(self.accept)
        layout = QGridLayout()
        layout.addWidget(btn)
        # self.setLayout(layout)
        self.variable = np.random.randint(0, 100)


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

    def __init__(self):
        super().__init__()
        self._isRunning = True
        self._success = False

    @pyqtSlot()
    def task1(self):
        i = 0
        while i <= 50 and self._isRunning:
            time.sleep(0.01)
            i += 1
            self.progress.emit(i)

    @pyqtSlot(int)
    def task2(self, result):
        i = 50
        while i < 100 and self._isRunning:
            time.sleep(0.01)
            i += 1
            self.progress.emit(i)

        if i == 100:
            self._success = True
            self.finished.emit(result)

    def stop(self):
        self._isRunning = False


class MainWindow(QDialog):
    def __init__(self):
        super().__init__()
        btn = QPushButton("test")
        btn.clicked.connect(self.run_test)
        self.pbar = QProgressBar()
        self.resultLabel = QLabel("Result:")
        layout = QGridLayout(self)
        layout.addWidget(btn)
        layout.addWidget(self.pbar)
        layout.addWidget(self.resultLabel)
        self.setLayout(layout)

        self.handler = None
        self.handler_thread = QThread()
        self.result = None

    def run_test(self):
        self.handler = Handler()
        self.handler.moveToThread(self.handler_thread)
        self.handler.progress.connect(self.progress)
        self.handler.finished.connect(self.finisher)
        self.handler_thread.started.connect(self.handler.task1)
        self.handler_thread.start()

    @pyqtSlot(int)
    def progress(self, val):
        self.pbar.setValue(val)
        if val == 50:
            dialog = SpecialDialog()
            dialog.exec_()
            result = dialog.variable
            wrapper = partial(self.handler.task2, result)
            QTimer.singleShot(0, wrapper)

    def finisher(self, result):
        self.result = result
        self.resultLabel.setText(f"Result: {result}")
        self.pbar.setValue(0)
        self.handler.stop()
        self.handler_thread.quit()
        self.handler_thread.wait()


if __name__ == "__main__":
    app = QApplication(sys.argv)
    GUI = MainWindow()
    GUI.show()
    sys.exit(app.exec_())
eyllanesc
  • 235,170
  • 19
  • 170
  • 241
  • Thank you for your answer! This put me on the right path I hope. I guess I made the example a little bit too simple, because I actually don't know when exactly the condition to open the new dialog is met (and if at all), and how often this happens. But with your solution I came up with my own, although I am not sure how good this is. It would be great if you could take a look at my answer below. – mapf Feb 25 '20 at 13:01
  • 1
    @mapf 1) I only solve the problem they propose based on the code they provide, so I don't presume the complexity or modifications. If you had provided a better MRE, I would have proposed a more appropriate solution to your real problem. 2) Since my answer solves the problem that is in your question then I hope you mark it as correct, if you do not know how to do it then check the [tour] – eyllanesc Feb 25 '20 at 13:06
  • I understand, sorry. I wasn't aware of how relevant the simplification of my problem would be for the solution. It's a thin line to walk between including too much or leaving our relevant bits. Do you suggest I ask the same question again with a more complex MRE and close this one? – mapf Feb 25 '20 at 13:13
  • @mapf Yes, ask a new question with a valid MRE and also delete your answer as it is incorrect. – eyllanesc Feb 25 '20 at 13:14