Проблема с обновлением модели 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 шт):
Я не понимаю вашу логику с классами и какую тяжелую задачу вы собираетесь выполнять в дополнительном потоке.
Вы пишите, что после каких-то событий надо обновить все ячееки таблицы, т.е. установить новую модель.
Пусть этим событием будет клик по кнопке '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())
