Python/PySide6:treeView 和 tableView 之间的单击序列后崩溃

Python/PySide6: Crashes after Click sequence between treeView and tableView

提问人:Vailsen Pic 提问时间:11/17/2023 最后编辑:Vailsen Pic 更新时间:11/23/2023 访问量:84

问:

在我当前的项目中满足某些条件后,我遇到了崩溃。 我花了一些时间才真正查明问题所在。

我正在将 Python 3.11 与 PySide6 一起使用。 简而言之,该应用程序显示:

  • treeView(使用 QStandardItemModel)
  • tableView1(使用 QAbstractTableModel)
  • tableView2(使用 QAbstractTableModel 和 QSortFilterProxyModel)

在功能方面,一切都按预期工作。 基本功能:

  • 用户从 treeView 中选择项目 -> tableView1 显示所选对象内的所有属性 -> tableView2 显示与所选对象匹配的所有条目的 SQL 数据库内容。
  • 用户现在可以与 tableView1 + tableView2 交互(单击、编辑...

问题(点击顺序):

  1. 单击 treeView 中的项目 -> tableView1 和 2 正在更新
  2. 单击 tableView2 中的列标题 (.setSortingEnabled(True))
  3. 单击 tableView2 中的单元格(仅突出显示)
  4. 单击 treeView 中的其他项目 -> tableView1 和 2 更新
  5. 再次单击 tableView2 中的列标题 ->崩溃(主要是在第一次单击时,但有时您可以在崩溃之前单击几次)

只有这个序列才会导致崩溃。如果你错过了一个步骤,它不会崩溃。 对于“崩溃”,我的意思是:

  • 不会引发异常,线程只是关闭
  • 调试器不会停止@exception,它只是崩溃

FaultHandler 堆栈跟踪不会显示太多信息...

重要:我已经解决了一个非常相似的错误: 点击顺序:

  1. 单击 treeView 中的项目 -> tableView1 和 2 正在更新
  2. 单击 tableView2 中的单元格(仅突出显示)
  3. 单击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_())

这两个错误都是可重现的:

  1. 单击treeView中的项目,单击tableView中的单元格,再次单击treeView ->崩溃(您必须尝试几次)
  2. 单击treeView,单击列标题,单击单元格,单击treeView,再次单击列标题 - >崩溃(您必须尝试几次)

有什么建议吗?

先谢谢你

python pyqt 崩溃 pyside6

评论

1赞 musicamante 11/17/2023
尽管您的描述可能很准确,但仅要真正了解和理解问题是不够的。请提供一个最小的可重复示例
0赞 Vailsen Pic 11/17/2023
该死的......我害怕这个要求:)我认为,有了所描述的所有元素,这不会是一个简短的例子。但我会对此进行调查。我希望有人能了解我对上一个类似错误的解决方案的解释是怎么回事。
0赞 musicamante 11/18/2023
不幸的是,您的描述只是关于过程的,我们不知道所有这些是如何通过代码实现的。您还在使用自定义子类(其他模型),问题实际上可能是由您对这些子类的实现引起的:由于我们对它们一无所知,因此我们只能进行疯狂的猜测,这对于一个非常具体的问题毫无意义,尤其是当它导致崩溃时。逐渐将代码简化为非常基本的实现,以便可以从等式中排除可能不必要的方面,同时不断测试更改以查看问题是否仍然存在。
0赞 Vailsen Pic 11/20/2023
我添加了最小的可重现示例。在此示例中,Filterproxy 不执行任何操作,但是,它是问题的一部分,并且在我的应用程序中具有一些过滤功能。
0赞 musicamante 11/21/2023
您能否在添加之前的原始代码中检查问题是否也已解决?self.tableView_model.layoutAboutToBeChanged.emit()self.tableView_model.table_data = df_for_tableView

答:

0赞 musicamante 11/22/2023 #1

有一些教程(以及 SO 上的帖子)隐含地表明,当表格的内容发生变化时,发出信号就足够了。layoutChanged

不幸的是,这几乎总是错误的,或者至少是不够的。

虽然只有发出通常有效,但这只是因为Qt项目视图的实现方式足够聪明,通常可以忽略一些缺失的方面,但实际上,我们永远不应该依赖这一点。layoutChanged

大多数 QAbstractItemModel 都具有每当模型结构发生变化时必须成对且按正确顺序发出的信号:形状(行/列计数,包括树模型的子项)和布局(项目顺序)。

该概念始终基于三个步骤,按以下精确顺序排列:

  1. 发出“即将做X”的信号;
  2. 更改基础数据;
  3. 发出“X完成”信号;

正确遵循上述过程可确保所有正确的信号以正确的顺序发送到连接的视图,这些视图在内部保留对持久索引的引用,并最终在更改完成后重新映射它们。

如果不这样做,通常会导致在最佳情况下选择不一致,但也可能导致绘图/更新问题,以及与你的情况一样,致命的崩溃。

例如,在添加一行时,必须首先发出(即使只是行),然后在内部数据中插入该行,最后调用 .正常过程是使用现有函数:beginInsertRows() 和 endInsertRows(),它们也在内部发出相关信号。rowsAboutToBeInsertedrowsInserted

如果布局发生变化,则使用 和 信号实现(同样,两个信号都必须按此顺序发射)。layoutAboutToBeChangedlayoutChanged

有趣的是,layoutChanged 信号文档指出了一个重要的要求,而这正是导致问题的原因。

在对 QAbstractItemModel 或 QAbstractProxyModel 进行子类化时,请确保在更改项的顺序或更改向视图公开的数据结构之前发出 layoutAboutToBeChanged(),并在更改布局后发出 layoutChanged()。

请注意,在此上下文中,“布局”意味着更改是按照项目在同一结构中的放置顺序进行的,而行/列计数保持不变。例如,在移动行或列时,甚至在排序时。

这显然不是您的情况,因为您可能会更改模型的全部内容(包括行/列计数、形状),因此您实际上需要使用模型重置信号: 和 .实际上,与上面的行插入类似,最好调用专用函数:beginResetModel()endResetModel()。modelAboutToBeResetmodelReset

最后,遵循封装和关注点分离的正确 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()parentQModelIndex()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]

如果您完全确定您的模型是单维(列表)或二维(表),则可以是 .parentNone

评论

0赞 Vailsen Pic 11/22/2023
感谢您抽出宝贵时间如此详细地回答!我很乐意调整您的其他建议:)
0赞 Vailsen Pic 11/24/2023
使用时:控制台中出现以下警告,编辑单元格时:编辑单元格时也会失去对已编辑单元格的焦点。功能仍然有效。如果我切换回“layoutChanged”-combo,则不存在此问题。那里有什么问题?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
0赞 musicamante 11/25/2023
@VailsenPic 这不应该发生,因为重置信号应该负责正确删除编辑器(我尝试使用您的原始代码和我的更改,但我没有看到任何警告)。我建议你创建一个单独的问题,并提供一个相关的最小可重复的例子
0赞 Vailsen Pic 12/7/2023
如果您有兴趣,我为此提出了一个新问题:)stackoverflow.com/questions/77619520/......