QTreeView,QAbstractItemModel。扩展节点时应用程序退出

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

我需要显示分层模型。用户展开节点时必须创建子节点。子节点的数量是事先不知道的。有些子节点可以在展开父节点后立即创建。而有些子节点需要时间通过发送请求获取数据,然后才能创建。

所以我创建了QTreeView + QSortFilterProxyModel + Qt模型(QAbstractItemModel继承者)+数据模型。下面的代码适用于代理模型。但是没有代理模型 + 对于立即创建的节点,我在扩展任何节点时都有

Process finished with exit code -1073741819 (0xC0000005)
。恐怕在有代理模式的情况下迟早也会出现这个错误

UPD 1:我添加了带有 QueuedConnection 的 _populate_request 信号,以将 fetchMore 调用堆栈与从模型中添加/删除节点“分开”(感谢@musicamante - 这与单发计时器的想法相同)。那有帮助。但这一步对我来说并不明显,我仍然想知道为什么直接调用会导致崩溃。

更新 2:添加了“重新加载”上下文菜单以重新加载子项。检查旧的子节点删除是否崩溃。

import random
import sys
import weakref
from enum import Enum, auto
from typing import Optional, List
from PyQt5 import QtCore, QtTest
from PyQt5.QtWidgets import QMainWindow, QTreeView, QVBoxLayout, QApplication, QMenu
from PyQt5.QtCore import QModelIndex, Qt


class TreeNodeStatus(Enum):
    NotPopulated = auto()
    Populating = auto()
    Populated = auto()
    Error = auto()


class TreeNode(QtCore.QObject):
    """ Node of objects tree; root node is essentially a data model """

    status_changed = QtCore.pyqtSignal(object)  # node
    before_children_added = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    children_added = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    before_children_removed = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count
    children_removed = QtCore.pyqtSignal(object, int, int)  # parent_node, pos, count

    _populate_request = QtCore.pyqtSignal()

    def __init__(self, name: str, parent: Optional['TreeNode']):
        super().__init__()

        self._name = name

        self._parent_ref = weakref.ref(parent) if parent is not None else lambda: None

        self._status: TreeNodeStatus = TreeNodeStatus.NotPopulated

        self._children: List[TreeNode] = []

        # to listen root node signals only
        if parent is not None:
            self.status_changed.connect(parent.status_changed)
            self.before_children_added.connect(parent.before_children_added)
            self.children_added.connect(parent.children_added)
            self.before_children_removed.connect(parent.before_children_removed)
            self.children_removed.connect(parent.children_removed)

        # to imitate minimal delay between fetchMore > populate call stack and adding/removing nodes;
        # for nodes that can be created immediately in populate direct calls
        # fetchMore > populate > _on_children_received causes crash;
        # using of this signal prevents crash
        self._populate_request.connect(self._populate, Qt.ConnectionType.QueuedConnection)

        # for nodes that can not be created immediately in populate;
        # to imitate delay due to getting response to a request
        self._timer = QtCore.QTimer()
        self._timer.setSingleShot(True)
        self._timer.setInterval(2 * 1000)  # 2s
        self._timer.timeout.connect(self._on_children_received)

    def parent(self) -> Optional['TreeNode']:
        return self._parent_ref()

    @property
    def status(self) -> TreeNodeStatus:
        return self._status

    def _set_status(self, status: TreeNodeStatus):
        self._status = status
        self.status_changed.emit(self)

    def populate(self):
        # # signal with QueuedConnection - works good
        # self._populate_request.emit()
        # direct call causes app crash for nodes that can be created immediately and if there is no proxy model
        self._populate()

    def _populate(self):

        # loading was started for this node already, exit
        if self.status == TreeNodeStatus.Populating:
            return

        # start loading
        self._set_status(TreeNodeStatus.Populating)

        # forget old children
        old_children_count = len(self._children)
        self.before_children_removed.emit(self, 0, old_children_count)
        # disconnect signals
        for child in self._children:
            child.status_changed.disconnect(self.status_changed)
            child.before_children_added.disconnect(self.before_children_added)
            child.children_added.disconnect(self.children_added)
            child.before_children_removed.disconnect(self.before_children_removed)
            child.children_removed.disconnect(self.children_removed)
        self._children.clear()
        self.children_removed.emit(self, 0, old_children_count)

        # request data about children nodes
        # # timer - for nodes that can not be created immediately
        # self._timer.start()
        # direct call - for nodes that can be created immediately
        self._on_children_received()

    def children(self) -> List['TreeNode']:
        return self._children

    @property
    def name(self) -> str:
        return self._name

    def _on_children_received(self):
        print('!_on_children_received', self.name)

        # create children nodes
        new_children_count = random.randint(0, 4)
        self.before_children_added.emit(self, 0, new_children_count)
        self._children = [TreeNode(self.name + ' ' + str(i), self) for i in range(new_children_count)]
        self.children_added.emit(self, 0, new_children_count)

        # update status
        self._set_status(TreeNodeStatus.Populated)


