首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >代理模型中不需要的空行

代理模型中不需要的空行
EN

Stack Overflow用户
提问于 2021-01-26 14:09:41
回答 1查看 596关注 0票数 3

关于Qt的模型/视图体系结构,我有一个问题。

我实现了一个继承自QAbstractItemModel的类QAbstractItemModel,使用自定义方法TestModel.addRevision(...)插入新行,TestModel.removeRevision(...)删除行。我的模型有一个层次结构,对于树中的不同层次,我有不同的方法add_Xremove_X

当然,根据文档,在插入或删除这样的行之前,我会调用所需的函数(省略号处理基于数据源的信息检索,而且我认为没有必要显示)。

代码语言:javascript
复制
def add_revision(self, name:str):
    parent_index = ...
    new_row_in_parent = ...
    self.beginInsertRows(parent_index, new_row_in_parent, new_row_in_parent)
    ...
    self.endInsertRows()

我是一个接一个地插入行,并且注意到,通过使用self.beginInsertRows(parent, start, end)调用end = start +1来添加一个太多行的错误并不常见。

这些方法具有相当相似的结构。

我可以通过附加一个QTreeView来验证我的模型工作得很好。现在我还有一个update方法,它执行以下操作(在伪代码中):

代码语言:javascript
复制
# models.py
class TestModel(QtCore.QAbstractItemMOdel):
    ...
    def __init__(self, parent=None):
        ...
        self.update()
    def update(self):
        # remove all items one by one using remove_revision in a foreach loop
        # scan the source for new (updated) revisions
        # add all revisions found one by one in a foreach loop

在模型上,此函数也按预期工作,一旦触发更新,视图也会自动更新。注意,我在初始化过程中也使用了update

下一步,我实现了一个用于排序和筛选的代理模型。我的问题甚至是在没有设置任何过滤器的情况下,默认的QSortFilterProxyModel也是可重复的。

我把视图设置成这样:

代码语言:javascript
复制
...
view = QTreeView(self)
model = TestModel(self)
proxy_model = QSortFilterModel(self)
proxy_model.setSourceModel(model)
view.setModel(proxy_model)

初始化后,视图按预期显示(见下面的屏幕截图)

然后,在触发update之后,视图显示更改为

把这些讨厌的空行加进去。它们是不可选的,不像“好”行,我也不知道它们是从哪里来的。我尝试用一个QSortFilterProxyModel替换QIdentityProxyModel,然后多余的行就消失了,所以我非常确信空行只在QSortFilterProxModel中添加。但是,这是默认的实现,我还没有覆盖任何排序和筛选方法。

有趣的是,当我使用QIdentityProxyModel时,视图将显示调用update后处于折叠状态的所有项,而使用QSortFilterProxyModel时,项目将保持展开状态。

问题:

打电话给beginInserRowsendInsertRows似乎是不够的。我是否需要发出其他信号来通知代理模型更新?

或者,在源模型中完成所有删除之前,代理模型是否更新得太快了?

编辑1

根据请求,这是我的模型的完整update方法。我还包括正在使用的其他类和方法:

更新模型:

代码语言:javascript
复制
def update(self, skip: bool = True):

    revisions_to_remove = []
    files_to_inspect = []

    for (index, key) in enumerate(self.lookup_virtual_paths):
        #  first remove everything beneath file
        obj = self.lookup_virtual_paths.get(key, None)

        if obj is None:
            continue

        if isinstance(obj, Revision):
            revisions_to_remove.append(obj)

        if isinstance(obj, File):
            files_to_inspect.append(obj)

    #  first remove revisions
    for revision in revisions_to_remove:
        self.remove_revision(revision)
        pass

    file: File
    for file in files_to_inspect:
        # add revisions
        # construct the filesystem path to lookup
        scraper: ScraperVFSObject = file.parent().parent()
        if scraper is None:
            log.warning('tbd')
            return

        path_scraper = Path(scraper.fs_storage_path())
        if not path_scraper.exists():
            w = 'path does not exist "%s"' % (
                path_scraper.absolute().as_posix())
            log.warning(w, path_scraper)
            return

        path_file = path_scraper / Path(file.machine_name)
        if not path_file.exists():
            w = 'path does not exist "%s"' % (
                path_file.absolute().as_posix())
            log.warning(w)
            return

        for elem in path_file.glob('*'):
            if not elem.is_dir():
                continue

            if not len(elem.name) == len(ScraperModel.to_timeformat(datetime.now())):
                continue

            actual_file = elem / \
                Path('%s_%s.html' % (file.machine_name, elem.name))
            if not actual_file.exists():
                continue

            self.add_revision(
                ScraperModel.from_timeformat(elem.name),
                actual_file.absolute().as_posix(),
                file.virtual_path())

