开发者

How can I efficiently expand an entire subtree of a QTreeView?

EDIT: it turns out that the root performance problem was a size-to-fit function attached to the expanded() signal, so I'm going to accept the first answer and delete this question for being misleading.

Note: I'm asking this question so I can provide an answer for it (and maybe get a better answer). The solution is not intuitive.

MacOS builds of Qt may have a way for the user to expand an entire QTreeView subtree (there was an open bug for it) but non-MacOS builds definitely do not. I am trying to implement the behavior "shift-click on the item decoration expands the entire subtree".

There are two problems. The easier of the two is detecting a shift-click on the decoration. I do this by intercepting the expanded/collapsed signals; they check some "global" state set up by the mousePressEvent:

# similar implementation for _on_expanded
@pyqtSlot(QModelIndex)
def _on_collapsed(self, index):
    if self._in_shift_press:
        self._in_shift_press = False
        _recursive_set_expanded(self, index, False)

def mousePressEvent(self, evt):
    # Make shift-click expand/collapse all
    if int(evt.modifiers() & Qt.ShiftModifier) != 0:
        self._in_shift_press = True
    try:
        QTreeView.mousePressEvent(self, evt)
    finally:
        self._in_shift_press = False

This is a little ugly, but it works well enough.

The harder problem is implementing _recursive_set_expanded(view, root, state). A recursive collapse is very quick. However, the obvious implementation of calling view.setExpanded(True) on all descendents of an index is very very slow -- multiple seconds for ~100 indices. The problem is not an expensive data model, since view.expandAll() is very fast.

Some source diving shows that expandAll() does significantly less work than expand(). However, the Qt API doesn't expose an expandSubtree()开发者_Python百科 method. How can this be made fast, short of digging into the private implementation?


Pressing the '*' (asterisk) key expands a node's subnodes, which is just what you want. Have you tried calling keyPressEvent with a fake '*' keypress?


Since expandAll() is fast, and collapse(QModelIndex) is fast, my solution is to use only those two methods and avoid calling expand(QModelIndex):

def _recursive_set_expanded(view, root, desired):
    """For |root| and all its descendents, set the 'expanded' state to |desired|.
    It is undefined whether expanded and collapsed signals are emitted."""
    state = {}
    def _recursive_get_state(view, model, index, under_root):
        if index == root:
            under_root = True
        num_children = model.rowCount(index)
        if num_children:
            if under_root:
                state[index] = desired
            else:
                state[index] = view.isExpanded(index)
            for i in xrange(model.rowCount(index)):
                _recursive_get_state(view, model, model.index(i,0, index), under_root)

    _recursive_get_state(view, view.model(), QModelIndex(), False)
    view.expandAll()
    for k,v in state.iteritems():
        if not v:
            view.setExpanded(k, False)


void MainWindow::expandNode(const QModelIndex &parentIndex, bool expand) {
  tree->setExpanded(parentIndex, expand);
  for (qint32 rowNum = 0; rowNum < treeModel->rowCount(parentIndex); ++rowNum) {
    QModelIndex childIndex = treeModel->index(rowNum, 0, parentIndex);
    tree->setExpanded(childIndex, expand);
    expandNode(childIndex);
  }
}


Since I just had a performance problem while trying to expand subtrees and this question is the first google result, I am going to share my insights, although I am using C++ and Qt5.

TonyK's solution with calling keyPressEvent works. However, this solution felt hacky to me, so I checked the Qt source, and it is indeed hardwired to the Qt::Key_Asterisk key. (Right in QTreeView::keyPressEvent, if you want to see for yourself.)

QTreeView::keyPressEvent just calls expand(), in a depth-first manner, nothing special, no private api calls. And surely, when I just copied the code from keyPressEvent so my function, it worked:

QModelIndex current = currentIndex();
QStack<QModelIndex> parents;
parents.push(current);
QAbstractItemModel * mod = model();
while (!parents.isEmpty()) {
    QModelIndex parent = parents.pop();
    for (int row = 0; row < mod->rowCount(parent); ++row) {
        QModelIndex child = mod->index(row, 0, parent);
        if (!child.isValid())
            break;
        parents.push(child);
        expand(child);
    }
}
expand(current);

However I wanted to create a setExpandedRecursive(QModelIndex index, bool expanded) function, so I checked their difference to my code, and its simply that they call expand() for the subtree's root node last instead of first. So I did that too in my function, and guess what, now collapsing was the problem instead of expanding. So here is my final solution that works for expanding and collapsing subtrees:

void TreeViewClass::setExpandedRecursive(QModelIndex index, bool expanded)
{
    if (!index.isValid())
        return;

    const QAbstractItemModel * model = index.model();
    if (!expanded)
        setExpanded(index, expanded);
    for (int row = 0, count = model->rowCount(index); row < count; ++row)
        setExpandedRecursive(model->index(row, 0, index), expanded);
    if (expanded)
        setExpanded(index, expanded);
}


QTreeWidget runs fastest when all UI elements are hidden, or at least not currently expanded.

Firstly, get your QTreeWidgetItem children using a generator, which can be faster than using a list.

def get_all_descendents(self, tree_widget_item):
    queue = [tree_widget_item]

    while queue:
        parent = queue.pop(0)
        for child_index in range(parent.childCount()):

            child = parent.child(child_index)
            yield child
            queue.append(child)

Now reverse that generated list when instantiating.

children = reversed(list(self.get_all_descendents(item)))

Reversing the generator list is important, because when we set item.setExpanded(True) the item will expand only through code, and not visually. Meaning Qt will not try to repaint the UI, slowing it down.

Then concatenate the for loop to speed up that process:

[child.setExpanded(item.isExpanded()) for child in children]

Additionally, you can setUpdatesEnabled(False) and blockSignals(True) on the QTreeWidget around this function to speed things up a little, as it might be the QTreeWidgetItem individually firing off signals slowing down your process.

Finally, this is how I'm currently using this setup:

# Set connections
self.module_tree_widget.itemExpanded.connect(self._expand_collapse_children)
self.module_tree_widget.itemCollapsed.connect(self._expand_collapse_children)

def get_all_descendents(self, tree_widget_item):
    queue = [tree_widget_item]
    while queue:

        parent = queue.pop(0)
        for child_index in range(parent.childCount()):

            child = parent.child(child_index)
            yield child
            queue.append(child)

def _expand_collapse_children(self, item):
    modifiers = QtWidgets.QApplication.keyboardModifiers()
    if modifiers == QtCore.Qt.ShiftModifier:

        self.tree_widget.setUpdatesEnabled(False)
        self.tree_widget.blockSignals(True)

        children = reversed(list(self.get_all_descendents(item)))
        [child.setExpanded(item.isExpanded()) for child in children]

        self.tree_widget.setUpdatesEnabled(True)
        self.tree_widget.blockSignals(False)

This will expand all QTreeWidgetItem children when holding the shift key and expanding any QTreeWidgetItem parent.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