Проблема с обновлением модели QAbstractTableModel в QTreeView

У меня есть модель, содержащая список класса MyClass. Для обновления данных в модели я написал функцию updateData, которая передаёт в модель новый список класса MyClass.

Класс MyClass содержит параметр (атрибут parameter), от которого зависят значения некоторых столбцов модели. После обновления данных модели в функции updateData запускается поток MyThread, в котором происходит заполнение параметра (выполняется функция setParameter) каждого элемента списка MyClass модели. Мне нужно, чтобы при изменении значения параметра, все связанные с ним ячейки таблицы обновлялись.

Я попробовал решить это следующим образом: я добавил в класс MyClass сигнал setParameter_signal, который испускается функцией setParameter.
Сигналы элементов списка я подключаю в функции updateData. Мне удалось добиться желаемого обновления ячеек таблицы, но по какой-то причине программа иногда крашится:

Process finished with exit code -1073741819 (0xC0000005)

при обновлении данных модели.

Ниже приведён минимальный воспроизводимый пример. Чтобы воспроизвести ошибку, которую я пытаюсь исправить, необходимо много раз нажать кнопку "Update" (не дожидаясь завершения потока MyThread). Помогите найти и исправить ошибку.

import typing
from random import randint
from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractTableModel, QObject, pyqtSignal, QThread, QModelIndex, Qt, QPersistentModelIndex


class MyClass(QObject):
    setParameter_signal: pyqtSignal = pyqtSignal()  # The signal emitted when the parameter is changed.

    def __init__(self):
        super().__init__()  # __init__() QObject.
        self.parameter = None

    def setParameter(self, value):
        self.parameter = value
        self.setParameter_signal.emit()  # Emit a signal that the parameter has been changed.


class MyThread(QThread):
    def __init__(self, thread_data_list: list[MyClass]):
        super().__init__()  # __init__() QThread.
        self._thread_data_list: list[MyClass] = thread_data_list

    def run(self) -> None:
        for i, my_class_instance in enumerate(self._thread_data_list):
            if self.isInterruptionRequested(): break
            self.usleep(100)
            my_class_instance.setParameter(i)


class Column:
    def __init__(self, header: str | None = None, display_function=None, parameter_dependence: bool = False):
        self.header: str | None = header
        self.getDisplay = display_function
        self.parameter_dependence: bool = parameter_dependence

    def __call__(self, role, *data):
        if role == Qt.ItemDataRole.DisplayRole:
            return None if self.getDisplay is None else self.getDisplay(*data)


class MyModel(QAbstractTableModel):
    def __init__(self):
        super().__init__()  # __init__() QAbstractTableModel.
        self._data_list: list[MyClass] = []
        self._thread: MyThread | None = None
        self.columns: tuple[Column, Column] = (
            Column(header='First',
                   display_function=lambda my_class_instance: 'Name'),
            Column(header='Second',
                   display_function=lambda my_class_instance: '' if my_class_instance.parameter is None else my_class_instance.parameter,
                   parameter_dependence=True),
        )

    def rowCount(self, parent: QModelIndex = ...) -> int:
        return len(self._data_list)

    def columnCount(self, parent: QModelIndex = ...) -> int:
        return len(self.columns)

    def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
        column: Column = self.columns[index.column()]
        my_class_instance: MyClass = self._data_list[index.row()]
        return column(role, my_class_instance)

    def updateData(self, data_list: list[MyClass]):
        """Sets the model data."""
        self.beginResetModel()
        self._stopThread()
        self._data_list = data_list
        '---------Connecting setParameter_signal signals to slots---------'
        def emitDataChanged(persistent_index: QPersistentModelIndex):
            index: QModelIndex = QModelIndex(persistent_index)
            self.dataChanged.emit(index, index)
        for row, my_class_instance in enumerate(self._data_list):
            for column, my_column_instance in enumerate(self.columns):
                if my_column_instance.parameter_dependence:  # Only for columns that depend on the parameter.
                    index: QModelIndex = self.index(row, column)
                    persistent_index: QPersistentModelIndex = QPersistentModelIndex(index)
                    my_class_instance.setParameter_signal.connect(lambda: emitDataChanged(persistent_index))  # Connect the update slot.
        '-----------------------------------------------------------------'
        self.endResetModel()
        self._startThread(self._data_list)

    def _startThread(self, data_list: list[MyClass]):
        self._thread = MyThread(data_list)
        self._thread.start()

    def _stopThread(self):
        if self._thread is not None:
            self._thread.requestInterruption()
            self._thread.wait()
            self._thread = None


class Form(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()  # __init__() QMainWindow.
        self.centralwidget = QtWidgets.QWidget(self)
        self.main_verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)

        self.tableView = QtWidgets.QTreeView(self.centralwidget)
        self.tableView.setModel(MyModel())
        self.main_verticalLayout.addWidget(self.tableView)

        self.button = QtWidgets.QPushButton(self)
        self.button.setText('Update')
        self.button.clicked.connect(lambda: self.tableView.model().updateData([MyClass() for _ in range(randint(1500, 3000))]))
        self.main_verticalLayout.addWidget(self.button)

        self.setCentralWidget(self.centralwidget)