class TreeModel(QtCore.QAbstractItemModel):

    def __init__(self, root_node: TreeNode):
        super().__init__()

        # root node == data model
        self._root_node = root_node
        self._root_node.status_changed.connect(self._on_node_status_changed)
        self._root_node.before_children_added.connect(self._before_children_added)
        self._root_node.children_added.connect(self._on_children_added)
        self._root_node.before_children_removed.connect(self._before_children_removed)
        self._root_node.children_removed.connect(self._on_children_removed)

    def index(self, row: int, column: int, parent=QModelIndex(), *args, **kwargs) -> QModelIndex:
        # discard non-existent indices: check for row/column for given parent inside
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        # get parent node by index
        if parent is None or not parent.isValid():
            parent_node: TreeNode = self._root_node
        else:
            parent_node: TreeNode = parent.internalPointer()

        # if has child with given row
        if row < len(parent_node.children()):
            # create index with node as internalPointer
            return self.createIndex(row, column, parent_node.children()[row])

        return QModelIndex()

    def parent(self, index: QModelIndex = None) -> QModelIndex:
        # invalid index => root node
        if not index.isValid():
            return QModelIndex()

        node: TreeNode = index.internalPointer()
        parent_node: TreeNode = node.parent()

        # if parent is root node, return invalid index
        if parent_node is self._root_node:
            return QModelIndex()

        # get row of parent node; parent_node is not root, must have it's own parent
        grandparent_node = parent_node.parent()
        parent_row = grandparent_node.children().index(parent_node)

        # create index with node as internalPointer
        return self.createIndex(parent_row, 0, parent_node)

    def hasChildren(self, parent=QModelIndex(),  *args, **kwargs) -> bool:
        # can we expand node? if we can we have a triangle to the left of the node

        parent_node = self._node_from_index(parent)

        # children loaded - look at the number
        if parent_node.status == TreeNodeStatus.Populated:
            return len(parent_node.children()) > 0
        # error - no children, can't expand
        elif parent_node.status == TreeNodeStatus.Error:
            return False
        # not loaded/loading - assume they are
        else:
            return True

    def canFetchMore(self, parent: QModelIndex) -> bool:
        # can we get more data (child nodes) for parent?
        # print('canFetchMore!', self._node_from_index(parent).name)
        return self._can_fetch_more(parent)

    def _can_fetch_more(self, parent: QModelIndex) -> bool:
        parent_node = self._node_from_index(parent)

        # children are not loaded/loading - assume they are
        if parent_node.status == TreeNodeStatus.NotPopulated:
            return True
        # in other cases - can not get more child nodes
        elif parent_node.status in [TreeNodeStatus.Populating,
                                    TreeNodeStatus.Populated,
                                    TreeNodeStatus.Error]:
            return False
        assert False

    def fetchMore(self, parent: QModelIndex) -> None:
        # get more data (child nodes) for parent
        print('!FetchMore', self._node_from_index(parent).name)

        if not self._can_fetch_more(parent):
            return

        parent_node = self._node_from_index(parent)

        if parent_node.status != TreeNodeStatus.Populating:
            parent_node.populate()

    def rowCount(self, parent=QModelIndex(), *args, **kwargs):
        parent_node = self._node_from_index(parent)
        return len(parent_node.children())

    def columnCount(self, parent=None, *args, **kwargs):
        return 1

    def _node_from_index(self, index: Optional[QModelIndex]) -> TreeNode:
        # invalid index - root node
        if index is None or not index.isValid():
            return self._root_node
        else:
            return index.internalPointer()

    def _index_from_node(self, node: TreeNode) -> Optional[QModelIndex]:
        # root node - invalid index
        if node is self._root_node:
            return QModelIndex()

        # according to the principle from index method
        parent_node = node.parent()
        row = parent_node.children().index(node)
        return self.createIndex(row, 0, node)

    def data(self, index, role=None):

        node = self._node_from_index(index)

        if role == Qt.DisplayRole:
            return node.name

        # get nodes by UserRole
        elif role == Qt.UserRole:
            return node

        elif role == Qt.DecorationRole:
            pass

    def _on_node_status_changed(self, node: TreeNode):
        index = self._index_from_node(node)

        if index is not None:
            # notify about changes - icon, tooltip
            self.dataChanged.emit(index, index, [Qt.DecorationRole, Qt.ToolTipRole])

    def _before_children_removed(self, parent_node: TreeNode, pos: int, count: int):
        parent_index = self._index_from_node(parent_node)

        if parent_index is not None:
            self.beginRemoveRows(parent_index, pos, pos + count - 1)

    def _on_children_removed(self, parent_node: TreeNode, pos: int, count: int):
        self.endRemoveRows()

    def _before_children_added(self, parent_node: TreeNode, pos: int, count: int):
        parent_index = self._index_from_node(parent_node)

        if parent_index is not None:
            self.beginInsertRows(parent_index, pos, pos + count - 1)
            print('!beginInsertRows', parent_node.name)

    def _on_children_added(self, parent_node: TreeNode, pos: int, count: int):
        self.endInsertRows()
        print('!endInsertRows', parent_node.name)