增加修订:

代码语言:javascript
复制
def add_revision(self, dt: datetime, file: str, to: str, skip=False):
    f = self.lookup_virtual_paths.get(to, None)
    if f is None:
        w = 'trying to add revision "%s" to virtual path "%s"' % (dt, to)
        log.warning(w)
        return

    r = Revision(dt, file, f, self)

    parent_index = r.parent().get_model_index()
    start = r.get_row_in_parent()

    self.beginInsertRows(parent_index, start, start)

    self.add_to_lookup(r)

    # announce that revision has been added
    self.endInsertRows()


    #  immediately add thumbnail groups to the revision,
    #  because a thumbnail-group can only exist in the revision
    known = ThumbnailGroupKnown(r, self)
    unknown = ThumbnailGroupUnknown(r, self)
    ignored = ThumbnailGroupIgnored(r, self)

    start = known.get_row_in_parent()
    end = ignored.get_row_in_parent()
    self.beginInsertRows(r.get_model_index(), start, end)
    self.add_to_lookup([known, unknown, ignored])
    self.endInsertRows()

删除修订:

代码语言:javascript
复制
def remove_revision(self, revision: "Revision"):
    #  first get ModelIndex for the revision
    parent_index = revision.parent().get_model_index()
    start = revision.get_row_in_parent()

    #  first remove all thumbnail groups
    tgs_to_remove = []
    for tg in revision.children():
        tgs_to_remove.append(tg)

    tg: ThumbnailGroup
    for tg in tgs_to_remove:
        self.beginRemoveRows(tg.parent().get_model_index(),
                             tg.get_row_in_parent(),
                             tg.get_row_in_parent())
        vpath = tg.virtual_path()
        tg.setParent(None)
        del self.lookup_virtual_paths[vpath]
        self.endRemoveRows()


    self.beginRemoveRows(parent_index, start, start)
    key = revision.virtual_path()

    # delete the revision from its parent
    revision.setParent(None)

    #  delete the lookup
    del self.lookup_virtual_paths[key]
    self.endRemoveRows()

编辑2

根据@Carlton的建议,我重新排序了remove_revision中的语句。我明白,这很容易成为一个问题(现在或以后)。现在的实施内容如下:

代码语言:javascript
复制
def remove_revision(self, revision: "Revision"):
    #  first remove all thumbnail groups
    tgs_to_remove = []
    for tg in revision.children():
        tgs_to_remove.append(tg)

    tg: ThumbnailGroup
    for tg in tgs_to_remove:
        self.beginRemoveRows(tg.parent().get_model_index(),
                             tg.get_row_in_parent(),
                             tg.get_row_in_parent())
        vpath = tg.virtual_path()
        tg.setParent(None)
        del self.lookup_virtual_paths[vpath]
        self.endRemoveRows()


    parent_index = revision.parent().get_model_index()
    start = revision.get_row_in_parent()
    self.beginRemoveRows(parent_index, start, start)
    key = revision.virtual_path()

    # delete the revision from its parent
    revision.setParent(None)

    #  delete the lookup
    del self.lookup_virtual_paths[key]
    self.endRemoveRows()

后来我计划直接传递索引,但是为了调试,我决定暂时存储它。然而,这种有问题的行为仍然没有改变。

编辑3

因此,根据@Carlton的建议,“幻影行”似乎是rowCount与实际数据不匹配的问题。

