Why can't I sort columns in my PyQt5 QTableWidget using UserRole data?

Question:

I am trying to sort my QTableWidget columns by the values stored in the user role of each QTableWidgetItem, but I am unable to do so. I have enabled sorting with self.setSortingEnabled(True), and I have set the data in each QTableWidgetItem with item.setData(Qt.DisplayRole, f'M - {r}') and item.setData(Qt.UserRole, r). However, when I try to sort the columns by the values stored in the user role, it sorts the columns by the values stored in the display role instead.

Here is a minimal working example of my code:

from random import randint

from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QApplication, QMainWindow, QTableWidget, QWidget, QGridLayout, 
    QTableWidgetItem, QPushButton


class Table(QTableWidget):
    def __init__(self):
        super().__init__()
        self.setSortingEnabled(True)

    def populate(self):
        self.clear()
        self.setColumnCount(3)
        self.setRowCount(200)
        for row in range(500):
            for column in range(3):
                r = randint(0, 1000)
                item = QTableWidgetItem()
                item.setData(Qt.DisplayRole, f'M - {r}')
                item.setData(Qt.UserRole, r)
                self.setItem(row, column, item)


class MainApp(QMainWindow):
    def __init__(self):
        super().__init__()
        self.table = Table()
        self.button = QPushButton('Roll')
        self.button.clicked.connect(self.table.populate)

        layout = QWidget()
        self.setCentralWidget(layout)
        grid = QGridLayout()
        layout.setLayout(grid)

        grid.addWidget(self.button)
        grid.addWidget(self.table)


if __name__ == '__main__':
    app = QApplication([])
    main_app = MainApp()
    main_app.showMaximized()
    app.exec()

Example

Additionally, I tried using EditRole, but the values that appear in the table are not the values from DisplayRole. For example, in the code below, I set item.setData(Qt.DisplayRole, f’M – {r}’), but even though r is an integer, the display role value is a string (‘M – {r}’). I was hoping that sorting by UserRole or EditRole would sort based on the integer value of r, but that doesn’t seem to be the case.

item.setData(Qt.DisplayRole, f'M - {r}')
item.setData(Qt.EditRole, int(r))
Asked By: Collaxd

||

Answers:

Use a QTableView instead. Widgets are meant for very basic use cases. It’s important to invoke setSortRole on the model. Also, your setData arguments were in reverse order, correct is data, role.

from random import randint
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5 import QtWidgets

class MainApp(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()
        self.table_view = QtWidgets.QTableView()
        self.table_view.setSortingEnabled(True)
        
        self.model = QtGui.QStandardItemModel()
        self.model.setSortRole(QtCore.Qt.UserRole)
        self.table_view.setModel(self.model)

        self.button = QtWidgets.QPushButton('Roll')

        layout = QtWidgets.QWidget()
        self.setCentralWidget(layout)
        grid = QtWidgets.QGridLayout()
        layout.setLayout(grid)

        grid.addWidget(self.button)
        grid.addWidget(self.table_view)

        self.button.clicked.connect(
            self.populate
        )

    def populate(self):
        self.table_view.model().clear()
        for _ in range(500):
            r = randint(0, 1000)
            item = QtGui.QStandardItem()
            item.setData(f'M - {r}', QtCore.Qt.DisplayRole)
            item.setData(r, QtCore.Qt.UserRole)
            self.table_view.model().appendRow(item)

if __name__ == '__main__':
    app = QtWidgets.QApplication([])
    main_app = MainApp()
    main_app.showMaximized()
    app.exec()
Answered By: misantroop

Sorting is always based on Qt.DisplayRole.

Trying to use the EditRole is pointless, as the setData() documentation points out:

Note: The default implementation treats Qt::EditRole and Qt::DisplayRole as referring to the same data.

The Qt.UserRole is a custom role that could be used for anything (and containing any type) and by default is not used for anything in Qt. Setting a value with the UserRole doesn’t change the sorting, because Qt knows nothing about its existence or how the value should be used.

Since you are using strings for the sorting, the result is that numbers are not sorted as you may think: for instance "120" is smaller than "13", because "12" comes before "13".

The only occurrence in which the DisplayRole properly sorts number values is when it is explicitly set with a number:

item.setData(Qt.DisplayRole, r)

Which will not work for you, as you want to show the "M – " prefix. Also, a common mistake is trying to use that in the constructor:

item = QTableWidgetItem(r)

And while the syntax is correct, its usage is wrong, as the integer constructor of QTableWidgetItem is used for other purposes.

If you want to support custom sorting, you must create a QTableWidgetItem subclass and implement the < operator, which, in Python, is the __lt__() magic method:

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

Then you have to create new items using that class. Note that:

  • you should always try to use existing items, instead of continuously creating new ones;
  • as explained in the setItem() documentation, you should always disable sorting before adding new items, especially when using a loop, otherwise the table will be constantly sorted at each insertion (thus making further insertion inconsistent);
  • you’re using the a range (500) inconsistent with the row count (200);
  • you should also set an item prototype based on the subclass above;
class Table(QTableWidget):
    def __init__(self):
        super().__init__()
        self.setSortingEnabled(True)
        self.setItemPrototype(SortUserRoleItem())

    def populate(self):
        self.setSortingEnabled(False)
        self.setColumnCount(3)
        self.setRowCount(200)
        for row in range(200):
            for column in range(3):
                r = randint(0, 1000)
                item = self.item(row, column)
                if not item:
                    item = SortUserRoleItem()
                    self.setItem(row, column, item)
                item.setData(Qt.DisplayRole, 'M - {}'.format(r))
                item.setData(Qt.UserRole, r)
        self.setSortingEnabled(True)

Note that, as an alternative, you could use a custom delegate, then just set the value of the item as an integer (as shown above) and override the displayText():

class PrefixDelegate(QStyledItemDelegate):
    def displayText(self, text, locale):
        if isinstance(text, int):
            text = f'M - {text}'
        return text


class Table(QTableWidget):
    def __init__(self):
        super().__init__()
        self.setItemDelegate(PrefixDelegate(self))
        # ...

    def populate(self):
        # ...
                item = self.item(row, column)
                if not item:
                    item = QTableWidgetItem()
                    self.setItem(row, column, item)
                item.setData(Qt.DisplayRole, r)
Answered By: musicamante
Categories: questions Tags: , , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.