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()
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))
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()
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)
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()
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))
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()
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
andQt::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)