Search code examples
pythonpysideqtreeview

Widgets Disappear From QTreeview


Why do my comboboxes disappear from the treeview after clear the search filter field?

Starting application looks like this: enter image description here

I then search using the QLineEdit which works as expected: enter image description here

I then clear the search field and all my comoboboxes are gone? enter image description here

import os, sys, pprint
sys.path.append(os.environ.get('PS_SITEPACKAGES'))
from Qt import QtGui, QtWidgets, QtCore


class VersionProxyModel(QtCore.QSortFilterProxyModel):

    def __init__(self, *args, **kwargs):
        super(VersionProxyModel, self).__init__(*args, **kwargs)
        self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
        self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)

    def checkParents(self, index):
        while (index.isValid()):
            if super(VersionProxyModel, self).filterAcceptsRow(index.row(), index.parent()):
                return True
            index = index.parent()
        return False

    def checkChildren(self, index):
        for i in range(0, self.sourceModel().rowCount(index)):
            if super(VersionProxyModel, self).filterAcceptsRow(i, index):
                return True

        # recursive
        for i in range(0, self.sourceModel().rowCount(index)):
            self.checkChildren(self.sourceModel().index(i, 0, index))

        return False 

    def filterAcceptsRow(self, source_row, parent):
        if super(VersionProxyModel, self).filterAcceptsRow(source_row, parent):
            return True

        if self.checkChildren(self.sourceModel().index(source_row, 0, parent)):
            return True

        return self.checkParents(parent)


