Search code examples
pyqt6

How do I filter QTableView with QSortFilterProxyModel using checked items AND text?


I'm working with TableViews in a Stacked Layout. The parent widget contains form controls (a lineEdit and a master checkbox) which manipulate the filters of the tableviews.

The master checkbox needs to be able to hide every checked item in the visible model.

How do I filter based on the CheckStateRole instead of the DisplayRole? In the code below, the third line from the bottom you will see a "what do I do here?", which is where I've lost the ability to understand any more of the documentation without concrete examples.

Current code:

class ShippingWindow(QWidget, Ui_shippingWidget):
    def __init__(self, saveData, objectData, parent=None):
        super().__init__(parent)
        self.setupUi(self)
        self.saveData = saveData
        self.objectData = objectData
        # set up our stacked layout. shipWidget, polyWidget and monoWidget will hold our tables.
        self.stackedLayout = QStackedLayout()
        self.shipWidget = QTableView()
        self.shipModel = QStandardItemModel()
        self.stackedLayout.addWidget(self.shipWidget)
        self.stackWidget = QWidget()
        self.stackWidget.setLayout(self.stackedLayout)
        self.stackedLayout.setCurrentWidget(self.shipWidget)
        self.verticalLayout.addWidget(self.stackWidget)
        # setup our contents
        self.populateShipping(saveData, objectData)
        # Attach the line edit to the Filter.
        self.filterTable.textChanged.connect(self.shippingFilterProxy.setFilterRegularExpression)
        # radio button logic for swapping currentWidget
        self.radioShip.toggled.connect(self.toggleShip)
        self.radioPoly.toggled.connect(self.togglePoly)
        self.radioMono.toggled.connect(self.toggleMono)
        # hide shipped checkbox logic
        self.hideShipped.stateChanged.connect(self.toggleShippedItems)

    def populateShipping(self, saveData, objectData):
        shippingBox = self.shipWidget
        excludedCats = ["Moved", "Tax-Exempt"]
        shippables = list(filter(None, [value if "SendOnly" in value and "FullShipment" in value["SendOnly"] and value["SendOnly"]["FullShipment"] and value["Category"] not in excludedCats else '' for value in objectData]))
        # sort by name
        sorted_shippables = sorted(shippables, key=lambda d: d["Name"])
        self.shipModel = QStandardItemModel(len(sorted_shippables), 5)
        self.shipModel.setHorizontalHeaderLabels(["Item", "Category", "Price", "Sources", "Developer"])
        shippingBox.verticalHeader().hide()
        # self.shipModel.setRowCount(len(shippables))
        row = 0
        for item in sorted_shippables:
            itemObj = SVObject(**item)
            nameCell = QStandardItem(itemObj.Name)
            nameCell.setFlags(QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEnabled)
            if itemObj.ID["objects"] in saveData["basicShipped"]:
                nameCell.setCheckState(QtCore.Qt.CheckState.Checked)
            else:
                nameCell.setCheckState(QtCore.Qt.CheckState.Unchecked)
            priceCell = QStandardItem()
            priceCell.setData(itemObj.Price, QtCore.Qt.ItemDataRole.DisplayRole)
            itemSources = list(item["Sources"].keys())
            sourceString = ", ".join(itemSources)
            sourceCell = QStandardItem(sourceString)
            self.shipModel.setItem(row, 0, nameCell)
            self.shipModel.setItem(row, 1, QStandardItem(itemObj.Category))
            self.shipModel.setItem(row, 2, priceCell)
            self.shipModel.setItem(row, 3, sourceCell)
            self.shipModel.setItem(row, 4, QStandardItem(itemObj.ModName))
            row += 1
        shippingBox.setSortingEnabled(True)
        # set up the filter proxy model
        self.shippingFilterProxy = QtCore.QSortFilterProxyModel()
        self.shippingFilterProxy.setSourceModel(self.shipModel)
        self.shippingFilterProxy.setFilterKeyColumn(0)
        self.shippingFilterProxy.setFilterRole(QtCore.Qt.ItemDataRole.DisplayRole)
        self.shippingFilterProxy.setFilterCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive)
        self.shipWidget.setModel(self.shippingFilterProxy)
        shippingBox.resizeColumnsToContents()

    def toggleShippedItems(self, s):
        if s == 2:
            if self.stackedLayout.currentIndex() == 0:
                # change the role to checkstate for a moment
                self.shippingFilterProxy.setFilterRole(QtCore.Qt.ItemDataRole.CheckStateRole)
                self.shippingFilterProxy.setFilterFixedString(2)   # what do I do here?
                # change the role back so the text filter works again?
                # self.shippingFilterProxy.setFilterRole(QtCore.Qt.ItemDataRole.DisplayRole)

Where I'm stuck is in toggleShippedItems at the very end. I've tried looping the models, they are non-iterable. I need to be able hide the checked items, keep them hidden, and still have the lineEdit search remain in effect. I'm much more comfortable with TableWidget, but since I need the text filter, I'm floundering out of my depth with TableView.


