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

时间:2015-12-22 15:22:52

标签: python performance pyside qtreeview

我遇到了一个令我难过的问题,我想知道是否有任何已知的解决方法。似乎在使用子类 QAbstractItemView QTreeView 上执行选择可能非常非常慢。

例证;我修改了使用PySide安装的示例文件:

..\site-packages\PySide\examples\itemviews\simpletreemodel\simpletreemodel.py有大约4000行数据项,高于原来的40行。

我还将树视图设置为QtGui.QAbstractItemView.SelectionMode.ExtendedSelection。运行这会导致gui交互最好是缓慢而且当“大”选择时非常慢。这通过鼠标操作,键盘操作和脚本操作来实现。

关于后者,我在修改后的脚本中添加了view.selectAll()并对其进行了分析,以显示选择所有项目需要约84秒。

我正在考虑禁用选择并编写自己的自定义 Selection_Manager ,看看我是否可以手动加速。有没有人对如何更快地制定标准工作流程有任何其他建议或想法/例子?

提前致谢。

这是展示此问题的脚本。

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

2 个答案:

答案 0 :(得分:1)

我使用Python profiler运行您的示例。总共需要大约86秒,其中51个是index listWed 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} 方法的花费。以下是分析器的输出,按时间排序,并以10个功能切断。

list.index()

TreeItem.row()中调用TreeItem方法。因此,要确定项目的行号,将遍历其父项的子项列表,直到找到该项目。这总是让我觉得效率低下但到目前为止我从未遇到过性能问题。

因此,似乎可能的优化是将行号存储在row中。这当然会使用额外的内存,如果更新树,则需要确保它保持一致。

另一个角度是研究为什么经常调用indexQTreeView.selectAll函数(大约三百万次,这对我来说似乎很多)。也许您可以通过查看var a: String!的Qt源代码找到答案。

祝您好运,如果您找到解决方案,请告诉我。

P.S。我过去手工制作过QItemSelection,请参阅this post。我认为它不会对你有所帮助。

答案 1 :(得分:1)

我最终做了我的建议,禁用了QTreeView中的选择并自行管理选择。选择所有时间下降到大约2秒。仍然很高,但更好。

my_qtreeview.setSelectionMode( QtGui.QAbstractItemView.SelectionMode.NoSelection )

我的完整解决方案不会被分解和模块化,以便在此处发布任何可立即重复使用的内容,但也许这个代码段对任何感兴趣的人都有帮助。

一些注意事项:

  • 我的QAbstractItemModel中有自定义的python节点。我利用&#34;。选择&#34;在每个中存储其选定的状态。
  • 在重写的QAbstractItemModel.data()方法中,当一个&#39; node.selected = True&#39;我将其背景颜色设置为表示选择了行的颜色。
  • 我最终需要手动处理键盘修饰符。我还需要跟踪点击的项目与项目发布。我还要考虑选择任何非左键单击鼠标。我在子类QTreeView中的mousePressEvent()和mouseReleaseEvent()中执行此操作。
  • 我有多个列,有些是单击时不应选择的图标,而是在视图显示的对象上切换某些属性。 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 )