I need to use a custom QtableView in python to display and format data.
The example app below shows a table with in the first column floats formatted as strings to get proper number of decimals, second column are pure float displayed so without formatting and the third one are strings.
When clicking on columns I want to sort my data which works fine for strings and floats (columns #2 and #3) but not for my column #1 with formatted floats as strings where it's sorted alphabetically rather than numerically.
I'm googling since a while without finding a way to have something working with QtableView.
Any clue on how to get both floats sorting and decimal formatting ?
Thanks & cheers
Stephane
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import *
# Table model
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
# Set columns headers
self.horizontalHeaders = [''] * 3
self.setHeaderData(0, Qt.Horizontal, "Col #1\nfloats as string")
self.setHeaderData(1, Qt.Horizontal, "Col #2\nfloats")
self.setHeaderData(2, Qt.Horizontal, "Col #3\nstrings")
def data(self, index, role):
value = self._data[index.row()][index.column()]
if role == Qt.DisplayRole:
# convert col #1 from floats to string to get proper number of decimal formatting
if index.column() == 0:
return '%.4f' % value
# otherwise display floats or strings for col #2 and #3
else:
return value
# Align values right
if role == Qt.TextAlignmentRole:
return Qt.AlignVCenter + Qt.AlignRight
def rowCount(self, index):
# The length of the outer list.
return len(self._data)
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return len(self._data[0])
def setHeaderData(self, section, orientation, data, role=Qt.EditRole):
if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
try:
self.horizontalHeaders[section] = data
return True
except:
return False
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return self.horizontalHeaders[section]
except:
pass
return super().headerData(section, orientation, role)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Create a TableView (not a TableWidget !!!)
self.table = QtWidgets.QTableView()
# sample data
data = [
[4.2, 9.6, 1],
[42.1, 0.0, 11],
[3.1, 5.55, 2],
[30.0, 3.55, 2222],
[7.99, 8.99, 33],
]
# Set table model
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
# Use proxy for column sorting
proxyModel = QSortFilterProxyModel()
proxyModel.setSourceModel(self.model)
self.table.setModel(proxyModel)
self.table.setSortingEnabled(True)
# hide vertical headers
self.table.verticalHeader().setVisible(False)
# format horizontal headers
stylesheet = "::section{Background-color:rgb(171,178,185);font-weight:bold}"
self.table.setStyleSheet(stylesheet)
self.table.setAlternatingRowColors(True)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.setMinimumSize(350, 250)
window.setWindowTitle('Sorting column example')
window.show()
app.exec_()
Thanks to a colleague I've found an implementation which works. Basically one has to override the sorting function and not using the QSortFilterProxyModel() function but rewrite your own function this new sorting function will be called and just do a custom sorting
Here is the modified code which now works fine for any type of data.
import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import *
# Table model
class TableModel(QtCore.QAbstractTableModel):
def __init__(self, data):
super(TableModel, self).__init__()
self._data = data
# Set columns headers
self.horizontalHeaders = [''] * 3
self.setHeaderData(0, Qt.Horizontal, "Col #1\nfloats as string")
self.setHeaderData(1, Qt.Horizontal, "Col #2\nfloats")
self.setHeaderData(2, Qt.Horizontal, "Col #3\nstrings")
def data(self, index, role):
value = self._data[index.row()][index.column()]
if role == Qt.DisplayRole:
# convert col #1 from floats to string to get proper number of decimal formatting
if index.column() == 0:
return '%.4f' % value
# otherwise display floats or strings for col #2 and #3
else:
return value
if role == Qt.UserRole:
return value
# Align values right
if role == Qt.TextAlignmentRole:
return Qt.AlignVCenter + Qt.AlignRight
def rowCount(self, index):
# The length of the outer list.
return len(self._data)
def columnCount(self, index):
# The following takes the first sub-list, and returns
# the length (only works if all rows are an equal length)
return len(self._data[0])
def setHeaderData(self, section, orientation, data, role=Qt.EditRole):
if orientation == Qt.Horizontal and role in (Qt.DisplayRole, Qt.EditRole):
try:
self.horizontalHeaders[section] = data
return True
except:
return False
return super().setHeaderData(section, orientation, data, role)
def headerData(self, section, orientation, role=Qt.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return self.horizontalHeaders[section]
except:
pass
return super().headerData(section, orientation, role)
class mysortingproxy(QSortFilterProxyModel):
def __init__(self):
super(mysortingproxy, self).__init__()
def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
leftDqtq = self.sourceModel().data(left, Qt.UserRole)
rightDqtq = self.sourceModel().data(right, Qt.UserRole)
return leftDqtq < rightDqtq
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
# Create a TableView (not a TableWidget !!!)
self.table = QtWidgets.QTableView()
# sample data
data = [
[4.2, 9.6, 1],
[42.1, 0.0, 11],
[3.1, 5.55, 2],
[30.0, 3.55, 2222],
[7.99, 8.99, 33],
]
# Set table model
self.model = TableModel(data)
self.table.setModel(self.model)
self.setCentralWidget(self.table)
# Use proxy for column sorting overriding the QSortFilterProxyModel() function with a custom sorting proxy function
proxyModel = mysortingproxy()
proxyModel.setSourceModel(self.model)
self.table.setModel(proxyModel)
self.table.setSortingEnabled(True)
# hide vertical headers
self.table.verticalHeader().setVisible(False)
# format horizontal headers
stylesheet = "::section{Background-color:rgb(171,178,185);font-weight:bold}"
self.table.setStyleSheet(stylesheet)
self.table.setAlternatingRowColors(True)
if __name__ == '__main__':
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.setMinimumSize(350, 250)
window.setWindowTitle('Sorting column example')
window.show()
app.exec_()