Natsukane
Natsukane

Reputation: 681

QTreeWidget - excluding top level items from sort

I have a 2-level QTreeWidget. The top level only has one column and is just used for grouping the second level, which holds the actual multi-column data.

When I sort by clicking on a header (or any other way, really), I only want the second level to be sorted, as the top-level items have a fixed order set elsewhere in the app and shouldn't be affected.

How do I achieve this?

Upvotes: 3

Views: 4557

Answers (3)

ekhumoro
ekhumoro

Reputation: 120738

The simplest solution is to create a subclass of QTreeWidgetItem and reimplement its __lt__ method so that it always returns False. Qt uses a stable sort algorithm, so this means the items will always retain their original order. This subclass should be used for the top-level items only.

Here is a working demo that seems to meet your spec:

import sys
from random import randint
from PyQt4 import QtCore, QtGui

class TreeWidgetItem(QtGui.QTreeWidgetItem):
    def __lt__(self, other):
        return False

class Window(QtGui.QTreeWidget):
    def __init__(self):
        super(Window, self).__init__()
        self.setHeaderLabels('Name X Y'.split())
        for text in 'One Two Three Four'.split():
            parent = TreeWidgetItem([text])
            for text in 'Red Blue Green Yellow'.split():
                child = QtGui.QTreeWidgetItem([
                    text, str(randint(0, 9)), str(randint(0, 9))])
                parent.addChild(child)
            self.addTopLevelItem(parent)
        self.expandAll()
        self.setSortingEnabled(True)
        self.sortByColumn(0, QtCore.Qt.AscendingOrder)

if __name__ == "__main__":

    app = QtGui.QApplication(sys.argv)
    window = Window()
    window.setGeometry(800, 100, 320, 400)
    window.show()
    sys.exit(app.exec_())

Upvotes: 3

Ceppo93
Ceppo93

Reputation: 1046

You can use the sortChildren method of QTreeWidgetItem(s) like this:

sort_column = 1
# Iterate top-level elements
for index in range(tree.topLevelItemCount()):
    # Obtain the actual top-level item
    top_level_item = tree.topLevelItem(index)
    # Sort
    top_level_item.sortChildren(sort_column)

Notice that if you add new children to an item you need to sort it again (only the changed item)

EDIT

Thanks, that's a step in the right direction, but the question now is how to override the default sort behavior. I'm doing self.treeWidget.header().sortIndicatorChanged.connect(self.s‌​ortChanged), but since that's a signal it just triggers after the widget does its regular sort, so in the end it has no effect - the top level items get sorted before the function is called

Unless you go for a custom model/view, you may try two approaches (I don't have tested them):

1. Override the "sorting" method of the QTreeWidget

try to replace the sortItems() method with some no-op, but probably the widget have other private methods that are called internally.

2. Override the root-item sortChildren()

It's possible that the sorting is implemented calling the (hidden) root-item sortChildren(), that will sort the top-level-items and call the sortChildren() of those. You may be able to figure out how to get/set this item using rootIndex()/setRootIndex() and then override the sort method (e.g. root.sortChildren = lambda c, o: None).

Upvotes: 1

Natsukane
Natsukane

Reputation: 681

I found a solution based on the answer for this question. Catch the sectionClicked signal and check if the clicked column is the first one. If so, force the static sort direction and then manually sort the children. Also needed a variable to manually track the sort direction for that column. Relevant code:

self.lastSortDir = 1
self.treeWidget.header().sectionClicked.connect(self.headerSectionClicked)

def headerSectionClicked(self, colId):
    flip = {0:1, 1:0}
    if colId == 0:
        self.treeWidget.header().setSortIndicator(0,1)
        sortDir = flip[self.lastSortDir]
        for gId in self.groupWidgets:
            self.groupWidgets[gId].sortChildren(0, sortDir)
        self.lastSortDir = sortDir

This still isn't ideal for two main reasons:

  1. Every time you sort by the first column, two sorts will be performed, one to force the static direction and then another for the children. Could be an issue with larger data sets and is just inelegant.

  2. The sort indicator for the first column will be stuck displaying the static sort direction instead of the actual direction the children are sorted by. Mostly just a cosmetic issue but it still triggers me.

Not ideal, but considering everything else I tried didn't work at all, I'll take it for now. I wasted enough time on it already, and imperfect but working is better than perfect but not working.

Upvotes: 0

Related Questions