if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Form()
    window.show()
    sys.exit(app.exec())

Ответы (1 шт):

Автор решения: S. Nick

Я не понимаю вашу логику с классами и какую тяжелую задачу вы собираетесь выполнять в дополнительном потоке.

Вы пишите, что после каких-то событий надо обновить все ячееки таблицы, т.е. установить новую модель.

Пусть этим событием будет клик по кнопке 'Update'. Попробуйте мой вариант обновления.


Обратите внимание, что:

  • randint(3_000, 10_000), чтобы было веселее;
  • вставил расчета прошедшего времени, чтобы вам было понятно сколько времени затрачивается (в миллисекундах) на выполнение некоторых фрагментов кода.

import typing
from random import randint
'''
from PyQt6 import QtWidgets
from PyQt6.QtCore import QAbstractTableModel, QObject, pyqtSignal, \
    QThread, QModelIndex, Qt, QPersistentModelIndex
'''
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.Qt import *


class Column:
#    def __init__(self, header: str | None = None, display_function=None, parameter_dependence: bool = False):
    def __init__(self, header=None, display_function=None, parameter_dependence=False):
        self.header = header
        self.getDisplay = display_function
        self.parameter_dependence: bool = parameter_dependence

    def __call__(self, role, *data):
        print(f'def __call__(self, role, *data): {data}') #
        if role == Qt.ItemDataRole.DisplayRole:
            return None if self.getDisplay is None else self.getDisplay(*data)
            

class MyClass(QObject):                                   # ??? ?
    # ??? ?
    # Сигнал, излучаемый при изменении параметра.
    setParameter_signal: pyqtSignal = pyqtSignal()  

    def __init__(self):
        super().__init__()                             
        self.parameter = None

    def setParameter(self, value):
        self.parameter = value
        # Сигнал, излучаемый при изменении параметра.
        self.setParameter_signal.emit()  


class TableModel(QAbstractTableModel):
    def __init__(self, data, parent=None):
        super(TableModel, self).__init__(parent)
        self._data = data
        
    def rowCount(self, parent=None):
        return len(self._data)
        
    def columnCount(self, parent=None):
        return len(self._data[0]) if self.rowCount() else 0
        
    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            row = index.row()
            if 0 <= row < self.rowCount():
                column = index.column()
                if 0 <= column < self.columnCount():
                    return self._data[row][column]


class Form(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()  
        self.centralwidget = QtWidgets.QWidget(self)
        self.setCentralWidget(self.centralwidget)
        self.main_verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)

        self.tableView = QtWidgets.QTreeView(self.centralwidget)
#        self.tableView.setModel(MyModel())
        self.main_verticalLayout.addWidget(self.tableView)

        self.button = QtWidgets.QPushButton(self)
        self.button.setText('Update')
        self.button.clicked.connect(
            lambda: self.populateTable('custom')                           # +++
#            lambda: self.tableView.model().updateData(
# ?                [MyClass() for _ in range(randint(1500, 3000))]  
#            )
        )
        self.main_verticalLayout.addWidget(self.button)

        self._data = []                                                    # !!! +++
        
    def populateTable(self, mode):                                         # +++
        # Класс QElapsedTimer обеспечивает быстрый способ расчета прошедшего времени.
        # https://doc.qt.io/qt-6/qelapsedtimer.html#elapsed
        timer = QElapsedTimer() 
        timer.start() 

        model = self.tableView.model()
        if model is not None:   
            self.tableView.setModel(None)
            model.deleteLater()

        self.value = randint(3_000, 10_000)                                  # !!!       
        if len(self._data) != self.value:             
            del self._data[:]
            rows = list(range(self.value))

            self.columns: tuple[Column, Column] = (
                Column(header='First',
                       display_function=lambda my_class_instance: 'Name'),
                Column(header='Second',
                       display_function=lambda my_class_instance: '' if my_class_instance.parameter is None else my_class_instance.parameter,
                       parameter_dependence=True),
            )
# +++
            for row in rows:
                # ? MyClass()
                parameter = row
                columns: tuple[Column, Column] = (
                    Column(header='First',
                           display_function='Name'),
                    Column(header='Second',
                           display_function='' if parameter is None else parameter,
                           parameter_dependence=True),
                )
  
                items = []
                for col, column in enumerate(columns):
                    if col == 0:
                        items.append(columns[0].getDisplay)
                    if col == 1: 
                        items.append(columns[1].getDisplay)
                self._data.append(items)

        print(f'111 {mode}: {timer.elapsed():>4} milliseconds;  {self.value} строк')                 
        timer.start()          

        self.tableView.setSortingEnabled(False)                           # !!!
        model = TableModel(self._data, self.tableView)                    # !!!
        self.tableView.setModel(model)                                    # !!!

        print(f'222 {mode}: {timer.elapsed():>4} milliseconds;  {self.value} строк\n')
                

if __name__ == '__main__':
    import sys
    app = QtWidgets.QApplication(sys.argv)
    window = Form()
    window.resize(250, 500)
    window.show()
    sys.exit(app.exec())

введите сюда описание изображения

→ Ссылка