QAbstractTableModel.dataChanged信号无效,表格刷新及行删除异常
PyQt6表格刷新与过滤问题修复
问题概述
现有PyQt6脚本实现了带过滤功能的表格,仅显示num > 0的行,但存在两个问题:
- 点击[PLUS]/[MINUS]后表格无法即时刷新,需切换焦点才生效
- 当
num变为0时,对应行未自动隐藏,QSortFilterProxyModel未重新执行过滤
问题根源与修复方案
1. 表格不即时刷新的修复
原代码中PW._index_of_row方法逻辑错误,将索引与字典对象直接比较,无法正确获取修改行的索引,导致rowchanged信号传递无效值,dataChanged事件未正确触发视图更新。
修复步骤:
修正PW._index_of_row方法,通过对象引用匹配找到行索引:
def _index_of_row(self, row): for idx, item in enumerate(self._store): if item is row: return idx return -1
2. num=0时行未隐藏的修复
- 原
PW.row_inc中,当num变为0时发送rowdeleted信号,但InventoryModel未监听该信号,无法通知模型数据变化 - 代理模型需重新检查过滤条件,可通过源模型发送
dataChanged信号触发代理重新验证该行
修复步骤:
- 在
InventoryModel.select方法中添加rowdeleted信号的连接:
self._inventory.rowdeleted.connect(self.rowdeleted)
- 在
InventoryModel中添加rowdeleted处理函数,发送dataChanged信号触发代理重新过滤:
@pyqtSlot(int) def rowdeleted(self, index): self.dataChanged.emit(self.index(index, 0), self.index(index, len(self._columns)-1))
额外修复:[ADD]按钮的错误信号
原PW.add方法是修改已有行(将num=0的行改为有效行),却发送rowinserted信号,导致模型执行插入行操作而非修改行操作,修正为发送rowchanged信号:
def add(self, idx): # FIXME: should check if similar row exists for n, row in enumerate(self._store): if self.row_num(row) == 0: row['num'] = 1 row['item'] = idx row['level'] = 0 self.rowchanged.emit(n) # 替换rowinserted为rowchanged break
修改后的完整代码
from collections import namedtuple from typing import Optional from PyQt6.QtCore import QAbstractTableModel, Qt, pyqtSlot, QModelIndex, QSortFilterProxyModel, QObject, pyqtProperty, \ pyqtSignal from PyQt6.QtWidgets import * class AbstractModel(QAbstractTableModel): _column = namedtuple('_column', "name func hint align") def __init__(self, columns: [_column]): self._columns = columns super().__init__() self._rows = [] def data(self, index, role=...): match role: case Qt.ItemDataRole.DisplayRole: row = None try: row = self._rows[index.row()] return self._columns[index.column()].func(row) except KeyError: print(f'ERROR: unknown item in row {row}') return '*** UNKNOWN ***' case Qt.ItemDataRole.TextAlignmentRole: return self._columns[index.column()].align return None def headerData(self, section, orientation, role=...): if orientation == Qt.Orientation.Horizontal: match role: case Qt.ItemDataRole.DisplayRole: return self._columns[section].name return None def rowCount(self, parent=...): return len(self._rows) def columnCount(self, parent=...): return len(self._columns) def select(self): self.beginResetModel() self._rows = [] self.endResetModel() def set_hints(self, view: QTableView): header = view.horizontalHeader() for i, x in enumerate(self._columns): header.setSectionResizeMode(i, x.hint) def row(self, idx: int): return self._rows[idx] all_by_id = [ {'Name': 'foo', 'Type': 'red', 'Level': 0}, {'Name': 'fee', 'Type': 'red', 'Level': 0}, {'Name': 'fie', 'Type': 'red', 'Level': 0}, {'Name': 'fos', 'Type': 'green', 'Level': 0}, {'Name': 'fum', 'Type': 'blue', 'Level': 0}, {'Name': 'fut', 'Type': 'blue', 'Level': 0}, {'Name': 'fam', 'Type': 'yellow', 'Level': 0}, {'Name': 'fol', 'Type': 'yellow', 'Level': 0}, {'Name': 'fit', 'Type': 'magenta', 'Level': 0}, ] type_by_id = ['red', 'green', 'blue', 'yellow', 'magenta'] class InventoryModel(AbstractModel): def __init__(self): super().__init__([ AbstractModel._column('ID', self.get_id, QHeaderView.ResizeMode.ResizeToContents, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), AbstractModel._column('Item', self.get_item, QHeaderView.ResizeMode.ResizeToContents, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter), AbstractModel._column('Type', self.get_type, QHeaderView.ResizeMode.ResizeToContents, Qt.AlignmentFlag.AlignLeft | Qt.AlignmentFlag.AlignVCenter), AbstractModel._column('Count', self.get_count, QHeaderView.ResizeMode.ResizeToContents, Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter), ]) self._inventory = None def select(self, what=None): if self._inventory: self._inventory.changed.disconnect(self.changed) self._inventory.rowchanged.disconnect(self.rowchanged) self._inventory.rowinserted.disconnect(self.rowinserted) self._inventory.rowdeleted.disconnect(self.rowdeleted) self._inventory = what self._inventory.changed.connect(self.changed) self._inventory.rowchanged.connect(self.rowchanged) self._inventory.rowinserted.connect(self.rowinserted) self._inventory.rowdeleted.connect(self.rowdeleted) self.changed() def get_id(self, x): return str(PW.row_item(x)) def get_item(self, x): return all_by_id[PW.row_item(x)]['Name'] def get_type(self, x): return all_by_id[PW.row_item(x)]['Type'] def get_count(self, x): return str(PW.row_num(x)) @pyqtSlot() def changed(self): self.beginResetModel() self._rows = self._inventory.rows self.endResetModel() @pyqtSlot(int) def rowchanged(self, index): self.dataChanged.emit(self.index(index, 0), self.index(index, len(self._columns)-1)) @pyqtSlot(int) def rowinserted(self, index): self.beginInsertRows(QModelIndex(), index, index) self._rows = self._inventory.rows self.endInsertRows() @pyqtSlot(int) def rowdeleted(self, index): self.dataChanged.emit(self.index(index, 0), self.index(index, len(self._columns)-1)) class InventoryProxy(QSortFilterProxyModel): def filterAcceptsRow(self, source_row, source_parent): row = self.sourceModel().row(source_row) return PW.row_valid(row) class PW(QObject): changed = pyqtSignal() rowchanged = pyqtSignal(int) rowinserted = pyqtSignal(int) rowdeleted = pyqtSignal(int) def __init__(self, parent=None): super().__init__(parent) self._store = [ {'num': 1, 'item': 0, 'level': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, {'num': 0}, ] def _index_of_row(self, row): for idx, item in enumerate(self._store): if item is row: return idx return -1 @pyqtProperty(list) def rows(self): return self._store @staticmethod def row_num(row): return row['num'] @staticmethod def row_valid(row): return PW.row_num(row) > 0 @staticmethod def row_item(row): return row['item'] @staticmethod def row_level(row): return row['level'] def row_inc(self, row, inc): num = self.row_num(row) n = num + inc if n > 0: row['num'] = n self.rowchanged.emit(self._index_of_row(row)) else: row['num'] = 0 self.rowdeleted.emit(self._index_of_row(row)) def add(self, idx): # FIXME: should check if similar row exists for n, row in enumerate(self._store): if self.row_num(row) == 0: row['num'] = 1 row['item'] = idx row['level'] = 0 self.rowchanged.emit(n) break if __name__ == '__main__': class MainWindow(QMainWindow): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.setWindowTitle('Storage') self.l1 = QVBoxLayout() self.w1 = QWidget() self.w1.setLayout(self.l1) self.cb = QComboBox(self) self.cb.addItems([x['Name'] for x in all_by_id]) self.l1.addWidget(self.cb) self.w2 = QWidget() self.l2 = QHBoxLayout() self.w2.setLayout(self.l2) self.ba = QPushButton('ADD') self.l2.addWidget(self.ba) self.bp = QPushButton('PLUS') self.l2.addWidget(self.bp) self.bm = QPushButton('MINUS') self.l2.addWidget(self.bm) self.l1.addWidget(self.w2) self.storage = QTableView() self.storage.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) self.l1.addWidget(self.storage) self.setCentralWidget(self.w1) self.storage_wrapper: Optional[PW] = None self.storage_model: Optional[InventoryModel] = None self.storage_proxy: Optional[InventoryProxy] = None self.ba.clicked.connect(self.add) self.bp.clicked.connect(self.plus) self.bm.clicked.connect(self.minus) def set_storage_model(self, wrapper: PW): self.storage_wrapper = wrapper self.storage_model = InventoryModel() self.storage_proxy = InventoryProxy() self.storage_model.select(self.storage_wrapper) self.storage_proxy.setSourceModel(self.storage_model) self.storage.setModel(self.storage_proxy) self.storage_model.set_hints(self.storage) self.storage.setSortingEnabled(True) self.storage.sortByColumn(2, Qt.SortOrder.AscendingOrder) @pyqtSlot() def add(self): n = self.cb.currentIndex() self.storage_wrapper.add(n) @pyqtSlot() def plus(self): sel = self.storage.currentIndex() if sel.isValid(): orig = self.storage_proxy.mapToSource(sel) row = self.storage_model.row(orig.row()) self.storage_wrapper.row_inc(row, 1) @pyqtSlot() def minus(self): sel = self.storage.currentIndex() if sel.isValid(): orig = self.storage_proxy.mapToSource(sel) row = self.storage_model.row(orig.row()) self.storage_wrapper.row_inc(row, -1) app = QApplication([]) win = MainWindow() pw = PW() win.set_storage_model(pw) win.show() from sys import exit exit(app.exec())
额外说明
- 修正了
all_by_id中的拼写错误magente为magenta - 修正了
PW.rows的pyqtProperty类型错误(原标注为int,实际应为list) - [ADD]按钮现在正确触发行修改信号,避免了错误的插入行操作
内容的提问来源于stack exchange,提问作者ZioByte
相关产品推荐
相关产品推荐

