0

I see the documentation at: https://doc.qt.io/qtforpython-6/PySide6/QtWidgets/QTableWidget.html#PySide6.QtWidgets.PySide6.QtWidgets.QTableWidget.cellClicked

I cannot figure out how to emit a clicked signal when a column header is clicked.

 self.table.clicked.connect(lambda: print('Hello'))

This co works on the table cells themselves, not the column headers. I want to use pandas to sort data ascending and replace the table.

I do not see any related calls to get the header cell.

Following the suggested question duplicate, I updated that code for the following:

import pandas as pd
from PySide6 import QtCore, QtWidgets
from PySide6.QtCore import Qt


class PandasTableModel(QtCore.QAbstractTableModel):

    def __init__(self,  parent=None, *args):
        super(PandasTableModel,  self).__init__(parent,  *args)
        self._filters = {}
        self._sortBy = []
        self._sortDirection = []
        self._dfSource = pd.DataFrame()
        self._dfDisplay = pd.DataFrame()

    def rowCount(self,  parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dfDisplay.shape[0]

    def columnCount(self,  parent=QtCore.QModelIndex()):
        if parent.isValid():
            return 0
        return self._dfDisplay.shape[1]

    def data(self, index, role):
        if index.isValid() and role == QtCore.Qt.ItemDataRole.DisplayRole:
            return self._dfDisplay.values[index.row()][index.column()]
        return None

    def headerData(self, col, orientation=QtCore.Qt.Orientation.Horizontal, role=QtCore.Qt.ItemDataRole.DisplayRole):
        if orientation == QtCore.Qt.Orientation.Horizontal and role == QtCore.Qt.ItemDataRole.DisplayRole:
            return str(self._dfDisplay.columns[col])
        return None

    def setupModel(self, header, data):
        self._dfSource = pd.DataFrame(data, columns=header)
        self._sortBy = []
        self._sortDirection = []
        self.setFilters({})

    def setFilters(self, filters):
        self.modelAboutToBeReset.emit()
        self._filters = filters
        self.updateDisplay()
        self.modelReset.emit()

    def updateDisplay(self):

        dfDisplay = self._dfSource.copy()

        # Filtering
        cond = pd.Series(True, index = dfDisplay.index)
        for column, value in self._filters.items():
            cond = cond & \
                (dfDisplay[column].str.lower().str.find(str(value).lower()) >= 0)
        dfDisplay = dfDisplay[cond]

        # Sorting
        if len(self._sortBy) != 0:
            dfDisplay.sort_values(by=self._sortBy,
                                ascending=self._sortDirection,
                                inplace=True)

        # Updating
        self._dfDisplay = dfDisplay

    def sort(self, col, order=QtCore.Qt.SortOrder.AscendingOrder):

        # Storing persistent indexes
        self.layoutAboutToBeChanged.emit()
        oldIndexList = self.persistentIndexList()
        oldIds = self._dfDisplay.index.copy()

        # Sorting data
        column = self._dfDisplay.columns[col]
        ascending = (order == QtCore.Qt.SortOrder.AscendingOrder)
        if column in self._sortBy:
            i = self._sortBy.index(column)
            self._sortBy.pop(i)
            self._sortDirection.pop(i)
        self._sortBy.insert(0, column)
        self._sortDirection.insert(0, ascending)
        self.updateDisplay()

        # Updating persistent indexes
        newIds = self._dfDisplay.index
        newIndexList = []
        for index in oldIndexList:
            id = oldIds[index.row()]
            newRow = newIds.get_loc(id)
            newIndexList.append(self.index(newRow, index.column(), index.parent()))
        self.changePersistentIndexList(oldIndexList, newIndexList)
        self.layoutChanged.emit()
        self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex())


class Ui_TableFilteringDialog(object):
    def setupUi(self, TableFilteringDialog):
        TableFilteringDialog.setObjectName("TableFilteringDialog")
        TableFilteringDialog.resize(400, 300)
        self.verticalLayout = QtWidgets.QVBoxLayout(TableFilteringDialog)
        self.verticalLayout.setObjectName("verticalLayout")
        self.tableView = QtWidgets.QTableView(TableFilteringDialog)
        self.tableView.setObjectName("tableView")
        self.tableView.setSortingEnabled(True)
        self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectionBehavior.SelectRows)
        self.verticalLayout.addWidget(self.tableView)
        self.groupBox = QtWidgets.QGroupBox(TableFilteringDialog)
        self.groupBox.setObjectName("groupBox")
        self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBox)
        self.verticalLayout_2.setObjectName("verticalLayout_2")
        self.formLayout = QtWidgets.QFormLayout()
        self.formLayout.setObjectName("formLayout")
        self.column1Label = QtWidgets.QLabel(self.groupBox)
        self.column1Label.setObjectName("column1Label")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.LabelRole, self.column1Label)
        self.column1Field = QtWidgets.QLineEdit(self.groupBox)
        self.column1Field.setObjectName("column1Field")
        self.formLayout.setWidget(0, QtWidgets.QFormLayout.ItemRole.FieldRole, self.column1Field)
        self.column2Label = QtWidgets.QLabel(self.groupBox)
        self.column2Label.setObjectName("column2Label")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.LabelRole, self.column2Label)
        self.column2Field = QtWidgets.QLineEdit(self.groupBox)
        self.column2Field.setObjectName("column2Field")
        self.formLayout.setWidget(1, QtWidgets.QFormLayout.ItemRole.FieldRole, self.column2Field)
        self.verticalLayout_2.addLayout(self.formLayout)
        self.verticalLayout.addWidget(self.groupBox)

        self.retranslateUi(TableFilteringDialog)
        QtCore.QMetaObject.connectSlotsByName(TableFilteringDialog)

    def retranslateUi(self, TableFilteringDialog):
        _translate = QtCore.QCoreApplication.translate
        TableFilteringDialog.setWindowTitle(_translate("TableFilteringDialog", "Dialog"))
        self.groupBox.setTitle(_translate("TableFilteringDialog", "Filters"))
        self.column1Label.setText(_translate("TableFilteringDialog", "Name"))
        self.column2Label.setText(_translate("TableFilteringDialog", "Occupation"))

