'QtableView column sorting with floats formatted as strings

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_()


Solution 1:[1]

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_()

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Stéphane REY