如何结合复选框状态与文本过滤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
相关产品推荐
相关产品推荐

