如何解决代理模型中不需要的空行
我实现了一个继承自 TestModel
的类 qabstractitemmodel
,使用自定义方法 TestModel.addRevision(...)
插入新行和 TestModel.removeRevision(...)
删除行。我的模型具有层次结构,对于树中的不同级别,我有不同的方法 add_X
和 remove_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)
在初始化之后,视图按预期显示(见下面的截图)
然后在我触发 update
后,视图显示变为
添加这些讨厌的空行的地方。它们是不可选择的,不像“好”行,我不知道它们来自哪里。我尝试用 QSortFilterProxyModel
替换 QIdentityProxyModel
并且多余的行消失了,所以我非常有信心只在 QSortFilterProxModel
中添加空行。但是,这是默认实现,我还没有覆盖任何排序和过滤方法。
有趣的是,当我使用 QIdentityProxyModel
时,视图在调用 update
后显示所有项目处于折叠状态,而使用 QSortFilterProxyModel
项目保持展开状态。
问题:
调用 beginInserRows
和 endInsertRows
似乎还不够。我是否需要发出其他信号来通知代理模型更新?
或者,在源模型中完成所有删除之前,代理模型更新太快了吗?
编辑 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()))
如您所见,我手动选择了 start
和 end
参数。通过此修改,我可以将数据插入实际放在 beginInsertRows
和 endInsertRows
之间,并且“幻像行”消失。但是,我遇到了一个新问题:我通常无法事先知道新行将出现在哪些索引处。这对于建议的 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 举报,一经查实,本站将立刻删除。