Re: QTreeView + sort + delete = crash
Kyle Altendorf <[email protected]>
| Newsgroups | gmane.comp.python.pyqt-pykde |
|---|---|
| Message-ID | <[email protected]> |
On 2019-05-04 23:08, John F Sturtz wrote: >> On 5/3/2019 1:36:04 AM, Florian Bruhin <[email protected]> wrote: >> >> You might want to try running pytest-qt's item model tester over your >> model: >> https://pytest-qt.readthedocs.io/en/latest/modeltester.html > > So, I think I wrapped my mind around pytest and pytest-qt enough to use > the model tester coherently. > > If I mock up a file with the simple model shown on the Model Tester > documentation site > (https://pytest-qt.readthedocs.io/en/latest/modeltester.html) and run > pytest on it, I get the output I expect -- it indicates that the test > has run and passed (attached file img1.png). > > But the model tester seems to crash on the model I posted previously. > The test starts to run but never finishes, and doesn't generate any > messages (attached file img2.png). > > I must have really done something egregiously wrong. :-/ > > The modified cat.py is attached (I cleaned it up a bit to make it a > little easier to add the tester function). It's late so I won't promise it's testing properly... but the attached .py runs the test and passes. I'd note that you shouldn't be passing the view to the model. The intention is that the Qt defined interface is all that is needed. For example, what happens when you have two views for one model? Cheers, -kyle _______________________________________________ PyQt mailing list [email protected] https://www.riverbankcomputing.com/mailman/listinfo/pyqt
x.py
(text/x-python, 10.5 KB)
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5 import uic
from itertools import groupby
import sys
# ------------------------------------------------------------------------------
# | CategoryNode |
# | |
# | |
# | |
# | Defines the nodes of the data tree that underlies CategoryTreeModel |
# ------------------------------------------------------------------------------
class CategoryNode:
def __init__(self, desc, parent=None):
self.parent_node = parent
self._desc = desc
self.children = []
# ----------------------------------------------------------------------
# | CategoryNode accessor functions
# ----------------------------------------------------------------------
# Number of columns is always one in this model
@staticmethod
def n_columns():
return 1
# Return category description
def desc(self):
return self._desc
# Set category description
def set_desc(self, desc):
self._desc = desc
# Return row
def row(self):
if self.parent_node:
return self.parent_node.children.index(self)
else:
return 0
# Return parent
def parent(self):
return self.parent_node
# Add a child
def append_child(self, c):
self.children.append(c)
# Return child from given row
def child(self, row):
return self.children[row]
# Return number of children
def n_children(self):
return len(self.children)
# Sort children
def sort_children(self):
self.children.sort(key=lambda x: x.desc())
return
# Delete child from given row
def del_child(self, row):
del self.children[row]
# Printable representation
def __repr__(self):
return str(f'->{self._desc}')
# Recursively dump tree contents
def dump(self, i=0):
print(f'''{(' ' * (i * 4))} {self.row()} -> {self}''')
for c in self.children:
c.dump(i+1)
# ------------------------------------------------------------------------------
# | CategoryTreeModel |
# | |
# | |
# | |
# | Model used by CategoryDialog tree view |
# ------------------------------------------------------------------------------
class CategoryTreeModel(QtCore.QAbstractItemModel):
# ------------------------------------------------------------[override]
# | __init__()
# ----------------------------------------------------------------------
def __init__(self, parent=None):
super().__init__(parent)
category_data = [
(4, 'Auto', None), (10, 'Auto', 'Gas'), (9, 'Auto', 'License'),
(11, 'Auto', 'Service'),
(23, 'Beer', None),
(12, 'Clothing', None), (13, 'Clothing', 'Cleaning'),
(18, 'Dining', None), (19, 'Dining', 'Breakfast'), (21, 'Dining', 'Lunch'),
(14, 'Healthcare', None), (26, 'Healthcare', 'Dental'),
(3, 'Household', None),
(5, 'Pet Expenses', None), (7, 'Pet Expenses', 'Biskies'),
(17, 'Pet Expenses', 'Chow'), (8, 'Pet Expenses', 'Grooming'),
(6, 'Pet Expenses', 'Medical'), (15, 'Pet Expenses', 'Miscellaneous'),
(25, 'Reimbursible', None)
]
# Create root node
self.root = CategoryNode('Category')
# Create main category/subcategory node structure
for g in groupby(category_data, lambda rec: rec[1]):
sub_list = list(g[1])
main_node = CategoryNode(g[0], self.root)
self.root.append_child(main_node)
for s in sub_list:
if s[2] is not None:
sub_node = CategoryNode(s[2], main_node)
main_node.append_child(sub_node)
# ------------------------------------------------------------[override]
# | rowCount()
# ----------------------------------------------------------------------
def rowCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return parent.internalPointer().n_children()
else:
return self.root.n_children()
# ------------------------------------------------------------[override]
# | columnCount()
# ----------------------------------------------------------------------
def columnCount(self, parent=QtCore.QModelIndex()):
if parent.isValid():
return parent.internalPointer().n_columns()
else:
return self.root.n_columns()
# ------------------------------------------------------------[override]
# | data()
# ----------------------------------------------------------------------
def data(self, index, role=QtCore.Qt.DisplayRole):
if index.isValid():
if role == QtCore.Qt.DisplayRole:
return index.internalPointer().desc()
elif role == QtCore.Qt.FontRole:
font = QtGui.QFont('Candara')
font.setPointSize(11)
return font
elif role == QtCore.Qt.ForegroundRole:
color = '#1e62ce'
return QtGui.QBrush(QtGui.QColor(color))
return None
# ------------------------------------------------------------[override]
# | flags()
# ----------------------------------------------------------------------
def flags(self, index):
if index.isValid():
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable
else:
return QtCore.Qt.NoItemFlags
# ------------------------------------------------------------[override]
# | index()
# ----------------------------------------------------------------------
def index(self, row, column, parent=QtCore.QModelIndex()):
# According to legend, index() may sometimes be called with invalid parameters
# (that is, with parameters that don't actually have an index). So this is
# necessary to keep it from crashing.
# https://stackoverflow.com/questions/26680168
# /pyqt-treeview-index-error-removing-last-row
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
elif parent.isValid():
return self.createIndex(row, column, parent.internalPointer().child(row))
else:
return self.createIndex(row, column, self.root.child(row))
# ------------------------------------------------------------[override]
# | parent()
# ----------------------------------------------------------------------
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
# print(f'parent(), before index.internalPointer() call - row={index.row()}')
child_node = index.internalPointer()
# print(f'parent(), after index.internalPointer() call - child_node={child_node}')
parent_node = child_node.parent()
if parent_node == self.root:
r = QtCore.QModelIndex()
else:
r = self.createIndex(parent_node.row(), 0, parent_node)
return r
# ------------------------------------------------------------[override]
# | setData()
# ----------------------------------------------------------------------
# noinspection PyUnresolvedReferences
def setData(self, index, value, role=QtCore.Qt.EditRole):
if role == QtCore.Qt.EditRole:
# Set value in data tree node
node = index.internalPointer()
node.set_desc(value)
# Re-sort at the level of the modified node (i.e., the children of the
# modified node's parent)
# Need to emit layoutAboutToBeChanged and layoutChanged signals to get
# QTreeView to update properly
self.layoutAboutToBeChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHint)
node.parent().sort_children()
self.layoutChanged.emit([], QtCore.QAbstractItemModel.VerticalSortHint)
# Make modified item current
parent = self.parent(index)
self.view.setCurrentIndex(self.index(node.row(), 0, parent))
self.view.setFocus(QtCore.Qt.OtherFocusReason)
# ------------------------------------------------------------[override]
# | insertRows()
# ----------------------------------------------------------------------
def insertRows(self, row, rows, parent=QtCore.QModelIndex()):
assert rows == 1
# Determine parent
if parent.internalPointer() is None:
parent_node = self.root
else:
parent_node = parent.internalPointer()
# Create new data tree node and add it to parent
new = CategoryNode(None, parent_node)
self.beginInsertRows(parent, row, row + rows - 1)
parent_node.append_child(new)
self.endInsertRows()
return True
# ------------------------------------------------------------[override]
# | removeRows()
# ----------------------------------------------------------------------
def removeRows(self, row, rows, parent=QtCore.QModelIndex()):
assert rows == 1
# Determine parent
if parent.internalPointer() is None:
parent_node = self.root
else:
parent_node = parent.internalPointer()
# Delete it from the parent
self.beginRemoveRows(parent, row, row + rows - 1)
parent_node.del_child(row)
self.endRemoveRows()
return True
# ------------------------------------------------------------------------------
# | pytest tester function |
# | |
# | |
# | |
# | |
# ------------------------------------------------------------------------------
def test_model(qtmodeltester):
qtmodeltester.check(CategoryTreeModel())