PySide6 QTableView提交数据警告
我在使用PySide6的 QtWidgets.QTableView,并复现按下回车后跳到下一单元格的功能,这与大多数电子表格的行为类似。我对QtWidgets.QTableView进行了子类化,以修改keyPressEvent。这个子类工作正常。我可以在选中的单元格输入数据,例如第0 列第1 行,当我按回车时,光标会跳到第2 行。
然而,我收到了如下警告:“QAbstractItemView::commitData called with an editor that does not belong to this view”。我已经把问题定位在子类中的self.setCurrentIndex方法上,但我仍然无法确定如何解决这个警告。
class CustomTableView(QtWidgets.QTableView):
def __init__(self):
super().__init__()
header = self.horizontalHeader()
header.setStyleSheet("QHeaderView::section { background-color: lightgray; border: 1px solid black; }")
row = self.verticalHeader()
row.setStyleSheet("QHeaderView::section { background-color: lightgray; border: 1px solid black; }")
row.setDefaultAlignment(Qt.AlignCenter)
def keyPressEvent(self, event):
if event.key() == Qt.Key_Return: # Check if the ENTER key is pressed
current_index = self.currentIndex()
if current_index.isValid():
# Move to the next cell in the same column
next_row = current_index.row() + 1
if next_row < self.model().rowCount():
self.setCurrentIndex(self.model().index(next_row, current_index.column()))
else:
# Optionally, wrap around to the first row
self.setCurrentIndex(self.model().index(0, current_index.column()))
event.accept() # Accept the event
else:
super().keyPressEvent(event) # Call the base class implementation
class Main(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(WINTITLE)
self.table = CustomTableView()
df = pd.read_csv(CSVFILE, header=None, low_memory=False)
self.model = TableModel(df)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
解决方案
你可以忽略它
警告不是错误:有时它们可以(或应当)被忽略。
这正是那种情况。
你看到的警告是由于 [commitData()] 和 [closeEditor()] 函数(QAbstractItemView的一部分,QTableView也是基于它)的内部两次调用造成的,当第二次调用发生时,编辑器已不再与某个索引相关联。
Qt对无法被满足的调用相对宽容(只要它们不导致严重错误),且无法知道这些“重复”调用的上下文,因此它只是发出一个警告并直接返回,不执行任何操作。
为什么会这样
Qt项目视图的默认委托是 QStyledItemDelegate,它实现了一个[事件过滤器]([event filter])([eventFilter()],尽管Qt 6.10引入了 [handleEditorEvent()],以统一对任何QAbstractItemDelegate的处理)。
在打开编辑器时,视图会自动将委托安装为事件过滤器,该过滤器会在事件被转发给编辑器之前先捕获所有目标给编辑器的事件,之后再决定要不要处理、让编辑器处理、同时处理,或什么也不做。
按键 Return(类似于数字小键盘上的 Enter)的处理是由委托的事件过滤器内部完成的,随后它会指示视图提交编辑器的数据,并在必要时通过发出其委托信号来关闭编辑器:[commitData()] 和 [closeEditor()]。
当视图接收到这些信号时,它会调用相应的视图函数:[commitData()] 会查找与给定编辑器相关联的 QModelIndex,并尝试将该索引在模型中的数据保存下来;而 [closeEditor()] 会尝试隐藏并删除编辑器(如可能),并根据 hint 参数决定后续的处理。
需要注意的是,上述委托信号并不是即时发出的:在这个特定案例中,委托实际通过一个带有队列连接的内部方法来触发这些信号,这意味着相关信号只有在当前事件处理完成后才会发出。
因此,当你的 keyPressEvent() 覆盖收到该事件时,编辑器对该索引而言仍然是活跃且有效的。
在此时修改当前索引的结果是,视图的 [currentChanged()] 会立即被调用,随后它会检查该 previous 索引是否仍有关联的编辑器;如果有(此时仍然有),它会对其“立刻”调用 commitData() 和 closeEditor()。之后,索引和编辑器不再“成对”,编辑器也被隐藏并标记为将来删除。
事件处理完成后,上述委托的队列方法终于被调用:信号将被发出,视图将尝试对编辑器仍然“为Qt所知”但不再与某个索引编辑器相关联的状态,调用 commitData() 和 closeEditor()。
由于编辑器不再与任何索引关联,Qt会发出警告并返回。对 closeEditor() 也存在类似警告,但只有编辑器仍然可见时才会出现(在上述情形中并不存在)。
可能的解决方案
如前所述,你可以安全地忽略该警告:你现在的做法并不“完全错误”,如果你对某些你知道并非关键的警告可以接受,就保持现状即可。
然而,重复调用并不是好事,未来的实现中也可能引发一些问题。
最简单的解决方案是使用自定义委托,在一个 eventFilter() 覆盖中直接忽略默认行为:
class MyDelegate(QStyledItemDelegate):
def eventFilter(self, editor, event):
if (
event.type() == QEvent.Type.KeyPress
and event.key() == Qt.Key.Key_Return
):
event.ignore()
return True
return super().eventFilter(editor, event)
class CustomTableView(QTableView):
def __init__(self):
...
self.setItemDelegate(MyDelegate(self))
def keyPressEvent(self, event):
if (
event.key() == Qt.Key.Key_Return
or event.key() == Qt.Key.Key_Enter # number pad
):
current = self.currentIndex()
if current.isValid():
next_row = current.row() + 1
if next_row >= self.model().rowCount():
next_row = 0
self.setCurrentIndex(current.siblingAtRow(next_row))
# no need to call event.accept() as it's accepted by default
else:
super().keyPressEvent(event)
一个常见的需求是在按下 Return / Enter 时继续编辑下一个索引,我们可以利用Qt的实现,采用正确的实现方式,而无需自定义委托。
下面的代码实现了这一点,在后面的索引处继续编辑,最终根据变量的设置回环到第一行或第一列。
请注意,这是对 CustomTableView 的完整实现,并不依赖上述修改,也不需要自定义委托。
class CustomTableView(QTableView):
_enterPressed = False # Do not change this!
# Change the following depending on the wanted behavior:
# continue editing after pressing Enter/Return?
_keepEditing = True
# if _keepEditing is True, edit the next row or column?
_editNextRow = True
# if _keepEditing and _editNextRow are True, edit the next column after
# the last row?
_editNextCol = True
def closeEditor(self, editor, hint):
if (
hint == QAbstractItemDelegate.EndEditHint.SubmitModelCache
and self._keepEditing
):
if self._editNextRow:
self._enterPressed = True
hint = QAbstractItemDelegate.EndEditHint.EditNextItem
super().closeEditor(editor, hint)
self._enterPressed = False
def moveCursor(self, action, modifiers):
if action == QAbstractItemView.MoveNext and self._enterPressed:
current = self.currentIndex()
if current.isValid():
row = current.row() + 1
col = current.column()
if row >= self.model().rowCount(current.parent()):
row = 0
if self._editNextCol:
col += 1
if col >= self.model().columnCount(current.parent()):
col = 0
return current.sibling(row, col)
return super().moveCursor(action, modifiers)
请注意,上述实现并未考虑隐藏的行或列,也未考虑头部中移动的分区。如果你需要考虑这些方面,那么你将需要在实现 moveCursor() 时,结合水平和垂直表头的可视索引与逻辑索引来进行调整。