提问人:Vailsen Pic 提问时间:11/17/2023 最后编辑:Vailsen Pic 更新时间:11/23/2023 访问量:84
Python/PySide6:treeView 和 tableView 之间的单击序列后崩溃
Python/PySide6: Crashes after Click sequence between treeView and tableView
问:
在我当前的项目中满足某些条件后,我遇到了崩溃。 我花了一些时间才真正查明问题所在。
我正在将 Python 3.11 与 PySide6 一起使用。 简而言之,该应用程序显示:
- treeView(使用 QStandardItemModel)
- tableView1(使用 QAbstractTableModel)
- tableView2(使用 QAbstractTableModel 和 QSortFilterProxyModel)
在功能方面,一切都按预期工作。 基本功能:
- 用户从 treeView 中选择项目 -> tableView1 显示所选对象内的所有属性 -> tableView2 显示与所选对象匹配的所有条目的 SQL 数据库内容。
- 用户现在可以与 tableView1 + tableView2 交互(单击、编辑...
问题(点击顺序):
- 单击 treeView 中的项目 -> tableView1 和 2 正在更新
- 单击 tableView2 中的列标题 (.setSortingEnabled(True))
- 单击 tableView2 中的单元格(仅突出显示)
- 单击 treeView 中的其他项目 -> tableView1 和 2 更新
- 再次单击 tableView2 中的列标题 ->崩溃(主要是在第一次单击时,但有时您可以在崩溃之前单击几次)
只有这个序列才会导致崩溃。如果你错过了一个步骤,它不会崩溃。 对于“崩溃”,我的意思是:
- 不会引发异常,线程只是关闭
- 调试器不会停止@exception,它只是崩溃
FaultHandler 堆栈跟踪不会显示太多信息...
重要:我已经解决了一个非常相似的错误: 点击顺序:
- 单击 treeView 中的项目 -> tableView1 和 2 正在更新
- 单击 tableView2 中的单元格(仅突出显示)
- 单击treeView ->崩溃中的不同项目(主要是在第一次单击时,但有时您可以在崩溃之前单击几次)
我注意到,在单击 tV2 中的单元格后,如果我单击其他地方(例如 tV1 中的单元格),那么在再次单击 treeView 时它不会崩溃。 所以我的假设是,当 tV2 中的单元格被点击时,一定在某个地方存储了一些东西。 经过快速研究,我尝试在执行 treeView 单击操作之前删除 tV2 的选择数据:
selection_model_tV2 = self.ui.tabView_2.selectionModel()
selection_model_tV2.clear()
这奏效了!
因此,自然而然地,我的第一种方法(针对当前的错误)是清除 treeView、tV1、tV2 和这些混合的选择数据,但到目前为止没有任何效果......
对于当前的错误,我不确定哪个 .selectionModel() 对错误负责(如果有的话),因此我尝试清除 treeView、tV1、tV2 和组合。
附加信息:
- 如果我禁用QSortFilterProxy,则不会发生两次崩溃...
更新: 在下面,一个可重现的示例:
import faulthandler
import sys
import pandas as pd
from PySide6.QtCore import QAbstractTableModel, QSortFilterProxyModel, Qt
from PySide6.QtGui import QStandardItem, QStandardItemModel
from PySide6.QtWidgets import (
QApplication,
QHBoxLayout,
QHeaderView,
QMainWindow,
QTableView,
QTreeView,
QWidget,)
faulthandler.enable()
class CustomTableModel(QAbstractTableModel):
def __init__(self, table_data=pd.DataFrame(), parent=None):
super(CustomTableModel, self).__init__()
self.table_data = table_data
self.parent = parent
def data(self, index, role):
value = self.table_data.iat[index.row(), index.column()]
if role == Qt.DisplayRole or role == Qt.EditRole:
return value
def setData(self, index, value, role):
return False
def rowCount(self, index):
return self.table_data.shape[0]
def columnCount(self, index):
return self.table_data.shape[1]
class CustomHeaderView(QHeaderView):
def __init__(self, orientation, parent=None):
super(CustomHeaderView, self).__init__(orientation, parent)
self.setSectionsClickable(True)
def mousePressEvent(self, event):
super().mousePressEvent(event)
class CustomProxyModel(QSortFilterProxyModel):
def __init__(self, parent=None):
super(CustomProxyModel, self).__init__(parent)
def sort(self, column, order):
return super().sort(column, order)
class SimpleGUI(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Simple PySide6 GUI")
self.setGeometry(100, 100, 800, 600)
# Create a QTreeView on the left
self.tree_view = QTreeView()
self.tree_view.setHeaderHidden(True)
self.tree_view.setRootIsDecorated(True)
# Create a QFileSystemModel to populate the QTreeView
self.treeView_model = QStandardItemModel(self)
self.tree_view.setModel(self.treeView_model)
self.init_treeView()
self.tree_view.clicked.connect(self.treeView_clicked)
# Create a QTableView on the right
self.table_view = QTableView()
self.tableView_model = CustomTableModel(parent=self)
# COMMENT THESE LINES FOR QSORTFILTERPROXY BYPASS
self.proxy_model = CustomProxyModel(self)
self.proxy_model.setSourceModel(self.tableView_model)
self.table_view.setModel(self.proxy_model)
# UNCOMMMENT FOR QSORTFILTERPROXY BYPASS
# self.table_view.setModel(self.tableView_model)
self.header_view = CustomHeaderView(Qt.Orientation.Horizontal, self)
self.table_view.setHorizontalHeader(self.header_view)
self.table_view.setSortingEnabled(True)
self.table_view_data_dict = {
"item1": ["1_1", "1_2", "1_3"],
"item2": ["2_1", "2_2", "2_3"],
"item3": ["3_1", "3_2", "3_3"],
}
# Create a vertical layout for the main window
main_layout = QHBoxLayout()
# Add the treeView to the layout
main_layout.addWidget(self.tree_view)
# Add the tableView to the layout
main_layout.addWidget(self.table_view)
# Create a central widget and set the main layout
central_widget = QWidget()
central_widget.setLayout(main_layout)
# Set the central widget for the main window
self.setCentralWidget(central_widget)
def init_treeView(self):
parent = self.treeView_model.invisibleRootItem()
data = [
"item1",
"item2",
"item3",
]
for element in data:
child = QStandardItem(element)
parent.appendRow(child)
def treeView_clicked(self, index):
self.clicked_treeView_item = self.treeView_model.itemFromIndex(index).text()
df_for_tableView = pd.DataFrame(self.table_view_data_dict[self.clicked_treeView_item]).T
self.tableView_model.table_data = df_for_tableView
self.tableView_model.layoutChanged.emit()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = SimpleGUI()
window.show()
sys.exit(app.exec_())
这两个错误都是可重现的:
- 单击treeView中的项目,单击tableView中的单元格,再次单击treeView ->崩溃(您必须尝试几次)
- 单击treeView,单击列标题,单击单元格,单击treeView,再次单击列标题 - >崩溃(您必须尝试几次)
有什么建议吗?
先谢谢你
答:
有一些教程(以及 SO 上的帖子)隐含地表明,当表格的内容发生变化时,发出信号就足够了。layoutChanged
不幸的是,这几乎总是错误的,或者至少是不够的。
虽然只有发出通常有效,但这只是因为Qt项目视图的实现方式足够聪明,通常可以忽略一些缺失的方面,但实际上,我们永远不应该依赖这一点。layoutChanged
大多数 QAbstractItemModel 都具有每当模型结构发生变化时必须成对且按正确顺序发出的信号:形状(行/列计数,包括树模型的子项)和布局(项目顺序)。
该概念始终基于三个步骤,按以下精确顺序排列:
- 发出“即将做X”的信号;
- 更改基础数据;
- 发出“X完成”信号;
正确遵循上述过程可确保所有正确的信号以正确的顺序发送到连接的视图,这些视图在内部保留对持久索引的引用,并最终在更改完成后重新映射它们。
如果不这样做,通常会导致在最佳情况下选择不一致,但也可能导致绘图/更新问题,以及与你的情况一样,致命的崩溃。
例如,在添加一行时,必须首先发出(即使只是一行),然后在内部数据中插入该行,最后调用 .正常过程是使用现有函数:beginInsertRows() 和 endInsertRows(
),
它们也在内部发出相关信号。rowsAboutToBeInserted
rowsInserted
如果布局发生变化,则使用 和 信号实现(同样,两个信号都必须按此顺序发射)。layoutAboutToBeChanged
layoutChanged
有趣的是,layoutChanged
信号文档指出了一个重要的要求,而这正是导致问题的原因。
在对 QAbstractItemModel 或 QAbstractProxyModel 进行子类化时,请确保在更改项的顺序或更改向视图公开的数据结构之前发出 layoutAboutToBeChanged(),并在更改布局后发出 layoutChanged()。
请注意,在此上下文中,“布局”意味着更改是按照项目在同一结构中的放置顺序进行的,而行/列计数保持不变。例如,在移动行或列时,甚至在排序时。
这显然不是您的情况,因为您可能会更改模型的全部内容(包括行/列计数、形状),因此您实际上需要使用模型重置信号: 和 .实际上,与上面的行插入类似,最好调用专用函数:beginResetModel()
和 endResetModel()。
modelAboutToBeReset
modelReset
最后,遵循封装和关注点分离的正确 OOP 原则,切勿从模型外部调用/发出上述函数/信号。更好的解决方案是在模型中创建一个自定义函数,然后从那里执行上述所有操作。
class CustomTableModel(QAbstractTableModel):
...
def setTableData(self, df):
self.beginResetModel()
self.table_data = df
self.endResetModel()
class SimpleGUI(QMainWindow):
...
def treeView_clicked(self, index):
key = index.data()
if key in self.table_view_data_dict:
table_data = pd.DataFrame(table_view_data_dict[key]).T
self.tableView_model.setTableData(table_data)
另请注意,即使您实际在表视图上使用模型,两者 and 也应始终具有可选参数(默认为 ,表示根),因为重写必须始终与重写函数的签名一致。出于同样的原因,两者都应该有一个默认值。rowCount()
columnCount()
parent
QModelIndex()
data()
setData()
role
巧合的是,这与上面解释的“智能实现”有关,这不应该是理所当然的。
class CustomTableModel(QAbstractTableModel):
...
# note the "role" default argument
def data(self, index, role=Qt.DisplayRole):
value = self.table_data.iat[index.row(), index.column()]
if role == Qt.DisplayRole or role == Qt.EditRole:
return value
# as above, but the default role of setData() is EditRole;
# note that this override is actually unnecessary, since the
# default behavior of QAbstractItemModel (which is directly
# called by default from QAbstractTableModel) does nothing, and
# already returns False
def setData(self, index, value, role=Qt.EditRole):
return False
def rowCount(self, index, parent=QModelIndex()):
return self.table_data.shape[0]
def columnCount(self, index, parent=QModelIndex()):
return self.table_data.shape[1]
如果您完全确定您的模型是单维(列表)或二维(表),则可以是 .parent
None
评论
self.beginResetModel() [...] self.endResetModel()
QAbstractItemView::closeEditor called with an editor that does not belong to this view QAbstractItemView::commitData called with an editor that does not belong to this view
评论
self.tableView_model.layoutAboutToBeChanged.emit()
self.tableView_model.table_data = df_for_tableView