sortingnumber-formattingqtableviewalphabetical-sort

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

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