class TreeView(QTreeView):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._menu = QMenu(self)

        # reload child nodes
        self._reload_act = self._menu.addAction('Reload')
        self._reload_act.triggered.connect(self._on_reload)

    def mouseReleaseEvent(self, event):
        """ Call context menu on right click button release """
        super().mouseReleaseEvent(event)
        if event.button() == Qt.MouseButton.RightButton:
            index = self.indexAt(event.pos())
            # above nodes only
            if index.isValid():
                self._menu.popup(self.viewport().mapToGlobal(event.pos()))

    def _on_reload(self):
        index = self.currentIndex()
        node = index.data(role=Qt.UserRole)
        if node.status != TreeNodeStatus.Populating:
            node.populate()


class ClientWindow(QMainWindow):

    def __init__(self):
        super().__init__()

        self._setup_ui()

        root_node = TreeNode('root', None)
        model = TreeModel(root_node)
        # proxy = QtCore.QSortFilterProxyModel()
        # proxy.setSourceModel(model)
        # FixMe crash on expanding any node if we put source model here
        self._view.setModel(model)

    def _setup_ui(self):
        self._view = TreeView()
        self._view.setSortingEnabled(True)

        central_wdg = self._view
        central_vlt = QVBoxLayout()
        central_wdg.setLayout(central_vlt)
        self.setCentralWidget(central_wdg)


if __name__ == '__main__':
    app = QApplication([])
    main_window = ClientWindow()
    main_window.show()
    sys.exit(app.exec())

python pyqt
1个回答
0
投票

问题出在 TreeNode._populate 方法中。我们只需要在

before_children_removed
时发出
count > 0
信号。像这样

if old_children_count > 0:
            self.before_children_removed.emit(self, 0, old_children_count)

children_removed
信号相同。

if old_children_count > 0:
            self.children_removed.emit(self, 0, old_children_count)

所以不再需要

_populate_request
信号和其他解决方法。

说明

before_children_removed
时发送
old_children_count == 0
信号是错误的。这种情况出现在第一次
populate
调用节点时。

在那种情况下,我与

TreeModel._before_children_removed
pos=0
进行了
count=0
通话,
TreeModel.beginRemoveRows
first=0
last=-1
在里面。这些值在 Qt 源代码中触发
Q_ASSERT(last >= first);
并导致另一个后续错误并最终崩溃。

void QAbstractItemModel::beginRemoveRows(const QModelIndex &parent, int first, int last)
{
    Q_ASSERT(first >= 0);
    Q_ASSERT(last >= first);
    Q_ASSERT(last < rowCount(parent));
    Q_D(QAbstractItemModel);
    d->changes.push(QAbstractItemModelPrivate::Change(parent, first, last));
    emit rowsAboutToBeRemoved(parent, first, last, QPrivateSignal());
    d->rowsAboutToBeRemoved(parent, first, last);
}

好奇这个

Q_ASSERT
在使用发布Qt DLL时不会导致应用程序崩溃。 Release DLLs with debug symbols 仅显示另一个后续错误,后果。我们只能通过调试 Qt DLL 才能看到真正的原因。

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