Add some explanation on sorting a QTableView

Heads up! You've already completed this tutorial.

David_Hansson | 2020-05-23 18:52:34 UTC | #1

https://www.pythonguis.com/courses/model-views/qtableview-modelviews-numpy-pandas/

Above is a great tutorial. Best on the internet on the subject. Both the content itself, the language that explain it, and the graphics itself. But I think it avoids some of the more difficult parts of the subject, like sorting and filtering, so it would be even better if you could add some code on how the QAbstractTableModel should be coded and what needs to be set for the QTableView (if needed some subclassing), and settings in Mainwindow class for everything to be non-crashable when sorting and filtering, all expanded on your example of the list of lists data structure of couse.

Keep up the good work. This is perhaps the only understandable and up to date websight on pyqt5 out there, for those that are not professionally trained coders.

/Best regards, David


Luca | 2020-05-26 00:36:24 UTC | #2

To add the sort and filter functionalities you can create a QSortFilterProxyModel object. This new object will be interposed between the model and the view. QSortFilterProxyModel works on the indexes so it will allow adding those functionalities without any modification or duplication of the data at the source.

Let me add a string column to the original data of the Martin's tutorial:

python
data = pd.DataFrame([
      [1, 9, 2, "door"],
      [1, 0, -1, "dog"],
      [3, 5, 2, "duck"],
      [3, 3, 2, "dog"],
      [5, 8, 9, "air"],
    ], columns = ['A', 'B', 'C', 'D'], index=['Row 1', 'Row 2', 'Row 3', 'Row 4', 'Row 5'])

The code was:

python
    self.model = TableModel(data)
    self.table.setModel(self.model)

For sorting it would become:

python
    self.model = TableModel(data)

    self.proxyModel = QSortFilterProxyModel()
    self.proxyModel.setSourceModel(self.model)

    self.table.setSortingEnabled(True)

    self.table.setModel(self.proxyModel)

The method setSortingEnabled(True) must be called because sorting is disabled by default.

While for filtering it would become:

python
    self.model = TableModel(data)

    self.proxyModel = QSortFilterProxyModel()
    self.proxyModel.setSourceModel(self.model)

    self.proxyModel.setFilterKeyColumn(3)
    self.proxyModel.setFilterFixedString("dog")
    #self.proxyModel.setFilterWildcard("do")
    #self.proxyModel.setFilterRegExp(QRegExp("do.*"))

    self.table.setModel(self.proxyModel)

The method setFilterKeyColumn() allows you to select the column for the filtering (column indexes start from 0).

For custom sorting you can reimplement the sort() method in the QAbstractTableModel class.

For custom filtering you can reimplement the filterAcceptsRow() or the filterAcceptsColumn() in the QSortFilterProxyModel class.


mike2750 | 2020-06-05 20:35:25 UTC | #3

Thanks for this advise i actually ended up making a killer implementation of this after alot of trial and error and leaving my example in case its helpful for anyone else.

It was a pita putting all the pieces together from this and other tutorials.

this is a pretty workable building block to both show all columns hide some and adjust alot of stuff. I haven't gotten to the contextual shading and fancy stuff yet but its on my todo.

how it looks in my app now with the sorting and filtering. :) Selection_999(569)|233x500

python
import sys
from PyQt5.QtSql import QSqlDatabase, QSqlQuery, QSqlTableModel
from PyQt5.QtCore import *
from PyQt5.QtWidgets import *