我重新安排了add_revision方法中的更多代码,以便为缩略图组提供以下内容:

代码语言:javascript
复制
 def add_revision(self, dt: datetime, file: str, to: str, skip=False):
    ...
    # no changes before here

    print('before (add_revision)', len(r.children()),
          self.rowCount(r.get_model_index()))

    self.beginInsertRows(r.get_model_index(), 0, 2)
    known = ThumbnailGroupKnown(r, self)
    unknown = ThumbnailGroupUnknown(r, self)
    ignored = ThumbnailGroupIgnored(r, self)
    self.add_to_lookup([known, unknown, ignored])
    self.endInsertRows()

    print('after (add_revision)', len(r.children()),
          self.rowCount(r.get_model_index()))

如您所见,我手动选择了startend参数。通过这种修改,我可以将数据插入到beginInsertRowsendInsertRows以及“幻影行”之间。然而,我有一个新的问题:我通常无法事先知道新行将出现在哪些索引上。这似乎是建议的layoutAboutToBeChanged信号的一个很好的使用,但是我如何在pyside6中传递父列表呢?

编辑4: MWE

下面是一个最小的工作示例。您需要安装PySide6。米维和直接托管在这里的相同代码:

代码语言:javascript
复制
import sys
from PySide6 import (
    QtCore,
    QtWidgets
)


class Node(QtCore.QObject):
    def __init__(self, val: str, model, parent=None):
        super().__init__(parent)
        self.value = val
        self._model = model

    def child_count(self) -> int:
        return len(self.children())

    def get_child(self, row: int) -> "Node":
        if row < 0 or row >= self.child_count():
            return None
        else:
            return self.children()[row]

    def get_model_index(self) -> QtCore.QModelIndex:
        return self._model.index(self.get_row_in_parent(), 0, self.parent().get_model_index())

    def get_row_in_parent(self) -> int:
        p = self.parent()
        if p is not None:
            return p.children().index(self)

        return -1


class RootNode(Node):
    def get_row_in_parent(self) -> int:
        return -1

    def get_model_index(self) -> QtCore.QModelIndex:
        return QtCore.QModelIndex()


