显然QAbstaractTableModel.dataChanged()信号没有效果

问题描述 投票:0回答:1

我正在编写一个相当大的脚本,其中有一个表,我需要操作添加和删除行以及更改一些值。

底层数据结构(强加给我,我无法更改它)是一个相当复杂的 XML,其中表行中显示的项目是固定数量,并且一个字段 (

num > 0
) 指示整个项目是否有效,因此应该显示,所以我使用
QSortFilterProxyModel
来过滤行。

下面是脚本的精简版本(它仍然太大,无法成为“最小示例”,但我希望它是可管理的;我不想删除太多实际的程序结构):

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 = []
        # self.select()

    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': 'magente', '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 = what
        self._inventory.changed.connect(self.changed)
        self._inventory.rowchanged.connect(self.rowchanged)
        self._inventory.rowinserted.connect(self.rowinserted)
        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)))

    @pyqtSlot(int)
    def rowinserted(self, index):
        self.beginInsertRows(QModelIndex(), index, index)
        self._rows = self._inventory.rows
        self.endInsertRows()


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 r, n in enumerate(self._store):
            if r == row:
                return n
        return None

    @pyqtProperty(int)
    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.rowinserted.emit(n)  # FIXME: this removes selection
                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)


    @pyqtSlot()
    def add(self):
        n = self.cb.currentIndex()
        self.storage_wrapper.add(n)


    app = QApplication([])
    win = MainWindow()
    pw = PW()
    win.set_storage_model(pw)
    win.show()

    from sys import exit
    exit(app.exec())

三个按钮的含义是:

  • [添加]:将上面组合中选择的项目添加到列表中(
    num = 1
    )。
  • [PLUS]:在所选行中将
    num
    加一(如果有)。
  • [减号]:所选行中的
    num
    减一(如果有);如果
    num goes to 
    0` 整行应该消失。

这有效......几乎。

主要问题是

[PLUS]
[MINUS]
操作并未立即反映在表格中(我需要通过切换焦点来强制刷新),尽管(显然是正确的)

self.dataChanged.emit(self.index(index, 0), self.index(index, len(self._columns)))

行删除也无法正常工作(

QSortFilterProxyModel.filterAcceptsRow()
似乎没有再次调用),所以我在某处遗漏了其他一些位。

model-view-controller refresh qtableview pyqt6 qmodelindex
1个回答
0
投票

当选择未正确保存时(包括当排序/过滤处于活动状态时)或数据未在视觉上更新时,这几乎总是意味着在某个地方提供了错误的 QModelIndex:项目视图具有自动机制来解决小模型不一致问题(防止致命错误) )但这些问题是无效索引的明显症状。

你的问题其实是由四个错误造成的:

  1. _index_of_row
    将行内容与枚举索引(而不是项目、字典的索引)进行比较;
  2. 即使比较有效,它也会返回“找到的”字典而不是行索引;
  3. 您正在更改比较之前行的值,从而使
    _index_of_row
    的匹配无效;
  4. dataChanged()
    使用无效的
    bottomRight
    索引,因为它基于不存在的列(
    len()
    而不是
    len() - 1
    );

以下是所需的更改:

class InventoryModel(AbstractModel):
    def rowchanged(self, index):
        self.dataChanged.emit(
            self.index(index, 0), 
            self.index(index, len(self._columns) - 1)
        )

...

class PW(QObject):
    def _index_of_row(self, row):
        for r, n in enumerate(self._store):
            if n == row:
                return r

    ...

    def row_inc(self, row, inc):
        num = self.row_num(row)
        n = num + inc
        rowNumber = self._index_of_row(row)
        if n > 0:
            row['num'] = n
            self.rowchanged.emit(rowNumber)
        else:
            row['num'] = 0
            self.rowdeleted.emit(rowNumber)

一个重要的建议,如果可以的话:非常仔细地考虑改进你的命名选择,因为短而模糊的名称会让调试变得比现在更加痛苦。

例如,您对

row
index
的使用令人困惑,因为您使用它们两者来指示 行索引,而前者还指示 行内容

使用极短的名称也无济于事。在小型上下文中(例如短函数中的 for 循环)可能是可以接受的,但您必须确保这些名称(或字母)清晰可辨且具有解释性,否则会出现诸如混淆

r 中的 
n
_index_of_row
 之类的错误
永远就在拐角处。
row_inc
是另一个类似的示例:
num
n
很难区分,即使它们引用相同的上下文,重要的是要弄清楚它们在代码中的用途。

极短的名称几乎没有什么好处(更少的打字,更短的代码行),但有很多缺点,而且根本没有性能改进。相反:如果与每次阅读它们时将其置于上下文中所花费的时间相比,您从中获得的收益几乎为零,尤其是在调试时(可能是在编码数小时之后),更不用说当“其他”人们必须这样做时阅读并理解您的代码。

© www.soinside.com 2019 - 2024. All rights reserved.