class TableFilteringDialog(QtWidgets.QDialog):

    def __init__(self, parent=None):
        super(TableFilteringDialog, self).__init__(parent)

        self.ui = Ui_TableFilteringDialog()
        self.ui.setupUi(self)

        self.tableModel = PandasTableModel()
        header = ['Name', 'Occupation']
        data = [
            ['Abe', 'President'],
            ['Angela', 'Chancelor'],
            ['Donald', 'President'],
            ['François', 'President'],
            ['Jinping', 'President'],
            ['Justin', 'Prime minister'],
            ['Theresa', 'Prime minister'],
            ['Vladimir', 'President'],
            ['Donald', 'Duck']
        ]
        self.tableModel.setupModel(header, data)
        self.ui.tableView.setModel(self.tableModel)

        self.ui.column1Field.textEdited.connect(self.filtersEdited)
        self.ui.column2Field.textEdited.connect(self.filtersEdited)

    def filtersEdited(self):
        filters = {}
        values = [
            self.ui.column1Field.text().lower(),
            self.ui.column2Field.text().lower()
        ]
        for col, value in enumerate(values):
            if value == '':
                continue
            column = self.tableModel.headerData(col, QtCore.Qt.Orientation.Horizontal, QtCore.Qt.ItemDataRole.DisplayRole).value()
            filters[column]=value
        self.tableModel.setFilters(filters)



if __name__ == '__main__':

    import sys
    app = QtWidgets.QApplication(sys.argv)

    dialog = TableFilteringDialog()
    dialog.show()

    sys.exit(app.exec_())

I plan on editing it to match my needs.

David Frick
  • 641
  • 1
  • 9
  • 25
  • That's because the `clicked` signal (which, by the way, is not the same as the `cellClicked` you referenced) only works for *cells of the table*. If you want to access the horizontal **header** then use the [`horizontalHeader()`](//doc.qt.io/qt-6/qtableview.html#horizontalHeader) function, so you could connect to [its signals](https://doc.qt.io/qt-6/qheaderview.html#signals). But that's actually irrelevant, as that's not how sorting is done: that should be implemented within the model's [`sort()`](//doc.qt.io/qt-6/qabstractitemmodel.html#sort) function, or by using a QSortFilterProxyModel. – musicamante Jul 28 '23 at 17:45
  • Also see this related question: [qTableview sorting with PandasModel](https://stackoverflow.com/q/67690242). Note that for standard usage (and large data models), using QSortFilterProxyModel is probably simpler and faster. I strongly suggest you to take your time to read through the [Model/View Programming](https://doc.qt.io/qt-6/model-view-programming.html) guide to better understand how the Qt model/view works, so that you wouldn't risk to try to implement things in the wrong way. – musicamante Jul 28 '23 at 17:48
  • following the first link, I saw https://doc.qt.io/qtforpython-6/considerations.html#qvariant in the documentation but it does not say what to replace it with. The line `When a method expects a QVariant::Type the programmer can use a string (the type name) or the type itself.` implies it should be replaced with type(), no? – David Frick Jul 28 '23 at 23:28
  • not type() as that requires parameters. – David Frick Jul 28 '23 at 23:46
  • https://doc.qt.io/qt-6/qvariant.html#details says it returns the Qtype for that object. However it is not clear what a empty constructor for QVariant should be replaced with. '''QVariant::QVariant() Constructs an invalid variant.''' implies I should return None or throw an error instead. – David Frick Jul 28 '23 at 23:49
  • Don't rely too much on the "Qt for Python" documentation: it's mostly automatically generated from the original C++ docs and is often unreliable unless you know the details about the wrapper and the original implementation. That said, I don't understand why you're looking into QVariant, as all the suggestions I gave you before have absolutely **no** relation to that. Either you implement the `sort()` override as explained in the linked answer, or you just create a QSortFilterProxyModel, set *your* model as its source, and set that proxy as the model for the view. – musicamante Jul 29 '23 at 01:34
  • In any case, QVariant can be *partially* considered as the Qt/C++ counterpart of `object` in Python; it acts as a generic type for *data*. Python bindings (PyQt/PySide) automatically convert QVariants in compatible python types (for instance, QString to `str`) when required from python code, and convert it back when a QVariant is expected by a Qt function (a `dict` to a QMap). The line you referenced is when a *type* is required, for instance when declaring signals or slots. When an invalid QVariant (a value) is expected, like an unknown value for `data()` in a model, just return `None`. – musicamante Jul 29 '23 at 01:51
  • Some of the code relied on QVariant. I was updating the code to reflect it being depreciated. I had updated it to `None` hence it was working. See my 'answer code' listed above for updated code. – David Frick Jul 29 '23 at 14:19

0 Answers0