pythonqtsortingpyside6qtreewidget

Sort QTreeWidget alphabetically, except one item


I have a QTreeWidget with items. One of the items I use it to provide a "Select All" option.

I would like to keep that "Select All" item pinned on top, and then sort all other items below alphabetically. I tried creating a ROLE for the "Select All" item to indicate that this one has to be always on top. So I override the lt method to return False whenever the skip_sorting is enabled, else sort alphabetically (see code below). The issue I am having is that if I print the comparisons that are called ( print(self.text(column), other.text(column))), self is always the "Select All" item, but I don't see the comparison between other items. I am using pyside6 6.3.2

class TreeWidgetItem(QTreeWidgetItem):
    """
    Sort items alphabetically but exclude the "Select All" on top
    """

    def __lt__(self, other):
        column = self.treeWidget().sortColumn()
        print(self.text(column), other.text(column))

        skip_sorting_self = self.data(column, _SKIP_SORT_ROLE)
        skip_sorting_other = other.data(column, _SKIP_SORT_ROLE)

        if skip_sorting_self and not skip_sorting_other:
            return False

        return self.text(column).lower() < other.text(column).lower()

enter image description here


Update:

Here there is a working example of what is failing:

import sys
from typing import Optional

from PySide6.QtCore import Qt
from PySide6.QtWidgets import QApplication, QTreeWidget, QTreeWidgetItem, QWidget

_SKIP_SORT_ROLE: int = (Qt.UserRole + 4)


class TreeWidgetItem(QTreeWidgetItem):
    """
    Sort items alphabetically but exclude the Select All
    """

    def __lt__(self, other):
        column = self.treeWidget().sortColumn()

        skip_sorting_self = self.data(column, _SKIP_SORT_ROLE)
        text = self.text(column)
        if skip_sorting_self:
            text = ''  # lowest possible

        return text > other.text(column).lower()


class TreeWidget(QTreeWidget):

    def __init__(self, parent: Optional[QWidget] = None):
        super(TreeWidget, self).__init__(parent)

        item = TreeWidgetItem(self)
        item.setText(0, 'Select All')
        item.setData(0, _SKIP_SORT_ROLE, True)

        for i in range(10):
            item = TreeWidgetItem(self)
            item.setText(0, f'Item {i}')

        for i in range(10):
            item = TreeWidgetItem(self)
            item.setText(0, f'Item {i}')

        self.setSortingEnabled(True)


if __name__ == '__main__':
    app = QApplication(sys.argv)
    tree = TreeWidget()
    tree.show()
    sys.exit(app.exec())

See the screenshot:

enter image description here


Solution

  • There are two problems.

    First of all, the comparison is not correct, because you're comparing the left-hand text as it is against the lower() of the right-hand text.
    The operator is also wrong, since __lt__ should return the result of <, not >. If you want to specify a different sorting order, then properly call sortByColumn(), as the default order is the descending one.

    Then, since you want to have a specific item always on top, no matter the order, making that comparison against an empty string is obviously insufficient, because the result of the __lt__ operator is negated when the sorting is inverted.

    Finally, the result of the "on top" item comparison cannot just consider the left-hand object, because the sorting may just query against the "other" items.

    The concept is that if the sorting order is ascending, then the result of the "unsorted item" is always True, so that it is always the first. If the order is descending, then it must return False so that it is always "last" (but since the sorting is reversed, it will then be the first).

    If the comparison is on the right hand side, then the sorting is inverted, so the opposite result must be returned instead.

    Here is a more appropriate implementation:

        def __lt__(self, other):
            column = self.treeWidget().sortColumn()
            if self.data(column, _SKIP_SORT_ROLE):
                order = self.treeWidget().header().sortIndicatorOrder()
                # if sorting is ascending, "less than" must be True, so that 
                # this is always the first item; if it's descending, then it
                # must be False, so that it's the "last"
                return order == Qt.SortOrder.AscendingOrder
            elif other.data(column, _SKIP_SORT_ROLE):
                # the opposite of the above
                order = self.treeWidget().header().sortIndicatorOrder()
                return order != Qt.SortOrder.AscendingOrder
    
            # proper text comparison
            return self.text(column).lower() < other.text(column).lower()
    

    Note, though, that sorting in tree views is quite peculiar, and if you need sub-level sorting then the above may require further checking depending on the parent level.