如何使用 PySide.QtCore.QAbstractItemModel 和 QTreeView 提高选择性能?

How to improve selection performance with PySide.QtCore.QAbstractItemModel and QTreeView?

我 运行 遇到了一个困扰我的问题,我想知道是否有任何已知的解决方法。似乎在使用子类 QAbstractItemViewQTreeView 上执行 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 中。这当然会使用额外的内存,如果您更新树,您需要确保它保持一致。

另一个角度是研究为什么rowindex函数被调用的频率如此之高(大约三百万次,我觉得很多)。或许你可以看看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 )