代理模型中不需要的空行

如何解决代理模型中不需要的空行

我有一个关于 Qt 模型/视图架构的问题。

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

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

>
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 来证明我的模型工作正常。现在我还有一个更新方法,它执行以下操作(在伪代码中):

# 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 重现,无需设置任何过滤器。

我这样设置视图:

...
view = QTreeView(self)
model = TestModel(self)
proxy_model = QSortFilterModel(self)
proxy_model.setSourceModel(model)
view.setModel(proxy_model)

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

after initialization

然后在我触发 update 后,视图显示变为

after update

添加这些讨厌的空行的地方。它们是不可选择的,不像“好”行,我不知道它们来自哪里。我尝试用 QSortFilterProxyModel 替换 QIdentityProxyModel 并且多余的行消失了,所以我非常有信心只在​​ QSortFilterProxModel 中添加空行。但是,这是默认实现,我还没有覆盖任何排序和过滤方法。

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

问题:

调用 beginInserRowsendInsertRows 似乎还不够。我是否需要发出其他信号来通知代理模型更新?

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

编辑 1

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

更新模型:

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())

添加修订:

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)

    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(),end)
    self.add_to_lookup([known,unknown,ignored])
    self.endInsertRows()

删除修订:

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)
    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 中的语句。我明白,这很容易成为一个问题(现在或以后)。现在实现如下:

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())
        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)
    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 方法中重新排列了更多代码,为缩略图组提供以下内容:

 def add_revision(self,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(),2)
    known = ThumbnailGroupKnown(r,self)
    self.add_to_lookup([known,ignored])
    self.endInsertRows()

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

如您所见,我手动选择了 startend 参数。通过此修改,我可以将数据插入实际放在 beginInsertRowsendInsertRows 之间,并且“幻像行”消失。但是,我遇到了一个新问题:我通常无法事先知道新行将出现在哪些索引处。这对于建议的 layoutAboutToBeChanged 信号似乎是一个很好的用途,但是我如何才能在 pyside6 中传递父列表?

编辑 4:MWE

这是一个最小的工作示例。您需要安装 PySide6。 MWE 同样的代码直接托管在这里:

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(),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'),'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,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,]
            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())

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-