I am trying to implement multi-thread in a QT application developed with PySide6. Starting from this example I have a code which looks like the code below.
The below code works well, without error. In my complete code, I got an error in the corresponding add_item_tree function. The error comes when I click (right click) on the new created item in the tree. The item is well created (as in my short example below) but after, if I click or interact with him I got the error.
I got the following error:
QObject::setParent: Cannot set parent, new parent is in a different thread
QObject::startTimer: Timers cannot be started from another thread
QObject::installEventFilter(): Cannot filter events for objects in a different thread.
QObject::installEventFilter(): Cannot filter events for objects in a different thread.
QObject::setParent: Cannot set parent, new parent is in a different thread
qt.qpa.window: Window position QRect(-4,42 283x401) outside any known screen, using primary screen
[1] 92579 segmentation fault
Is there a way to know, when I reach the add_item_tree function, which thread is running? The Tree widget and several items are created before. The object returned by the function executed through the worker does not create any widget. New widget are created in the add_item_tree function.
I don't know how to dig into this error ... The main goal, is to be able to add to my tree a new item from the object returned by the worker.
from PySide6.QtWidgets import (QVBoxLayout, QLabel, QPushButton, QWidget,
QMainWindow, QApplication, QTreeWidget,
QTreeWidgetItem, QStackedWidget)
from PySide6.QtCore import QRunnable, Slot, Signal, QObject, QThreadPool, QThread
import sys
import time
import traceback
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import Cursor
from matplotlib.backends.backend_qtagg import NavigationToolbar2QT as NavigationToolbar
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
class BasePlotter(FigureCanvasQTAgg):
""" This class define base method for plotter classes """
def __init__(self, my_object, a: float):
self.fig, self.ax = plt.subplots(1, 1)
super().__init__(self.fig)
self.my_object = my_object
self.a = a
self.toolbar = NavigationToolbar(self)
self.cursor = Cursor(self.ax, horizOn=True, vertOn=True,
useblit=True, color='black', linewidth=1)
x = np.linspace(0, 1, 10)
self.ax.plot(x, self.a * x ** my_object.n)
self.draw()
def get_plot_widget(self, parent):
plot_widget = QWidget(parent=parent)
layout = QVBoxLayout(plot_widget)
layout.addWidget(self)
layout.addWidget(self.toolbar)
return plot_widget
class WorkerSignals(QObject):
'''
Defines the signals available from a running worker thread.
'''
started = Signal(str)
finished = Signal(str)
error = Signal(tuple)
result = Signal(object)
class Worker(QRunnable):
"""
This is a general class to run any threads.
Inherits from QRunnable to handler worker thread setup, signals and wrap-up.
"""
def __init__(self, fn, *args, **kwargs):
"""
Initialize the work with the function to run and its arguments
Args:
fn (function): the callback function to be run in the threads
args: positional arguments to pass to the callback function
kwargs: keywords arguments to pass to the callback function
"""
super().__init__()
# Store constructor arguments (re-used for processing)
self.fn = fn
self.args = args
self.kwargs = kwargs
self.signals = WorkerSignals()
def run(self):
'''
Initialise the runner function with passed args, kwargs.
'''
self.signals.started.emit(self.fn.__name__)
# Retrieve args/kwargs here; and fire processing using them
try:
# execute the function and catch any errors
# worker args and kwargs are passed to the function
result = self.fn(*self.args, **self.kwargs)
except Exception:
# get and return the error
traceback.print_exc()
exctype, value = sys.exc_info()[:2]
self.signals.error.emit((exctype, value, traceback.format_exc()))
else:
# return the results in case of normal ends
self.signals.result.emit(result)
finally:
# final worker signal
self.signals.finished.emit("THREAD COMPLETE!")
class PlotItem(QTreeWidgetItem):
def __init__(self, parent, plot, idx, name):
super().__init__(parent, [f"{name}"])
self.plot = plot
self.idx = idx
class CustomItem(QTreeWidgetItem):
def __init__(self, parent, my_object):
super().__init__(parent, [f"Item {my_object.n}"])
self.my_object = my_object
@classmethod
def get_new_item(cls, tree, stack, my_object):
# do more operations
item = cls(tree, my_object)
for ai in [0.01, 0.1]:
plot = BasePlotter(my_object, ai)
plot_widget = plot.get_plot_widget(stack)
stack.addWidget(plot_widget)
idx = stack.indexOf(plot_widget)
pitem = PlotItem(item, plot, idx, f"plot {ai}")
item.addChild(pitem)
def __str__(self):
return f"my object {self.my_object.n}"
class MyObject:
counter = 0
def __init__(self, n: int):
self.n = n
@classmethod
def get_object(cls, n: int = None):
print("get object thread: ", QThread.currentThread())
time.sleep(2)
cls.counter += 1
return cls(cls.counter)
class MainWindow(QMainWindow):
def __init__(self, *args, **kwargs):
super(MainWindow, self).__init__(*args, **kwargs)
layout = QVBoxLayout()
self.tree = QTreeWidget()
self.tree.setColumnCount(1)
self.tree.setHeaderLabels(["counter"])
button = QPushButton("Run!")
button.pressed.connect(self.add_item)
self.label = QLabel()
self.plot_stack = QStackedWidget()
item = CustomItem.get_new_item(self.tree, self.plot_stack, MyObject(0))
self.tree.insertTopLevelItem(0, item)
layout.addWidget(button)
layout.addWidget(self.label)
layout.addWidget(self.plot_stack)
layout.addWidget(self.tree)
central_widget = QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
self.show()
self.tree.itemSelectionChanged.connect(self.handle_selected_item)
self.threadpool = QThreadPool()
print("Multithreading with maximum %d threads" % self.threadpool.maxThreadCount())
def handle_selected_item(self):
item = self.tree.selectedItems()[0]
self.label.setText(f"Item {str(item)}")
if isinstance(item, PlotItem):
self.plot_stack.setCurrentIndex(item.idx)
# @Slot(object)
def add_item_tree(self, my_object):
print("add_item thread: ", QThread.currentThread())
item = CustomItem.get_new_item(self.tree, self.plot_stack, my_object)
self.tree.insertTopLevelItem(0, item)
@Slot(str)
def print_message(self, message):
print(message)
def add_item(self):
# add item without threads
# myo = MyObject.get_object()
# self.add_item_tree(myo)
# add item with threads
worker = Worker(MyObject.get_object)
worker.signals.started.connect(self.print_message)
worker.signals.result.connect(self.add_item_tree)
worker.signals.finished.connect(self.print_message)
# Execute
self.threadpool.start(worker)
print("Initial thread: ", QThread.currentThread())
app = QApplication(sys.argv)
window = MainWindow()
app.exec()
EDIT
I edit the code to succeed to reproduce part of the error including more functionalities. As you may see, there are two type of TreeItem in the tree, data or plots, plots being children of data. From the app, you can make operations on the data to produce new data or new plots and finally add new item (or child) in the tree.
I say that I reproduce part of the error because here, I got a segmentation fault. Actually it depends if I set the @Slot on the add_item_tree. Moreover, the finished signal is never fired. The message is not printed.
$ > python --version
Python 3.9.18
$ > python mythreads2.py
PS: 6.5.2 QT: 6.5.2
Initial thread: <PySide6.QtCore.QThread(0x600000024180) at 0x7fe8ac3b44c0>
Multithreading with maximum 8 threads
get object thread: <PySide6.QtCore.QThread(0x600000ce9020, name = "Thread (pooled)") at 0x7fe8acc921c0>
get_object
add_item thread: <PySide6.QtCore.QThread(0x600000024180) at 0x7fe8ac1d0900>
[1] 12306 segmentation fault python mythreads2.py
$ > python -m pip freeze
PySide6==6.5.2
PySide6-Addons==6.5.2
PySide6-Essentials==6.5.2