How to customize sorting behaviour in QTableWidget

Question:

How do I alter the column sorting behavior so that instead of sorting alphabetically like this:

1, 11.1, 2, 22.2, 3, etc.

it will sort numerically, like this:

1, 2, 3, 11.1, 22.2, etc

I need to alter the sorting behavior only on specific columns and not to the whole table, as I want to keep the normal alphabetic ordering for names.

I also have a column that has file sizes. They are represented in MB and GB, and I want to sort it so that 800 MB will come first and 1.4 GB will come after. It also has the problem of alphabetic sorting. Is there a method that can do this?

Asked By: Steve Hemmingsen

||

Answers:

Probably the simplest and most flexible way to do this is to store the raw values (for sorting) alongside the formatted values (for display). You can then subclass QTableWidgetItem and reimplement its less than operator, so that only the raw values are compared when sorting. This subclass can be used selectively for whichever columns need special sorting.

For this to work correctly with file sizes, you will need to store the raw values as byte-counts so that all formats can be compared in the same way.

Here’s a simple demo that shows how to implement this:

import sys
from PyQt5 import QtCore, QtWidgets

class NumericItem(QtWidgets.QTableWidgetItem):
    def __lt__(self, other):
        return (self.data(QtCore.Qt.UserRole) <
                other.data(QtCore.Qt.UserRole))

class Window(QtWidgets.QTableWidget):
    def __init__(self):
        super(Window, self).__init__(6, 3)
        for column, values in enumerate((
            ('Red', 'Green', 'Yellow', 'Blue', 'White', 'Black'),
            (1, 11.1, 2, 22.2, 3, 17),
            (37885792, 755, 25504, 4805, 3751225472, 14529792),
            )):
            for row, value in enumerate(values):
                if column == 0:
                    item = QtWidgets.QTableWidgetItem(value)
                else:
                    if column == 1:
                        text = str(value)
                    else:
                        text = self.formatSize(value)
                    item = NumericItem(text)
                    item.setData(QtCore.Qt.UserRole, value)
                self.setItem(row, column, item)
        self.setSortingEnabled(True)
        self.sortItems(0, QtCore.Qt.AscendingOrder)

    def formatSize(self, size, precision=2):
        if size < 0:
            return ''
        if size < 1024:
            return '%.0f B' % size
        size /= 1024.0
        if size < 1024:
            return '%.*f KiB' % (precision, size)
        size /= 1024
        if size < 1024:
            return '%.*f MiB' % (precision, size)
        return '%.*f GiB' % (precision, size / 1024)

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 100, 350, 250)
    window.show()
    sys.exit(app.exec_())
Answered By: ekhumoro

Here is a PyQt6 version of the great sample by @ekhumoro
(fixes eg AttributeError: type object 'Qt' has no attribute 'UserRole'
and AttributeError: type object 'Qt' has no attribute 'AscendingOrder')
:

import sys
from PyQt6 import QtCore, QtWidgets

class NumericItem(QtWidgets.QTableWidgetItem):
    def __lt__(self, other):
        return (self.data(QtCore.Qt.ItemDataRole.UserRole) <
                other.data(QtCore.Qt.ItemDataRole.UserRole))

class Window(QtWidgets.QTableWidget):
    def __init__(self):
        super(Window, self).__init__(6, 3)
        for column, values in enumerate((
            ('Red', 'Green', 'Yellow', 'Blue', 'White', 'Black'),
            (1, 11.1, 2, 22.2, 3, 17),
            (37885792, 755, 25504, 4805, 3751225472, 14529792),
            )):
            for row, value in enumerate(values):
                if column == 0:
                    item = QtWidgets.QTableWidgetItem(value)
                else:
                    if column == 1:
                        text = str(value)
                    else:
                        text = self.formatSize(value)
                    item = NumericItem(text)
                    item.setData(QtCore.Qt.ItemDataRole.UserRole, value)
                self.setItem(row, column, item)
        self.setSortingEnabled(True)
        self.sortItems(0, QtCore.Qt.SortOrder.AscendingOrder)

    def formatSize(self, size, precision=2):
        if size < 0:
            return ''
        if size < 1024:
            return '%.0f B' % size
        size /= 1024.0
        if size < 1024:
            return '%.*f KiB' % (precision, size)
        size /= 1024
        if size < 1024:
            return '%.*f MiB' % (precision, size)
        return '%.*f GiB' % (precision, size / 1024)

if __name__ == '__main__':

    app = QtWidgets.QApplication(sys.argv)
    window = Window()
    window.setGeometry(600, 100, 350, 250)
    window.show()
    sys.exit(app.exec())

Answered By: eddygeek