我正在编写一个相当大的脚本,其中有一个表,我需要操作添加和删除行以及更改一些值。
底层数据结构(强加给我,我无法更改它)是一个相当复杂的 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
)。num
加一(如果有)。num
减一(如果有);如果 num goes to
0` 整行应该消失。这有效......几乎。
主要问题是
[PLUS]
和 [MINUS]
操作并未立即反映在表格中(我需要通过切换焦点来强制刷新),尽管(显然是正确的)
self.dataChanged.emit(self.index(index, 0), self.index(index, len(self._columns)))
行删除也无法正常工作(
QSortFilterProxyModel.filterAcceptsRow()
似乎没有再次调用),所以我在某处遗漏了其他一些位。
当选择未正确保存时(包括当排序/过滤处于活动状态时)或数据未在视觉上更新时,这几乎总是意味着在某个地方提供了错误的 QModelIndex:项目视图具有自动机制来解决小模型不一致问题(防止致命错误) )但这些问题是无效索引的明显症状。
你的问题其实是由四个错误造成的:
_index_of_row
将行内容与枚举索引(而不是项目、字典的索引)进行比较;_index_of_row
的匹配无效;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
很难区分,即使它们引用相同的上下文,重要的是要弄清楚它们在代码中的用途。
极短的名称几乎没有什么好处(更少的打字,更短的代码行),但有很多缺点,而且根本没有性能改进。相反:如果与每次阅读它们时将其置于上下文中所花费的时间相比,您从中获得的收益几乎为零,尤其是在调试时(可能是在编码数小时之后),更不用说当“其他”人们必须这样做时阅读并理解您的代码。