Re: QTreeView + sort + delete = crash
"John F Sturtz" <[email protected]>
| Newsgroups | gmane.comp.python.pyqt-pykde |
|---|---|
| Message-ID | <[email protected]> |
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). /John _______________________________________________ PyQt mailing list [email protected] https://www.riverbankcomputing.com/mailman/listinfo/pyqt
img1.png
(image/png, 21.1 KB) - not displayed
img2.png
(image/png, 20.1 KB) - not displayed
cat.py
(text/plain, 15.6 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, view, parent=None):
super().__init__(parent)
self.view = view
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
# ------------------------------------------------------------------------------
# | TreeView |
# | |
# | |
# | |
# | QTreeView object |
# ------------------------------------------------------------------------------
class TreeView(QtWidgets.QTreeView):
def __init__(self, parent=None):
super().__init__(parent)
# ------------------------------------------------------------------------------
# | Dlg |
# | |
# | |
# | |
# | Dialog widget (defined in dlg.ui) |
# ------------------------------------------------------------------------------
dlg_base, dlg_form = uic.loadUiType('dlg.ui')
class Dlg(dlg_base, dlg_form):
def __init__(self, parent=None):
super(dlg_base, self).__init__(parent)
self.setupUi(self)
self.setWindowFlags(QtCore.Qt.Dialog|QtCore.Qt.FramelessWindowHint)
self.treeView.setHeaderHidden(True)
self.treeView.setAnimated(True)
# Set model
self.model = CategoryTreeModel(self.treeView)
self.treeView.setModel(self.model)
# Set Delegate
self.delegate = CategoryDelegate()
self.treeView.setItemDelegate(self.delegate)
# Connect buttons
self.addMainButton.clicked.connect(self.add_main)
self.addSubButton.clicked.connect(self.add_sub)
self.deleteButton.clicked.connect(self.delete)
self.doneButton.clicked.connect(quit)
def keyPressEvent(self, event):
key = event.key()
mod = int(event.modifiers())
if key == QtCore.Qt.Key_Q and mod == QtCore.Qt.CTRL:
exit()
def add_main(self):
print('Add Main')
view = self.treeView
model = view.model()
model.insertRows(model.rowCount(), 1)
index = model.index(model.rowCount() - 1, 0)
view.setCurrentIndex(index)
view.scrollTo(index, QtWidgets.QAbstractItemView.EnsureVisible)
view.edit(index)
def add_sub(self):
print('Add Sub')
view = self.treeView
model = view.model()
current = view.currentIndex()
if model.parent(current).internalPointer():
add_to = model.parent(current)
else:
add_to = current
model.insertRows(model.rowCount(add_to), 1, add_to)
index = model.index(model.rowCount(add_to) - 1, 0, add_to)
view.setCurrentIndex(index)
view.scrollTo(index, QtWidgets.QAbstractItemView.EnsureVisible)
view.edit(index)
def delete(self):
print('Delete')
view = self.treeView
model = view.model()
current = view.currentIndex()
model.removeRows(current.row(), 1, model.parent(current))
# ------------------------------------------------------------------------------
# | CategoryDelegate |
# | |
# | |
# | |
# | Delegate for editing |
# ------------------------------------------------------------------------------
class CategoryDelegate(QtWidgets.QStyledItemDelegate):
def __init__(self, parent=None):
super().__init__(parent)
def createEditor(self, parent, option, index):
self.editor = QtWidgets.QLineEdit(parent)
return self.editor
def setEditorData(self, editor, index):
pass
def updateEditorGeometry(self, editor, option, index):
editor.setGeometry(option.rect)
def setModelData(self, editor, model, index):
model.setData(index, editor.text())
# ------------------------------------------------------------------------------
# | pytest tester function |
# | |
# | |
# | |
# | |
# ------------------------------------------------------------------------------
def test_model(qtmodeltester):
dlg = Dlg()
qtmodeltester.check(dlg.model)
# ------------------------------------------------------------------------------
# | main() |
# | |
# | |
# | |
# | |
# ------------------------------------------------------------------------------
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
app.setStyle('Fusion')
dlg = Dlg()
dlg.show()
sys.exit(app.exec_())
dlg.ui
(text/xml, 4.2 KB)
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CategoryDialog</class>
<widget class="QDialog" name="CategoryDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>381</width>
<height>423</height>
</rect>
</property>
<property name="font">
<font>
<family>Corbel</family>
<pointsize>10</pointsize>
</font>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<widget class="QWidget" name="layoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>361</width>
<height>401</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="TreeView" name="treeView">
<property name="font">
<font>
<family>Corbel</family>
<pointsize>10</pointsize>
</font>
</property>
<property name="cursor" stdset="0">
<cursorShape>PointingHandCursor</cursorShape>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="addMainButton">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Main</string>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="addSubButton">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Sub</string>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<widget class="QToolButton" name="deleteButton">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Delete</string>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="doneButton">
<property name="font">
<font>
<pointsize>9</pointsize>
</font>
</property>
<property name="text">
<string>Done</string>
</property>
<property name="iconSize">
<size>
<width>40</width>
<height>40</height>
</size>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonTextUnderIcon</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<customwidgets>
<customwidget>
<class>TreeView</class>
<extends>QTreeView</extends>
<header>cat.h</header>
</customwidget>
</customwidgets>
<resources/>
<connections/>
</ui>