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

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信号触发代理重新验证该行

修复步骤:

  1. 在InventoryModel.select方法中添加rowdeleted信号的连接:
self._inventory.rowdeleted.connect(self.rowdeleted)
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 19:22:04