Search code examples
pythonpyqt5python-multithreading

Python PyQt5 threading QObject: Cannot create children for a parent that is in a different thread


I'm using Python 3.7.6 with PyQt5 on my Windows 10 computer. I am trying to write a simple application that will run three different procedures at the same time showing the output in three separate text boxes in the same window. I have tried to create some simple base code to add to, but am having an issue using the threading module with PyQt5. Here is my code:

import sys, time
from threading import Thread

from PyQt5.QtWidgets import (
    QApplication, QMainWindow, QWidget, QPlainTextEdit, QHBoxLayout
    )

def run1():
    for i in range(20):
        text_1.setPlainText(text_1.toPlainText()  + (f"{i}\n"))
        time.sleep(0.0125)

def run2():
    for i in range(20):
        text_2.setPlainText(text_2.toPlainText()  + (f"{i}\n"))
        time.sleep(0.0125)

def run3():
    for i in range(20):
        text_3.setPlainText(text_3.toPlainText()  + (f"{i}\n"))
        time.sleep(0.0125)

app = QApplication([sys.argv])
win = QMainWindow()

text_1 = QPlainTextEdit()
text_2 = QPlainTextEdit()
text_3 = QPlainTextEdit()

my_widget = QWidget()
my_widget.layout = QHBoxLayout()
my_widget.layout.addWidget(text_1)
my_widget.layout.addWidget(text_2)
my_widget.layout.addWidget(text_3)
my_widget.setLayout(my_widget.layout)

win.setCentralWidget(my_widget)

t1 = Thread(target=run1)
t2 = Thread(target=run2)
t3 = Thread(target=run3)

t1.start()
t2.start()
t3.start()

t1.join()
t2.join()
t3.join()

win.show()

sys.exit(app.exec_())

When I run this code, it shows the desired output, but with multiple instances of the following error:

QObject: Cannot create children for a parent that is in a different thread.
(Parent is QTextDocument(0x166abf795e0), parent's thread is QThread(0x166a9bb0fb0), current thread is QThread(0x166abf56000)

I think I know why this is happening, but do not know how to fix it. I presume that I should use PyQt5's own QThread class, but cannot get my head around how to do it. Currently, I will just run my three separate text based Python applications with each showing its output in its own window, but I would prefer a single GUI based application with all three incorporated.


Solution

  • The problem has nothing to do with the use of QThread or not. The problem is that the GUI elements (for example the QWidget, QTextDocument, etc) are not thread-safe so you should not modify them or create them in a different thread than the main one. To emphasize my initial comment in my solution I will not use QThread but I will continue using threading but I will send the information to the main thread through signals(what if they are thread-safe):

    import sys, time
    from threading import Thread
    
    from PyQt5.QtCore import pyqtSignal, QObject
    from PyQt5.QtWidgets import (
        QApplication,
        QMainWindow,
        QWidget,
        QPlainTextEdit,
        QHBoxLayout,
    )
    
    
    class Worker(QObject):
        messageChanged = pyqtSignal(str)
    
        def start(self, fn):
            Thread(target=self._execute, args=(fn,), daemon=True).start()
    
        def _execute(self, fn):
            fn(self)
    
        def write(self, message):
            self.messageChanged.emit(message)
    
    
    def run1(worker):
        for i in range(20):
            worker.write(f"{i}\n")
            time.sleep(0.0125)
    
    
    def run2(worker):
        for i in range(20):
            worker.write(f"{i}\n")
            time.sleep(0.0125)
    
    
    def run3(worker):
        for i in range(20):
            worker.write(f"{i}\n")
            time.sleep(0.0125)
    
    
    app = QApplication([sys.argv])
    win = QMainWindow()
    
    text_1 = QPlainTextEdit()
    text_2 = QPlainTextEdit()
    text_3 = QPlainTextEdit()
    
    my_widget = QWidget()
    my_widget.layout = QHBoxLayout()
    my_widget.layout.addWidget(text_1)
    my_widget.layout.addWidget(text_2)
    my_widget.layout.addWidget(text_3)
    my_widget.setLayout(my_widget.layout)
    
    win.setCentralWidget(my_widget)
    
    worker1 = Worker()
    worker1.messageChanged.connect(text_1.appendPlainText)
    
    worker2 = Worker()
    worker2.messageChanged.connect(text_2.appendPlainText)
    
    worker3 = Worker()
    worker3.messageChanged.connect(text_3.appendPlainText)
    
    worker1.start(run1)
    worker2.start(run2)
    worker3.start(run3)
    
    
    win.show()
    
    sys.exit(app.exec_())