Re: QTreeView + sort + delete = crash

"John F Sturtz" <[email protected]>
Newsgroups gmane.comp.python.pyqt-pykde
Message-ID <[email protected]>
On 5/7/2019 12:21:24 PM, Kyle Altendorf <[email protected]> wrote:
Is this really all you do when you sort?

# Sort children
def sort_children(self):
self.children.sort(key=lambda x: x.desc())
return

Yes, that would upset Qt very much. You would need to do a series of
moves or a restructure or... I'm forgetting the terms offhand. But
yes, you have to tell Qt when stuff moves around. This would be part of
why it's useful to let an entirely stock Qt object like a proxy take
care of it for you.

Cheers,
-kyle

Problem solved, I think (haven't seen any issues or crashes in testing so far).

It seems you were right, Kyle -- a proxy was the way to go.  It took me some mucking around to eventually grok QSortFilterProxyModel well enough to hook it into my app.  Once I did, that seems to do the trick.

Thanks again for all the help Kyle and Florian!  Learning all the time ...

The modified file that seems to work is attached, in case anyone else was following.

/John 

_______________________________________________
PyQt mailing list    [email protected]
https://www.riverbankcomputing.com/mailman/listinfo/pyqt
cat.py (text/plain, 15.1 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, category_data, parent=None):
        super().__init__(parent)

        # 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()

        child_node = index.internalPointer()
        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()
    # ----------------------------------------------------------------------
    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)

        return True


    # ------------------------------------------------------------[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


# ------------------------------------------------------------------------------
# | Proxy                                                                      |
# |                                                                            |
# |                                                                            |
# |                                                                            |
# | QSortFilterProxyModel object                                               |
# ------------------------------------------------------------------------------
class Proxy(QtCore.QSortFilterProxyModel):

    def __init__(self, parent=None):
        super().__init__(parent)

    def setData(self, index, value, role=QtCore.Qt.EditRole):

        # Call source model's setData()
        super().setData(index, value, role)

        # Re-sort
        self.sort(0, QtCore.Qt.AscendingOrder)
        self.invalidate()



# ------------------------------------------------------------------------------
# | 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 and proxy
        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)
        ]
        self.model = CategoryTreeModel(category_data)
        self.proxy = Proxy()
        self.proxy.setSourceModel(self.model)
        self.treeView.setModel(self.proxy)

        # Set Delegate
        self.delegate = CategoryDelegate(self.treeView)
        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())


# ------------------------------------------------------------------------------
# | 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>
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.