class Model(QtCore.QAbstractItemModel):
    def __init__(self, parent=None):
        super().__init__(parent)

        self.root_item = None

        # simulate the changing data
        self._data = [
            (1, 'child 1 of 1'),
            (1, 'child 2 of 1'),
            (1, 'child 3 of 1'),
        ]

        self._initialize_static_part()

        self.update()

    def _initialize_static_part(self):
        """This is the part of my model which never changes at runtime
        """
        self.root_item = RootNode('root', self, self)

        nodes_to_add = []

        for i in range(0, 5):
            new_node = Node(str(i), self)
            nodes_to_add.append(new_node)

        for node in nodes_to_add:
            self.add_node(node, self.root_item)

    def update(self):
        """This is the part which needs update during runtime
        """

        rows_to_add = []
        rows_to_delete = []

        self.layoutAboutToBeChanged.emit()

        for c in self.root_item.children():
            for d in c.children():
                rows_to_delete.append(d)

        for (parent_identifier, name) in self._data:
            node = Node(name, self)
            #  actually, the future parent is a different function, but for the MWE this suffices
            future_parent = self.root_item.get_child(parent_identifier)
            rows_to_add.append((future_parent, node))

        for node in rows_to_delete:
            self.remove_node(node)

        for (parent, node) in rows_to_add:
            self.add_node(node, parent)

        self.layoutChanged.emit()

    def add_node(self, node: Node, parent: Node):
        self.beginInsertRows(parent.get_model_index(),
                            parent.child_count(),
                            parent.child_count())
        node.setParent(parent)
        self.endInsertRows()

    def remove_node(self, node):
        parent_node = node.parent()
        row = parent_node.get_model_index().row()
        self.beginRemoveRows(parent_node.get_model_index(
        ), row, row)
        # print(parent_node.get_model_index().isValid())

        node.setParent(None)

        # print(node)
        # print(parent_node.children())

        self.endRemoveRows()
        # reimplement virtual method

    def columnCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
        return 1

    # reimplement virtual method
    def rowCount(self, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> int:
        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        return parent_item.child_count()

    # reimplement virtual method
    def index(self, row: int, column: int, parent: QtCore.QModelIndex = QtCore.QModelIndex()) -> QtCore.QModelIndex:
        if not self.hasIndex(row, column, parent):
            return QtCore.QModelIndex()

        if not parent.isValid():
            parent_item = self.root_item
        else:
            parent_item = parent.internalPointer()

        child_item: Node = parent_item.get_child(row)
        if child_item is not None:
            return self.createIndex(row, column, child_item)

        return QtCore.QModelIndex()

    # reimplement virtual method
    def parent(self, index: QtCore.QModelIndex) -> QtCore.QModelIndex:
        if not index.isValid():
            return QtCore.QModelIndex()

        child_item: Node = index.internalPointer()
        parent_item = child_item.parent()

        if parent_item is not None:
            return parent_item.get_model_index()

        return QtCore.QModelIndex()

    # reimplement virtual method
    def data(self, index: QtCore.QModelIndex, role: int = QtCore.Qt.DisplayRole) -> object:
        if not index.isValid():
            return None

        if role == QtCore.Qt.DisplayRole:
            item: Node = index.internalPointer()
            if item is not None:
                return item.value
            return 'whats this?'

        return None


class MyWindow(QtWidgets.QMainWindow):
    defaultsize = QtCore.QSize(780, 560)

    def __init__(self, app, parent=None):
        super().__init__(parent)
        self.app = app
        self.resize(self.defaultsize)
        main_layout = QtWidgets.QSplitter(QtCore.Qt.Vertical)
        self.panel = Panel(main_layout)
        self.setCentralWidget(main_layout)

        self.model = Model(self)

        proxy_model1 = QtCore.QSortFilterProxyModel(self)
        proxy_model1.setSourceModel(self.model)

        proxy_model2 = QtCore.QIdentityProxyModel(self)
        proxy_model2.setSourceModel(self.model)

        view1 = QtWidgets.QTreeView(self.panel)
        view1.setAlternatingRowColors(True)
        view1.setModel(proxy_model1)
        view1.expandAll()

        view2 = QtWidgets.QTreeView(self.panel)
        view2.setAlternatingRowColors(True)
        view2.setModel(proxy_model2)
        view2.expandAll()

        self.panel.addWidget(view1)
        self.panel.addWidget(view2)

        # we simulate a change, which would usually be triggered manually
        def manual_change_1():
            self.model._data = [
                (1, 'child 2 of 1'),
                (1, 'child 3 of 1'),
            ]
            self.model.update()

        QtCore.QTimer.singleShot(2000, manual_change_1)


class App(QtWidgets.QApplication):
    def __init__(self):
        super().__init__()
        self.window = MyWindow(self)

    def run(self):
        self.window.show()
        result = self.exec_()
        self.exit()


class Panel(QtWidgets.QSplitter):
    pass


if __name__ == '__main__':
    app = App()
    app.startTimer(1000)

    sys.exit(app.run())
EN

回答 1

Stack Overflow用户

发布于 2022-06-21 15:59:49

虽然有点晚,但我在QT5.X上遇到了同样的问题,并注意到我需要手动连接对行插入/删除信号的无效调用。

代码语言:javascript
复制
// cpp
connect(base, &SomeBaseModel::rowsInserted, proxy, &MyProxyModel::invalidate);
connect(base, &SomeBaseModel::rowsRemoved, proxy, &MyProxyModel::invalidate);
代码语言:javascript
复制
# python
base.rowsInserted.connect(proxy.invalidate)
base.rowsRemoved.connect(proxy.invalidate)

之后,代理和基正确地对齐。我假设对::rowsMoved信号也可以使用同样的方法,但在我的具体情况下不需要这样做。

我选择这种方法而不是layoutChanged方法,以避免对大型、复杂的模型数据进行过度计算。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/65902899

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档