class Window(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(Window, self).__init__(parent)
        self.resize(800, 400)

        self.uiSearch = QtWidgets.QLineEdit()

        self.versionModel = QtGui.QStandardItemModel()

        self.versionProxyModel = VersionProxyModel()
        self.versionProxyModel.setSourceModel(self.versionModel)
        self.versionProxyModel.setDynamicSortFilter(True)

        self.uiVersionTreeView = QtWidgets.QTreeView()
        self.uiVersionTreeView.sortByColumn(0, QtCore.Qt.AscendingOrder)
        self.uiVersionTreeView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
        self.uiVersionTreeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        self.uiVersionTreeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        self.uiVersionTreeView.setModel(self.versionProxyModel)
        self.uiVersionTreeView.setRootIsDecorated(False)

        # layout
        self.layout = QtWidgets.QVBoxLayout()
        self.layout.addWidget(self.uiSearch)
        self.layout.addWidget(self.uiVersionTreeView)
        self.setLayout(self.layout)

        # signals/slots
        self.uiSearch.textChanged.connect(self.searchFilterChanged)
        self.populate()


    def populate(self):
        sortColumn = self.uiVersionTreeView.header().sortIndicatorSection()
        sortDirection = self.uiVersionTreeView.header().sortIndicatorOrder()
        self.versionModel.clear()
        self.uiVersionTreeView.setSortingEnabled(False)
        self.versionModel.setHorizontalHeaderLabels(['Entity', 'Type', 'Name', 'Versions'])

        versions = {   
            'Leslie': [
                    {'fullname': 'medic_skin_v001', 'name': 'Medic', 'type': 'Bulky'}
                ],
            'Mike': [ 
                    {'fullname': 'tech_skin_v001', 'name': 'Tech', 'type': 'Average'},
                    {'fullname': 'tech_skin_v002', 'name': 'Master', 'type': 'Average'}
                ],
            'Michelle': [
                    {'fullname': 'warrior_skin_v001', 'name': 'Warrior', 'type': 'Athletic'},
                    {'fullname': 'warrior_skin_v002', 'name': 'Warrior', 'type': 'Athletic'},
                    {'fullname': 'warrior_skin_v003', 'name': 'Warrior', 'type': 'Athletic'}]
            }

        for key, values in versions.items():
            col1 = QtGui.QStandardItem(values[0]['name'])
            col2 = QtGui.QStandardItem(values[0]['type'])
            col3 = QtGui.QStandardItem(key)
            col4 = QtGui.QStandardItem()
            self.versionModel.appendRow([col1, col2, col3, col4])

            # set data 
            col2.setData(QtGui.QColor(80,150,200), role=QtCore.Qt.ForegroundRole)

            combo = QtWidgets.QComboBox()
            for x in values:
                combo.addItem(x['fullname'], x)

            mIndex = self.versionProxyModel.mapFromSource(col4.index())
            self.uiVersionTreeView.setIndexWidget(mIndex, combo)

        # Restore
        self.uiVersionTreeView.setSortingEnabled(True)
        self.uiVersionTreeView.setSortingEnabled(True)
        self.uiVersionTreeView.sortByColumn(sortColumn, sortDirection)
        self.uiVersionTreeView.expandAll()
        for i in range(self.versionModel.columnCount()):
            self.uiVersionTreeView.resizeColumnToContents(i)


    def searchFilterChanged(self, text):
        self.versionProxyModel.setFilterWildcard(text)


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    ex = Window()
    ex.show()
    app.exec_()

Solution

  • If you check the logic of your code you will see that the combobox is highly related to the QModelIndex, that is, if the QModelIndex disappears, so will the QComboBox. In the case of a QSortFilterProxyModel when doing the filtering is eliminating and creating QModelIndex so consequently also eliminates the QComboBox, and they will not be restored, a possible solution is to track the deletion but that is very complicated. Another best solution is to use a delegate that provides a QComboBox as editor, and these QComboBox are created on demand.

    import os, sys, pprint
    sys.path.append(os.environ.get('PS_SITEPACKAGES'))
    from Qt import QtGui, QtWidgets, QtCore   
    
    class VersionProxyModel(QtCore.QSortFilterProxyModel):
        def __init__(self, *args, **kwargs):
            super(VersionProxyModel, self).__init__(*args, **kwargs)
            self.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive)
            self.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive)
    
        def checkParents(self, index):
            while (index.isValid()):
                if super(VersionProxyModel, self).filterAcceptsRow(index.row(), index.parent()):
                    return True
                index = index.parent()
            return False
    
        def checkChildren(self, index):
            for i in range(0, self.sourceModel().rowCount(index)):
                if super(VersionProxyModel, self).filterAcceptsRow(i, index):
                    return True
    
            # recursive
            for i in range(0, self.sourceModel().rowCount(index)):
                self.checkChildren(self.sourceModel().index(i, 0, index))
    
            return False 
    
        def filterAcceptsRow(self, source_row, parent):
            if super(VersionProxyModel, self).filterAcceptsRow(source_row, parent):
                return True
    
            if self.checkChildren(self.sourceModel().index(source_row, 0, parent)):
                return True
    
            return self.checkParents(parent)
    
    
    class ComboBoxDelegate(QtWidgets.QStyledItemDelegate):
        def paint(self, painter, option, index):
            if isinstance(self.parent(), QtWidgets.QAbstractItemView):
                 self.parent().openPersistentEditor(index)
            super(ComboBoxDelegate, self).paint(painter, option, index)
    
        def createEditor(self, parent, option, index):
            editor = QtWidgets.QComboBox(parent)
            editor.currentIndexChanged.connect(self.commitEditor)
            return editor
    
        @QtCore.Slot()
        def commitEditor(self):
            editor = self.sender()
            self.commitData.emit(editor)
            if isinstance(self.parent(), QtWidgets.QAbstractItemView):
                self.parent().updateEditorGeometries()
    
        def setEditorData(self, editor, index):
            values = index.data(QtCore.Qt.UserRole + 100)
            val = index.data(QtCore.Qt.UserRole + 101)
            editor.clear()
            for i, x in enumerate(values):
                editor.addItem(x['fullname'], x)
                if val['fullname'] == x['fullname']:
                    editor.setCurrentIndex(i)
    
        def setModelData(self, editor, model, index):
            values = index.data(QtCore.Qt.UserRole + 100)
            ix = editor.currentIndex()
            model.setData(index, values[ix] , QtCore.Qt.UserRole + 101)
    
        def updateEditorGeometry(self, editor, option, index):
            editor.setGeometry(option.rect)
    
    class Window(QtWidgets.QDialog):
        def __init__(self, parent=None):
            super(Window, self).__init__(parent)
            self.resize(800, 400)
    
            self.uiSearch = QtWidgets.QLineEdit()
            self.versionModel = QtGui.QStandardItemModel()
            self.versionProxyModel = VersionProxyModel()
            self.versionProxyModel.setSourceModel(self.versionModel)
            self.versionProxyModel.setDynamicSortFilter(True)
            self.uiVersionTreeView = QtWidgets.QTreeView()
            self.uiVersionTreeView.sortByColumn(0, QtCore.Qt.AscendingOrder)
            self.uiVersionTreeView.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
            self.uiVersionTreeView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
            self.uiVersionTreeView.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
            self.uiVersionTreeView.setModel(self.versionProxyModel)
            self.uiVersionTreeView.setRootIsDecorated(False)
            delegate = ComboBoxDelegate(self.uiVersionTreeView)
            self.uiVersionTreeView.setItemDelegateForColumn(3, delegate)
            # layout
            self.layout = QtWidgets.QVBoxLayout()
            self.layout.addWidget(self.uiSearch)
            self.layout.addWidget(self.uiVersionTreeView)
            self.setLayout(self.layout)
            # signals/slots
            self.uiSearch.textChanged.connect(self.versionProxyModel.setFilterWildcard)
            self.populate()
    
    
        def populate(self):
            sortColumn = self.uiVersionTreeView.header().sortIndicatorSection()
            sortDirection = self.uiVersionTreeView.header().sortIndicatorOrder()
            self.versionModel.clear()
            self.uiVersionTreeView.setSortingEnabled(False)
            self.versionModel.setHorizontalHeaderLabels(['Entity', 'Type', 'Name', 'Versions'])
    
            versions = {   
                'Leslie': [
                        {'fullname': 'medic_skin_v001', 'name': 'Medic', 'type': 'Bulky'}
                    ],
                'Mike': [ 
                        {'fullname': 'tech_skin_v001', 'name': 'Tech', 'type': 'Average'},
                        {'fullname': 'tech_skin_v002', 'name': 'Master', 'type': 'Average'}
                    ],
                'Michelle': [
                        {'fullname': 'warrior_skin_v001', 'name': 'Warrior', 'type': 'Athletic'},
                        {'fullname': 'warrior_skin_v002', 'name': 'Warrior', 'type': 'Athletic'},
                        {'fullname': 'warrior_skin_v003', 'name': 'Warrior', 'type': 'Athletic'}]
                }
    
            for key, values in versions.items():
                col1 = QtGui.QStandardItem(values[0]['name'])
                col2 = QtGui.QStandardItem(values[0]['type'])
                col3 = QtGui.QStandardItem(key)
                col4 = QtGui.QStandardItem()
                self.versionModel.appendRow([col1, col2, col3, col4])
                col2.setData(QtGui.QColor(80,150,200), role=QtCore.Qt.ForegroundRole)
    
                col4.setData(values, QtCore.Qt.UserRole + 100)
                col4.setData(values[0], QtCore.Qt.UserRole + 101)
            # Restore
            self.uiVersionTreeView.setSortingEnabled(True)
            self.uiVersionTreeView.sortByColumn(sortColumn, sortDirection)
            self.uiVersionTreeView.expandAll()
            for i in range(self.versionModel.columnCount()):
                self.uiVersionTreeView.resizeColumnToContents(i)
    
    if __name__ == '__main__':
        app = QtWidgets.QApplication(sys.argv)
        ex = Window()
        ex.show()
        app.exec_()