You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何结合复选框状态与文本过滤QTableView(QSortFilterProxyModel)

问题

在堆叠布局中使用QTableView,父控件包含用于表格过滤的LineEdit和主复选框。需求是:主复选框勾选时隐藏所有已勾选的表格项,同时保留LineEdit的文本搜索功能。

当前卡在toggleShippedItems方法中,无法基于CheckStateRole实现过滤,原代码中标记what do I do here?的位置不知道如何处理。原代码如下:

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()
        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)
解决方案

直接切换FilterRole会导致文本过滤和复选框过滤互斥,无法同时生效。正确的做法是自定义QSortFilterProxyModel子类,重写filterAcceptsRow方法,同时处理两种过滤条件。

1. 自定义ProxyModel类

添加一个控制是否隐藏已勾选项目的属性,并重写过滤逻辑:

class CustomFilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._hide_checked = False  # 控制是否隐藏已勾选项

    def set_hide_checked(self, hide):
        self._hide_checked = hide
        self.invalidateFilter()  # 触发重新过滤

    def filterAcceptsRow(self, source_row, source_parent):
        # 先执行父类的文本过滤逻辑
        text_accepted = super().filterAcceptsRow(source_row, source_parent)
        if not text_accepted:
            return False

        # 如果不需要隐藏已勾选项,直接返回文本过滤结果
        if not self._hide_checked:
            return True

        # 获取当前行第0列的复选框状态
        index = self.sourceModel().index(source_row, 0, source_parent)
        check_state = index.data(QtCore.Qt.ItemDataRole.CheckStateRole)
        # 返回未勾选的项(即隐藏已勾选的)
        return check_state != QtCore.Qt.CheckState.Checked

2. 修改原代码中的ProxyModel初始化

在populateShipping方法中,替换默认的ProxyModel为自定义类:

# 替换原有的proxy model初始化代码
self.shippingFilterProxy = CustomFilterProxyModel()
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)

3. 修改toggleShippedItems方法

不需要切换FilterRole,只需调用自定义ProxyModel的set_hide_checked方法:

def toggleShippedItems(self, s):
    if self.stackedLayout.currentIndex() == 0:
        # s == 2表示复选框被勾选,需要隐藏已勾选项
        self.shippingFilterProxy.set_hide_checked(s == QtCore.Qt.CheckState.Checked)

4. 保留文本过滤功能

原有的LineEdit连接逻辑无需修改,自定义ProxyModel继承了父类的文本过滤逻辑,会自动和复选框过滤条件叠加生效。

完整修改后的代码

from PyQt6 import QtCore, QtWidgets
# 假设SVObject和Ui_shippingWidget已正确导入

class CustomFilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._hide_checked = False

    def set_hide_checked(self, hide):
        self._hide_checked = hide
        self.invalidateFilter()

    def filterAcceptsRow(self, source_row, source_parent):
        text_accepted = super().filterAcceptsRow(source_row, source_parent)
        if not text_accepted:
            return False

        if not self._hide_checked:
            return True

        index = self.sourceModel().index(source_row, 0, source_parent)
        check_state = index.data(QtCore.Qt.ItemDataRole.CheckStateRole)
        return check_state != QtCore.Qt.CheckState.Checked

class ShippingWindow(QtWidgets.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 = QtWidgets.QStackedLayout()
        self.shipWidget = QtWidgets.QTableView()
        self.shipModel = QtCore.QStandardItemModel()
        self.stackedLayout.addWidget(self.shipWidget)
        self.stackWidget = QtWidgets.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 = QtCore.QStandardItemModel(len(sorted_shippables), 5)
        self.shipModel.setHorizontalHeaderLabels(["Item", "Category", "Price", "Sources", "Developer"])
        shippingBox.verticalHeader().hide()
        row = 0
        for item in sorted_shippables:
            itemObj = SVObject(**item)
            nameCell = QtCore.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 = QtCore.QStandardItem()
            priceCell.setData(itemObj.Price, QtCore.Qt.ItemDataRole.DisplayRole)
            itemSources = list(item["Sources"].keys())
            sourceString = ", ".join(itemSources)
            sourceCell = QtCore.QStandardItem(sourceString)
            self.shipModel.setItem(row, 0, nameCell)
            self.shipModel.setItem(row, 1, QtCore.QStandardItem(itemObj.Category))
            self.shipModel.setItem(row, 2, priceCell)
            self.shipModel.setItem(row, 3, sourceCell)
            self.shipModel.setItem(row, 4, QtCore.QStandardItem(itemObj.ModName))
            row += 1
        shippingBox.setSortingEnabled(True)
        # set up the filter proxy model
        self.shippingFilterProxy = CustomFilterProxyModel()
        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 self.stackedLayout.currentIndex() == 0:
            self.shippingFilterProxy.set_hide_checked(s == QtCore.Qt.CheckState.Checked)

    # 补充toggleShip、togglePoly、toggleMono方法(原代码未实现,需自行补充)
    def toggleShip(self, checked):
        if checked:
            self.stackedLayout.setCurrentWidget(self.shipWidget)
    def togglePoly(self, checked):
        if checked:
            # 切换到polyWidget逻辑
            pass
    def toggleMono(self, checked):
        if checked:
            # 切换到monoWidget逻辑
            pass

关键说明

  • 自定义ProxyModel通过filterAcceptsRow同时处理文本过滤和复选框过滤,两个条件是与的关系:只有文本过滤通过且符合复选框状态要求的行才会显示。
  • 调用invalidateFilter()会触发ProxyModel重新计算所有行的显示状态,确保修改过滤条件后界面实时更新。
  • 保留了原有的文本过滤逻辑,LineEdit的搜索功能不受影响。

内容的提问来源于stack exchange,提问作者The Onion Ninja

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.06 23:07:10