如何使用 PySide.QtCore.QAbstractItemModel 和 QTreeView 提高选择性能?
How to improve selection performance with PySide.QtCore.QAbstractItemModel and QTreeView?
我 运行 遇到了一个困扰我的问题,我想知道是否有任何已知的解决方法。似乎在使用子类 QAbstractItemView 的 QTreeView 上执行 selections 可能非常非常慢。
举个例子;我修改了随 PySide 安装的示例文件:
..\site-packages\PySide\examples\itemviews\simpletreemodel\simpletreemodel.py
拥有约 4000 行数据项,高于原来的 40 行。
我也将树视图设置为 QtGui.QAbstractItemView.SelectionMode.ExtendedSelection
。 运行 这导致 gui 交互充其量是缓慢的,并且在生成 "large" select 离子时非常缓慢。通过鼠标操作、键盘操作和脚本操作都是如此。
关于后者,我在修改后的脚本中添加了一个 view.selectAll()
并对其进行了分析,显示 select 所有项目花费了大约 84 秒。
我正在考虑禁用 selections 并编写我自己的自定义 Selection_Manager 以查看是否可以手动加快速度。有没有人对如何加快标准工作流程有任何其他建议或ideas/examples?
提前致谢。
这是展示此问题的脚本。
from PySide import QtCore, QtGui
NUMBER_OF_ITEMS = 1000
NUMBER_OF_CHILDREN_PER_ITEM = 4
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return len(self.itemData)
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, num=NUMBER_OF_ITEMS, num_of_children=NUMBER_OF_CHILDREN_PER_ITEM, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem( ["Title"] )
self.setupModelData( self.rootItem, num=num, num_of_children=num_of_children )
def columnCount(self, parent):
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def setupModelData(self, parent, num=100, num_of_children=10, ):
"""
Simple test method to fill the Model with items.
"""
for i in range( num ):
data = [ '{0}.Item'.format( i ) ]
item = TreeItem( data, parent )
parent.appendChild( item )
for j in range( num_of_children ):
data = [ '{0}.{1}.Child'.format( i, j ) ]
child = TreeItem( data, item )
item.appendChild( child )
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
model = TreeModel( num=NUMBER_OF_ITEMS, num_of_children=NUMBER_OF_CHILDREN_PER_ITEM )
view = QtGui.QTreeView()
view.setModel(model)
view.setSelectionMode( QtGui.QAbstractItemView.SelectionMode.ExtendedSelection )
view.expandAll()
view.setWindowTitle("Simple Tree Model")
view.show()
view.selectAll()
sys.exit(app.exec_())
我已经 运行 你的例子使用了 Python profiler。总共花费了大约 86 秒,其中 51 秒被 list
个对象的 index
方法花费了。这是分析器的输出,按时间排序并在 10 个函数处截断。
Wed Dec 23 12:30:15 2015 stats.dat
21859835 function calls (21859832 primitive calls) in 86.528 seconds
Ordered by: internal time
List reduced from 110 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
3038204 51.266 0.000 51.266 0.000 {method 'index' of 'list' objects}
1 14.445 14.445 84.939 84.939 {exec_}
3053317 9.016 0.000 70.051 0.000 main.py:94(parent)
3060258 4.769 0.000 4.769 0.000 {method 'createIndex' of 'PySide.QtCore.QAbstractItemModel' objects}
3038204 2.383 0.000 53.649 0.000 main.py:36(row)
3168995 1.409 0.000 1.409 0.000 {method 'isValid' of 'PySide.QtCore.QModelIndex' objects}
1 1.187 1.187 86.528 86.528 main.py:2(<module>)
3053317 0.776 0.000 0.776 0.000 main.py:33(parent)
3121496 0.525 0.000 0.525 0.000 {method 'internalPointer' of 'PySide.QtCore.QModelIndex' objects}
22054 0.189 0.000 0.333 0.000 {method 'hasIndex' of 'PySide.QtCore.QAbstractItemModel' objects}
在TreeItem.row()
中调用了list.index()
方法。因此,为了确定一个项目的行号,遍历其父项的子列表,直到找到该项目。这总是让我觉得效率低下,但到目前为止我从未遇到过性能问题。
因此,似乎可能的优化是将行号存储在 TreeItem
中。这当然会使用额外的内存,如果您更新树,您需要确保它保持一致。
另一个角度是研究为什么row
和index
函数被调用的频率如此之高(大约三百万次,我觉得很多)。或许你可以看看QTreeView.selectAll
.
的Qt源码就知道了
祝您好运,如果您找到解决方案,请告诉我。
p.s。我过去手工制作了一个 QItemSelection,请参阅 this post。我认为这对你没有帮助。
我最终按照我的建议做了,在 QTreeView 中禁用 selection 并自己管理 selection。 select 所有时间都下降到大约 2 秒。仍然很高,但好多了。
my_qtreeview.setSelectionMode( QtGui.QAbstractItemView.SelectionMode.NoSelection )
我的完整解决方案并未分解和模块化到可以在此处发布任何可立即重复使用的内容的程度,但也许此代码段会对任何感兴趣的人有所帮助。
一些注意事项:
- 我的 QAbstractItemModel 中有自定义 python 节点。我利用每个中的“.selected”来存储其 selected 状态。
- 在覆盖的 QAbstractItemModel.data() 方法中,当 'node.selected=True' 我将其背景颜色设置为表明该行已 selected.
- 我最终需要手动处理键盘修饰符。我还需要跟踪点击的项目与发布的项目。我还排除了任何 non-left 鼠标点击被视为 select 离子。我在子类 QTreeView 的 mousePressEvent() 和 mouseReleaseEvent() 中执行此操作。
- 我有多个列,有些是图标,单击时不应 selected,而是在视图显示的对象上切换一些 属性。 screenshot of my treeview
下面的 - on_tree_clicked_left() 位于 QTreeView 的父级中,并通过 QTreeView 中的自定义信号触发。
这是我的核心代码示例,应该传达我正在做的事情的要点:
def on_tree_clicked_left( self, index_start, index_end, keyboard_modifiers ):
"""
:param index_start: QtCore.QModelIndex
:param index_end: QtCore.QModelIndex
:param keyboard_modifiers: QtGui.QApplication.keyboardModifiers
"""
if ( index_start is None and index_end is None ) or \
not index_start.isValid() or not index_end.isValid():
self.model_data_objs.deselect_all()
return None
if index_start is None:
index_start = index_end
start_node = index_start.model().mapToSource( index_start ).internalPointer()
end_node = index_end.model().mapToSource( index_end ).internalPointer()
# get the nodes to operate on ...
nodes = [ start_node ]
if not index_start == index_end:
nodes_above = list()
nodes_below = list()
index_above = self.gui_tree.indexAbove( index_start )
index_below = self.gui_tree.indexBelow( index_start )
while not end_node in nodes_above and not end_node in nodes_below:
if index_above.isValid():
node_after = index_above.model().mapToSource( index_above ).internalPointer()
nodes_above.append( node_after )
index_above = self.gui_tree.indexAbove( index_above )
if index_below.isValid():
node_before = index_below.model().mapToSource( index_below ).internalPointer()
nodes_below.append( node_before )
index_below = self.gui_tree.indexBelow( index_below )
if end_node in nodes_above:
nodes += nodes_above
if end_node in nodes_below:
nodes += nodes_below
# get the operation, based on index_start column ...
clicked_column = index_start.column()
if not clicked_column == 0:
attr = COLUMNS[ clicked_column ].get( 'attr_name' )
else:
attr = 'selected'
if not keyboard_modifiers == QtCore.Qt.ControlModifier and \
not keyboard_modifiers == QtCore.Qt.AltModifier:
self.model_data_objs.deselect_all()
# perform the operation
new_value = not getattr( start_node, attr )
for node in nodes:
setattr( node, attr, new_value )
我 运行 遇到了一个困扰我的问题,我想知道是否有任何已知的解决方法。似乎在使用子类 QAbstractItemView 的 QTreeView 上执行 selections 可能非常非常慢。
举个例子;我修改了随 PySide 安装的示例文件:
..\site-packages\PySide\examples\itemviews\simpletreemodel\simpletreemodel.py
拥有约 4000 行数据项,高于原来的 40 行。
我也将树视图设置为 QtGui.QAbstractItemView.SelectionMode.ExtendedSelection
。 运行 这导致 gui 交互充其量是缓慢的,并且在生成 "large" select 离子时非常缓慢。通过鼠标操作、键盘操作和脚本操作都是如此。
关于后者,我在修改后的脚本中添加了一个 view.selectAll()
并对其进行了分析,显示 select 所有项目花费了大约 84 秒。
我正在考虑禁用 selections 并编写我自己的自定义 Selection_Manager 以查看是否可以手动加快速度。有没有人对如何加快标准工作流程有任何其他建议或ideas/examples?
提前致谢。
这是展示此问题的脚本。
from PySide import QtCore, QtGui
NUMBER_OF_ITEMS = 1000
NUMBER_OF_CHILDREN_PER_ITEM = 4
class TreeItem(object):
def __init__(self, data, parent=None):
self.parentItem = parent
self.itemData = data
self.childItems = []
def appendChild(self, item):
self.childItems.append(item)
def child(self, row):
return self.childItems[row]
def childCount(self):
return len(self.childItems)
def columnCount(self):
return len(self.itemData)
def data(self, column):
try:
return self.itemData[column]
except IndexError:
return None
def parent(self):
return self.parentItem
def row(self):
if self.parentItem:
return self.parentItem.childItems.index(self)
return 0
class TreeModel(QtCore.QAbstractItemModel):
def __init__(self, num=NUMBER_OF_ITEMS, num_of_children=NUMBER_OF_CHILDREN_PER_ITEM, parent=None):
super(TreeModel, self).__init__(parent)
self.rootItem = TreeItem( ["Title"] )
self.setupModelData( self.rootItem, num=num, num_of_children=num_of_children )
def columnCount(self, parent):
if parent.isValid():
return parent.internalPointer().columnCount()
else:
return self.rootItem.columnCount()
def data(self, index, role):
if not index.isValid():
return None
if role != QtCore.Qt.DisplayRole:
return None
item = index.internalPointer()
return item.data(index.column())
def flags(self, index):
if not index.isValid():
return QtCore.Qt.NoItemFlags
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable
def headerData(self, section, orientation, role):
if orientation == QtCore.Qt.Horizontal and role == QtCore.Qt.DisplayRole:
return self.rootItem.data(section)
return None
def index(self, row, column, parent):
if not self.hasIndex(row, column, parent):
return QtCore.QModelIndex()
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
childItem = parentItem.child(row)
if childItem:
return self.createIndex(row, column, childItem)
else:
return QtCore.QModelIndex()
def parent(self, index):
if not index.isValid():
return QtCore.QModelIndex()
childItem = index.internalPointer()
parentItem = childItem.parent()
if parentItem == self.rootItem:
return QtCore.QModelIndex()
return self.createIndex(parentItem.row(), 0, parentItem)
def rowCount(self, parent):
if parent.column() > 0:
return 0
if not parent.isValid():
parentItem = self.rootItem
else:
parentItem = parent.internalPointer()
return parentItem.childCount()
def setupModelData(self, parent, num=100, num_of_children=10, ):
"""
Simple test method to fill the Model with items.
"""
for i in range( num ):
data = [ '{0}.Item'.format( i ) ]
item = TreeItem( data, parent )
parent.appendChild( item )
for j in range( num_of_children ):
data = [ '{0}.{1}.Child'.format( i, j ) ]
child = TreeItem( data, item )
item.appendChild( child )
if __name__ == '__main__':
import sys
app = QtGui.QApplication(sys.argv)
model = TreeModel( num=NUMBER_OF_ITEMS, num_of_children=NUMBER_OF_CHILDREN_PER_ITEM )
view = QtGui.QTreeView()
view.setModel(model)
view.setSelectionMode( QtGui.QAbstractItemView.SelectionMode.ExtendedSelection )
view.expandAll()
view.setWindowTitle("Simple Tree Model")
view.show()
view.selectAll()
sys.exit(app.exec_())
我已经 运行 你的例子使用了 Python profiler。总共花费了大约 86 秒,其中 51 秒被 list
个对象的 index
方法花费了。这是分析器的输出,按时间排序并在 10 个函数处截断。
Wed Dec 23 12:30:15 2015 stats.dat
21859835 function calls (21859832 primitive calls) in 86.528 seconds
Ordered by: internal time
List reduced from 110 to 10 due to restriction <10>
ncalls tottime percall cumtime percall filename:lineno(function)
3038204 51.266 0.000 51.266 0.000 {method 'index' of 'list' objects}
1 14.445 14.445 84.939 84.939 {exec_}
3053317 9.016 0.000 70.051 0.000 main.py:94(parent)
3060258 4.769 0.000 4.769 0.000 {method 'createIndex' of 'PySide.QtCore.QAbstractItemModel' objects}
3038204 2.383 0.000 53.649 0.000 main.py:36(row)
3168995 1.409 0.000 1.409 0.000 {method 'isValid' of 'PySide.QtCore.QModelIndex' objects}
1 1.187 1.187 86.528 86.528 main.py:2(<module>)
3053317 0.776 0.000 0.776 0.000 main.py:33(parent)
3121496 0.525 0.000 0.525 0.000 {method 'internalPointer' of 'PySide.QtCore.QModelIndex' objects}
22054 0.189 0.000 0.333 0.000 {method 'hasIndex' of 'PySide.QtCore.QAbstractItemModel' objects}
在TreeItem.row()
中调用了list.index()
方法。因此,为了确定一个项目的行号,遍历其父项的子列表,直到找到该项目。这总是让我觉得效率低下,但到目前为止我从未遇到过性能问题。
因此,似乎可能的优化是将行号存储在 TreeItem
中。这当然会使用额外的内存,如果您更新树,您需要确保它保持一致。
另一个角度是研究为什么row
和index
函数被调用的频率如此之高(大约三百万次,我觉得很多)。或许你可以看看QTreeView.selectAll
.
祝您好运,如果您找到解决方案,请告诉我。
p.s。我过去手工制作了一个 QItemSelection,请参阅 this post。我认为这对你没有帮助。
我最终按照我的建议做了,在 QTreeView 中禁用 selection 并自己管理 selection。 select 所有时间都下降到大约 2 秒。仍然很高,但好多了。
my_qtreeview.setSelectionMode( QtGui.QAbstractItemView.SelectionMode.NoSelection )
我的完整解决方案并未分解和模块化到可以在此处发布任何可立即重复使用的内容的程度,但也许此代码段会对任何感兴趣的人有所帮助。
一些注意事项:
- 我的 QAbstractItemModel 中有自定义 python 节点。我利用每个中的“.selected”来存储其 selected 状态。
- 在覆盖的 QAbstractItemModel.data() 方法中,当 'node.selected=True' 我将其背景颜色设置为表明该行已 selected.
- 我最终需要手动处理键盘修饰符。我还需要跟踪点击的项目与发布的项目。我还排除了任何 non-left 鼠标点击被视为 select 离子。我在子类 QTreeView 的 mousePressEvent() 和 mouseReleaseEvent() 中执行此操作。
- 我有多个列,有些是图标,单击时不应 selected,而是在视图显示的对象上切换一些 属性。 screenshot of my treeview 下面的
- on_tree_clicked_left() 位于 QTreeView 的父级中,并通过 QTreeView 中的自定义信号触发。
这是我的核心代码示例,应该传达我正在做的事情的要点:
def on_tree_clicked_left( self, index_start, index_end, keyboard_modifiers ):
"""
:param index_start: QtCore.QModelIndex
:param index_end: QtCore.QModelIndex
:param keyboard_modifiers: QtGui.QApplication.keyboardModifiers
"""
if ( index_start is None and index_end is None ) or \
not index_start.isValid() or not index_end.isValid():
self.model_data_objs.deselect_all()
return None
if index_start is None:
index_start = index_end
start_node = index_start.model().mapToSource( index_start ).internalPointer()
end_node = index_end.model().mapToSource( index_end ).internalPointer()
# get the nodes to operate on ...
nodes = [ start_node ]
if not index_start == index_end:
nodes_above = list()
nodes_below = list()
index_above = self.gui_tree.indexAbove( index_start )
index_below = self.gui_tree.indexBelow( index_start )
while not end_node in nodes_above and not end_node in nodes_below:
if index_above.isValid():
node_after = index_above.model().mapToSource( index_above ).internalPointer()
nodes_above.append( node_after )
index_above = self.gui_tree.indexAbove( index_above )
if index_below.isValid():
node_before = index_below.model().mapToSource( index_below ).internalPointer()
nodes_below.append( node_before )
index_below = self.gui_tree.indexBelow( index_below )
if end_node in nodes_above:
nodes += nodes_above
if end_node in nodes_below:
nodes += nodes_below
# get the operation, based on index_start column ...
clicked_column = index_start.column()
if not clicked_column == 0:
attr = COLUMNS[ clicked_column ].get( 'attr_name' )
else:
attr = 'selected'
if not keyboard_modifiers == QtCore.Qt.ControlModifier and \
not keyboard_modifiers == QtCore.Qt.AltModifier:
self.model_data_objs.deselect_all()
# perform the operation
new_value = not getattr( start_node, attr )
for node in nodes:
setattr( node, attr, new_value )