1

Suppose I have a QTreeWidget with three columns. Two of which with string values and a third with integral values, all of which may appear more than once in each column.

  • Username (str)
  • Product (str)
  • Quantity (int)

I then want to be able to sort these items by either username or product, and to have rows that share these values to be sorted by quantity.

As a side note, I also need to be able to sort the values of the hypothetical quantity as numeric values.

Imagine I had three rows sorted by quantity in the previous example and that those rows had these values:

  • 1
  • 2
  • 10

I would then want these rows to be sorted in that same order, not as they would be if they were sorted as string values:

  • 1
  • 10
  • 2

How do I implement this combination using PyQt5?

darxtR
  • 31
  • 8
  • The built-in way is to use `QSortFilterProxyModel` https://forum.qt.io/topic/1330/is-it-possible-to-sort-on-multiple-columns-in-qtableview-using-qsortfilterproxymodel – Joe Dec 27 '19 at 09:02
  • https://www.walletfox.com/course/qsortfilterproxymodelexample.php – Joe Dec 27 '19 at 09:04
  • https://www.qtcentre.org/threads/24267-QSortFilterProxyModel-setFilterRegExp-for-more-than-1-column – Joe Dec 27 '19 at 09:05
  • Does this answer your question? [Use a QSortFilterProxyModel from QML with PyQt5](https://stackoverflow.com/questions/36823456/use-a-qsortfilterproxymodel-from-qml-with-pyqt5) – Joe Dec 27 '19 at 09:07
  • https://stackoverflow.com/questions/36823456/use-a-qsortfilterproxymodel-from-qml-with-pyqt5 – Joe Dec 27 '19 at 09:07
  • An example can be found here https://github.com/baoboa/pyqt5/blob/master/examples/itemviews/basicsortfiltermodel.py – Joe Dec 27 '19 at 09:08
  • @Joe In QTableWidgets, QTreeWidgets and QListwidget you cannot use a QSortFilterProxyModel since you cannot replace the model in the view, so Qt uses `__lt__` – eyllanesc Dec 27 '19 at 19:15
  • @eyllanesc, yes, you are right More info can be found [here](https://stackoverflow.com/questions/34252413/how-to-create-a-filter-for-qtablewidget) and [here](https://doc.qt.io/qt-5/qtwidgets-itemviews-customsortfiltermodel-example.html) and [here](https://doc.qt.io/qt-5/model-view-programming.html#proxy-models) – Joe Dec 27 '19 at 20:52
  • As @eyllanesc already said: https://stackoverflow.com/questions/34252413/how-to-create-a-filter-for-qtablewidget#comment56324640_34286827 – Joe Dec 27 '19 at 20:56
  • @Joe If you realize in QSortFilterProxyModel it only works for QListView, QTableView and QTreeView but it is not applicable for QListWidget, QTableWidget and QTreeWidget as your examples show. – eyllanesc Dec 27 '19 at 23:06

1 Answers1

2

Foreword

I'm not a big fan of long answers, and even loss a fan of long pieces of hard to read code, but these are none the less the solution(s) that I came up with when looking for an answer to this question myself a while back.

Simple

This first piece of code is basically a very simplified solution of what I used in the end. It's more efficient and, more importantly, much more easy to read and understand.

from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem


class SimpleMultisortTreeWidget(QTreeWidget):

    def __init__(self, *a, **k):
        super().__init__(*a, **k)
        self._csort_order = []
        self.header().sortIndicatorChanged.connect(self._sortIndicatorChanged)
    
    def _sortIndicatorChanged(self, n, order):
        try:
            self._csort_order.remove(n)
        except ValueError:
            pass
        self._csort_order.insert(0, n)
        self.sortByColumn(n, order)


class SimpleMultisortTreeWidgetItem(QTreeWidgetItem):
    
    def __lt__(self, other):
        corder = self.treeWidget()._csort_order
        return list(map(self .text, corder)) < \
               list(map(other.text, corder))

Extended

I also had the need to...

  • Sort some columns as integers and/or decimal.Decimal type objects.
  • Mix ascending and descending order (i.e. mind the Qt.SortOrder set for each column)

The following example is therefore what I ended up using myself.

from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem


class MultisortTreeWidget(QTreeWidget):
    u"""QTreeWidget inheriting object, to be populated by
    ``MultisortTreeWidgetItems``, that allows sorting of multiple columns with
    different ``Qt.SortOrder`` values.
    """

    def __init__(self, *arg, **kw):
        r"Pass on all positional and key word arguments to super().__init__"
        super().__init__(*arg, **kw)
        self._csort_corder = []
        self._csort_sorder = []
        self.header().sortIndicatorChanged.connect(
            self._sortIndicatorChanged
        )

    def _sortIndicatorChanged(self, col_n, order):
        r"""
        Update private attributes to reflect the current sort indicator.
        
        (Connected to self.header().sortIndicatorChanged)
        
        :param col_n: Sort indicator indicates column with this index to be
        the currently sorted column.
        :type  col_n: int
        :param order: New sort order indication. Qt enum, 1 or 0.
        :type  order: Qt.SortOrder
        """
        # The new and current column number may, or may not, already be in the
        # list of columns that is used as a reference for their individual
        # priority.
        try:
            i = self._csort_corder.index(col_n)
        except ValueError:
            pass
        else:
            del self._csort_corder[i]
            del self._csort_sorder[i]
        # Force current column to have highest priority when sorting.
        self._csort_corder.insert(0, col_n)
        self._csort_sorder.insert(0, order)
        self._csort = list(zip(self._csort_corder,self._csort_sorder))
        # Resort items using the modified attributes.
        self.sortByColumn(col_n, order)


class MultisortTreeWidgetItem(QTreeWidgetItem):
    r"""QTreeWidgetÍtem inheriting objects that, when added to a
    MultisortTreeWidget, keeps the order of multiple columns at once. Also
    allows for column specific type sensitive sorting when class attributes
    SORT_COL_KEYS is set.
    """
    
    @staticmethod
    def SORT_COL_KEY(ins, c):
        return ins.text(c)

    SORT_COL_KEYS = []
    
    def __lt__(self, other):
        r"""Compare order between this and another MultisortTreeWidgetItem like
        instance.
 
        :param other: Object to compare against.
        :type  other: MultisortTreeWidgetItem.
        :returns: bool
        """
        # Fall back on the default functionality if the parenting QTreeWidget
        # is not a subclass of MultiSortTreeWidget or the SortIndicator has not
        # been changed.
        try:
            csort = self.treeWidget()._csort
        except AttributeError:
            return super(MultisortTreeWidgetItem, self).__lt__(other)
        # Instead of comparing values directly, place them in two lists and
        # extend those lists with values from columns with known sort order.
        order = csort[0][1]
        left  = []
        right = []
        for c, o in csort:
            try:
                key = self.SORT_COL_KEYS[c]
            except (KeyError, IndexError):
                key = self.SORT_COL_KEY
            #  Reverse sort order for columns not sorted according to the
            # current sort order indicator.
            if o == order:
                left .append(key(self , c))
                right.append(key(other, c))
            else:
                left .append(key(other, c))
                right.append(key(self , c))
        return left < right

Usage

The static method SORT_COL_KEY and the SORT_COL_KEYS class attribute of the above stated MultisortTreeWidgetItem class also allow for other values than those returned by self.text(N) to be used, for example a list returned by self.data().

The following example sort the text in the rows of the first column as integers and sorts the rows of the third column by the corresponding object in a list returned by self.data(). All other columns is sorted by the item.text() values, sorted as strings.

class UsageExampleItem(MultisortTreeWidgetItem):
    
    SORT_COL_KEYS = {
        0: lambda item, col: int(item.text(col)),
        2: lambda item, col: item.data()[col],  
        5: lambda item, col: int(item.text(col) or 0) # Empty str defaults to 0
    }

Create a MultisortTreeWidget object and add it to a layout, then create UsageExampleItems and add them to the MultisortTreeWidget.

This solution "remembers" the columns and sort order used previously. So, if you want to sort the items in a UsageExampleItems widget by the values in the first column, and have rows that share a value to be sorted by the second column among themselves, then you would first click on the header item of the second column and then proceed to click on the header item of the first column.

darxtR
  • 31
  • 8
  • Calling `sortByColumn` from `sortIndicatorChanged` event leads to an endless recursion in my case. – ababak Feb 11 '22 at 09:20