class MainWindow(QWidget):
    def __init__(self, parent=None):
        super().__init__(parent)
        self.db = QSqlDatabase.addDatabase("QSQLITE")
        self.db.setDatabaseName("wizardassistant.db")
        self.db.open()
        self.model = QSqlTableModel()
        self.initializedModel()

        self.tableView = QTableView()

        # turn off row numbers
        self.tableView.verticalHeader().setVisible(False)
        # turn off horizontal headers
        # self.tableView.horizontalHeader().setVisible(False)

        # self.tableView.resizeColumnsToContents()
        # self.tableView.resizeRowsToContents()

        self.tableView.horizontalHeader().setDefaultAlignment(Qt.AlignLeft)
        self.source_model = self.model
        # self.initializedModel()
        self.proxy_model = QSortFilterProxyModel(self.source_model)
        self.searchcommands = QLineEdit("")
        self.searchcommands.setObjectName(u"searchcommands")
        self.searchcommands.setAlignment(Qt.AlignLeft)
        self.proxy_model.setSourceModel(self.source_model)
        self.tableView.setModel(self.proxy_model)

        # hide columns from the maintableview
        self.tableView.hideColumn(0)
        self.tableView.hideColumn(3)
        self.tableView.hideColumn(4)
        self.tableView.hideColumn(5)
        self.tableView.hideColumn(6)
        self.tableView.hideColumn(7)
        self.proxy_model.setFilterRegExp(QRegExp(self.searchcommands.text(), Qt.CaseInsensitive,
                                                 QRegExp.FixedString))
        # search all columns
        self.proxy_model.setFilterKeyColumn(-1)

        # enable sorting by columns
        self.tableView.setSortingEnabled(True)

        # set editing disabled for my use
        self.tableView.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tableView.setWordWrap(True)
        self.layout = QVBoxLayout()
        self.command_description = QLabel()
        self.command_description.setWordWrap(True)
        self.command_requires_label = QLabel("Command Requires:")
        self.command_requires_label.setWordWrap(True)
        hLayout = QHBoxLayout()
        hLayout.addWidget(self.command_requires_label)
        self.layout.addWidget(self.tableView)
        self.layout.addWidget(self.searchcommands)
        self.layout.addWidget(self.command_description)
        self.layout.addLayout(hLayout)
        self.setLayout(self.layout)
        self.resize(400, 600)

        self.searchcommands.textChanged.connect(self.searchcommands.update)
        self.searchcommands.textChanged.connect(self.proxy_model.setFilterRegExp)
        self.searchcommands.setText("")
        self.searchcommands.setPlaceholderText(u"search command")
        self.tableView.clicked.connect(self.listclicked)

    def initializedModel(self):
        self.model.setTable("commands")
        # self.model.setEditStrategy(QSqlTableModel.OnFieldChange)
        self.model.setEditStrategy(QSqlTableModel.OnManualSubmit)
        self.model.select()
        self.model.setHeaderData(0, Qt.Horizontal, "ID")
        self.model.setHeaderData(1, Qt.Horizontal, "Category")
        self.model.setHeaderData(2, Qt.Horizontal, "command_alias")
        self.model.setHeaderData(3, Qt.Horizontal, "command")
        self.model.setHeaderData(4, Qt.Horizontal, "requires")
        self.model.setHeaderData(5, Qt.Horizontal, "description")
        self.model.setHeaderData(6, Qt.Horizontal, "controlpanel")
        self.model.setHeaderData(7, Qt.Horizontal, "verification")

    def onAddRow(self):
        self.model.insertRows(self.model.rowCount(), 1)
        self.model.submit()

    def onDeleteRow(self):
        self.model.removeRow(self.tableView.currentIndex().row())
        self.model.submit()
        self.model.select()

    def closeEvent(self, event):
        self.db.close()

    def listclicked(self, index):
        row = index.row()
        cmd = self.proxy_model.data(self.proxy_model.index(row, 3))
        cmd_requires = self.proxy_model.data(self.proxy_model.index(row, 4))
        cmd_description = self.proxy_model.data(self.proxy_model.index(row, 5))
        print(cmd_description)
        self.command_description.setText(cmd_description)
        self.command_requires_label.setText('This command requires being executed via: ' + cmd_requires.upper())

if __name__ == "__main__":
    app = QApplication(sys.argv)
    window = MainWindow()
    window.show()
    sys.exit(app.exec_())

David_Hansson | 2020-06-09 06:01:09 UTC | #4

What Luca wrote above worked. Thank you.


delvian | 2020-10-01 09:42:57 UTC | #5

Hi Luca,

Thanks. I'm able to sort my table now, but my app crashes with a segmentation fault when I try to update the source model. I'm not accessing any indexes directly, just updating the source model with a call to the db. What am I doing wrong?

Regards, Delvian


delvian | 2020-10-01 15:47:30 UTC | #6

I figured it out after reading the C++ docs. The Qt for Python docs doesn't mention that you need to emit the layoutAboutToBeChanged signal first.


ERIC_HUYGEN | 2021-02-23 16:21:27 UTC | #7

Hi

I have a question about the QSortFilterProxyModel used here. When I connect a slot to the QTableView to change the formatting of a cell when clicked, that works fine when no filtering is done, but when I have filtered, the QModelIndex that I get back in my slot is not the expected index.row(), but the one from the filtered table. How would I get the row() of the clicked cell in the original unfiltered model?

python
        self.table_view.clicked.connect(self.cell_clicked)

    def cell_clicked(self, index: QModelIndex, *args, **kwargs):
        print(f"{index.row()=}, {index.column()=}")
        self.proxy_model.sourceModel().toggleDisplayRole(index)

Thanks, Rik


Well done, you've finished this tutorial! Mark As Complete
[[ user.completed.length ]] completed [[ user.streak+1 ]] day streak

Add some explanation on sorting a QTableView was written by Martin Fitzpatrick .

Martin Fitzpatrick has been developing Python/Qt apps for 8 years. Building desktop applications to make data-analysis tools more user-friendly, Python was the obvious choice. Starting with Tk, later moving to wxWidgets and finally adopting PyQt.