Solution

  • For those searching in the future, here is how I resolved the question on my own. It's cobbled together from PyQt4 and PyQt5 snippets updated to PyQt6, and C++ versions run through an automated C++ to Python translator.

    New populateShipping method:

        def populateShipping(self, saveData, objectData):
            shippingBox = self.shipWidget
            shippingBox.setStyleSheet("border: 5px ridge rgb(91, 43, 42);")
    
            excludedCats = ["Moved", "Tax-Exempt"]
            shippables = list(filter(None, [value if "SendOnly" in value and "FullShipment" in value["SendOnly"] and value["SendOnly"]["FullShipment"] and value["Category"] not in excludedCats else '' for value in objectData]))
            # sort by name
            sorted_shippables = sorted(shippables, key=lambda d: d["Name"])
            dataTable = []
            for item in sorted_shippables:
                dataRow = []
                # populate the dataTable here, snip snip
                dataTable.append(dataRow)
            labels = ["Name", "Category", "Price", "Sources", "Developer"]
    
            # set up the model and proxies
            self.shippingmodel = ShippablesTableModel(dataTable, labels)
    
            # check the boxes for any shipped items
            checkCol = 0
            for r in range(self.shippingmodel.rowCount()):
                itemID = sorted_shippables[r]["ID"]["objects"]
                checkIdx = self.shippingmodel.index(r, checkCol)
                if str(itemID) in self.saveData["basicShipped"]:
                    self.shippingmodel.setData(checkIdx, QtCore.Qt.CheckState.Checked, QtCore.Qt.ItemDataRole.CheckStateRole)
    
            # checkbox proxy filter
            self.shippedProxyModel = ShippingCheckProxy()
            self.shippedProxyModel.setSourceModel(self.shippingmodel)
            self.shippedProxyModel.setFilterFixedString("All")
    
            # search box proxy filter nests checkbox proxy filter
            self.shippeditemNameProxyModel = QtCore.QSortFilterProxyModel()
            self.shippeditemNameProxyModel.setSourceModel(self.shippedProxyModel)
            self.shippeditemNameProxyModel.setFilterKeyColumn(0)
            self.shippeditemNameProxyModel.setFilterCaseSensitivity(QtCore.Qt.CaseSensitivity.CaseInsensitive)
    
            # populate and design the actual table.
            shippingBox.setSortingEnabled(True)
            shippingBox.setModel(self.shippeditemNameProxyModel)
            shippingBox.verticalHeader().hide()
            shippingBox.horizontalHeader().setStyleSheet("border: none;")
            shippingBox.resizeColumnsToContents()
    

    Custom Table Model:

    class ShippablesTableModel(QtCore.QAbstractTableModel):
        def __init__(self, shippingData, headerNames, parent=None):
            super(ShippablesTableModel, self).__init__(parent)
            self.tableData = shippingData
            self.columnNames = headerNames
            self.checks = {}
    
        def columnCount(self, *args):
            return len(self.columnNames)
    
        def rowCount(self, *args):
            return len(self.tableData)
    
        def checkState(self, index):
            if index in self.checks.keys():
                return self.checks[index]
            else:
                return QtCore.Qt.CheckState.Unchecked
    
        def data(self, index, role=QtCore.Qt.ItemDataRole.DisplayRole):
            row = index.row()
            col = index.column()
            if role == QtCore.Qt.ItemDataRole.DisplayRole:
                value = self.tableData[row][col]
                return value
            elif role == QtCore.Qt.ItemDataRole.CheckStateRole and col == 0:
                return self.checkState(QtCore.QPersistentModelIndex(index))
            return None
    
        def setData(self, index, value, role=QtCore.Qt.ItemDataRole.EditRole):
            if not index.isValid():
                return False
            if role == QtCore.Qt.ItemDataRole.CheckStateRole:
                self.checks[QtCore.QPersistentModelIndex(index)] = value
                self.dataChanged.emit(index, index)
                return True
            return False
    
        def flags(self, index):
            fl = QtCore.QAbstractTableModel.flags(self, index)
            if index.column() == 0:
                fl = QtCore.Qt.ItemFlag.ItemIsUserCheckable | QtCore.Qt.ItemFlag.ItemIsEnabled
            return fl
    
        def headerData(self, col, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole):
            if role == QtCore.Qt.ItemDataRole.DisplayRole and orientation == QtCore.Qt.Orientation.Horizontal:
                return self.columnNames[col]
            return None
    

    Custom QSortFilterProxyModel:

    class ShippingCheckProxy(QtCore.QSortFilterProxyModel):
        def __init__(self, parent=None):
            super(ShippingCheckProxy, self).__init__(parent)
    
        def filterAcceptsRow(self, row_num, parent):
            model = self.sourceModel()
            shippedIdx = model.index(row_num, 0, parent)
            thisState = shippedIdx.data(QtCore.Qt.ItemDataRole.CheckStateRole)
            # we can only pass a regex through, but we only want the actual text 
            # (pattern), not a parsed regex.
            regex = self.filterRegularExpression()
            pattern = regex.pattern()
            # If the 0 pattern is submitted, only show rows with unchecked boxes.
            if pattern == "0":
                if thisState == QtCore.Qt.CheckState.Unchecked or thisState == 0:
                    return True
                else:
                    return False
            # otherwise show everything.
            return True