python-3.xpyqt5qtableviewqsortfilterproxymodelqsqltablemodel

How to use custome FilterProxyModel and QtCore.Qt.EditRole at a time?


Below is the example code:

class FilterProxyModel(QtCore.QSortFilterProxyModel):
    def __init__(self, parent=None):
        super().__init__(parent)
        self._filter_value = None

    @property
    def filter_value(self):
        return self._filter_value

    @filter_value.setter
    def filter_value(self, value):
        self._filter_value = value
        self.invalidateFilter()

    def filterAcceptsRow(self, sourceRow, sourceParent):
        if self.filter_value is None:
            return super().filterAcceptsRow(sourceRow, sourceParent)
        if self.filterKeyColumn() >= 0:
            value = (
                self.sourceModel()
                .index(sourceRow, self.filterKeyColumn(), sourceParent)
                .data(self.filterRole())
            )
            return value == self.filter_value

        for column in range(self.columnCount()):
            value = (
                self.sourceModel()
                .index(sourceRow, column, sourceParent)
                .data(self.filterRole())
            )
            if value == self.filter_value:
                return True
        return False
    
    def setFilterRegExp(self, filter):
        self.filter_value = None
        super().setFilterRegExp(filter)



class UI(QMainWindow):
    def __init__(self):
        super(UI, self).__init__()
        uic.loadUi("tableview.ui", self)
        self.show()

        db = QSqlDatabase.addDatabase('QSQLITE')
        db.setDatabaseName('book.db')
        db.open()

        self.model = QtSql.QSqlTableModel(self)
        self.model.setTable("card")
        self.model.select()
        self.tableView.setModel(self.model)
        
        self.proxy = FilterProxyModel(self)
        self.proxy.setSourceModel(self.model)
        
        self.tableView.setModel(self.proxy)
        self.model.select()

        self.edit.clicked.connect(self.edit_items)
        self.refresh.clicked.connect(self.refresh_table)

        column_names = ["Name","Age","Gender"]

        self.comboBox.addItems([x for x in column_names])

        self.horizontalHeader = self.tableView.horizontalHeader()
        self.horizontalHeader.sectionClicked.connect(
            self.tableView_horizontalHeader_sectionClicked
            )
        self.lineEdit.textChanged.connect(self.lineEdit_textChanged)

    def tableView_horizontalHeader_sectionClicked(self, logicalIndex):
        menu = QtWidgets.QMenu(self)

        values = []

        for row in range(self.model.rowCount()):
            value = self.model.index(row, logicalIndex).data(self.proxy.filterRole())
            values.append(value)

        action_all = QtWidgets.QAction("All", self)
        action_all.setData(None) 
        menu.addAction(action_all)
        menu.addSeparator()

        for value in sorted(list(set(values))):
            action = QtWidgets.QAction(str(value), self)
            action.setData(value)
            menu.addAction(action)

        headerPos = self.tableView.mapToGlobal(self.horizontalHeader.pos())
        posY = headerPos.y() + self.horizontalHeader.height()
        posX = headerPos.x() + self.horizontalHeader.sectionPosition(logicalIndex)

        action = menu.exec_(QtCore.QPoint(posX, posY))
        
        if action is not None:
            self.proxy.setFilterKeyColumn(logicalIndex)
            self.proxy.filter_value = action.data()

    def lineEdit_textChanged(self):
        search = QtCore.QRegExp(    self.lineEdit.text(),
                                    QtCore.Qt.CaseInsensitive,
                                    QtCore.QRegExp.RegExp
                                    )
        self.proxy.setFilterKeyColumn(self.comboBox.currentIndex() - 1)
        self.proxy.setFilterRegExp(search)        
            
    def edit_items(self):

        if not self.model.rowCount():
            return
        index = self.tableView.currentIndex()
        if index.isValid():
            row = index.row()
        else:
            row = 0

        dialog = QtWidgets.QDialog()
        dialog.setWindowTitle("Edit Window")
        layout = QtWidgets.QVBoxLayout(dialog)

        formLayout = QtWidgets.QFormLayout()
        layout.addLayout(formLayout)

        name_line = QtWidgets.QLineEdit(self.model.index(row, 0).data())
        formLayout.addRow('Name', name_line)
        name_line.setReadOnly(True)

        age_edit = QtWidgets.QSpinBox()
        formLayout.addRow('Age', age_edit)
        age_edit.setFocus()
        age_edit.setValue(self.model.index(row, 1).data())

        genders = 'M', 'F'
        gender_combo = QtWidgets.QComboBox()
        formLayout.addRow('Gender', gender_combo)
        gender_combo.addItems(genders)
        gender = self.model.index(row, 2).data()
        if gender and gender.upper() in genders:
            gender_combo.setCurrentIndex(genders.index(gender.upper()))
        else:
            gender_combo.setCurrentIndex(-1)

        updateButton = QtWidgets.QPushButton('Update')
        layout.addWidget(updateButton)
        updateButton.clicked.connect(dialog.accept)

        if not dialog.exec_():
            return

        self.model.setData(self.model.index(row, 1), age_edit.value(), 
            QtCore.Qt.EditRole)
        if gender_combo.currentIndex() >= 0:
            self.model.setData(self.model.index(row, 2), 
                gender_combo.currentText(), QtCore.Qt.EditRole)
        # submit all changes to the database
        self.model.submitAll()

    def refresh_table(self):
        print("rsfersh")

This is working good Before filtering the columns with FilterProxyModel. The problem is: Not getting exact row into "Edit Window", after filter the columns. For example its getting the row of db index row not from tableview.

Below is the reference Image.

enter image description here

As above image after filter, I want to edit 4th column("name4") but it is taking 1st column("name1"). How to edit required column after filtered.


Solution

  • The currentIndex method of QTableView returns a QModelIndex associated with the model established in the model, in this it is "self.proxy" and not with respect to "self.model". On the other hand, in these cases the task of mapping the columns is simplified using QDataWidgetMapper:

    import sys
    from PyQt5 import QtCore, QtWidgets, QtSql, uic
    
    
    class FilterProxyModel(QtCore.QSortFilterProxyModel):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._filter_value = None
    
        @property
        def filter_value(self):
            return self._filter_value
    
        @filter_value.setter
        def filter_value(self, value):
            self._filter_value = value
            self.invalidateFilter()
    
        def filterAcceptsRow(self, sourceRow, sourceParent):
            if self.filter_value is None:
                return super().filterAcceptsRow(sourceRow, sourceParent)
            if self.filterKeyColumn() >= 0:
                value = (
                    self.sourceModel()
                    .index(sourceRow, self.filterKeyColumn(), sourceParent)
                    .data(self.filterRole())
                )
                return value == self.filter_value
    
            for column in range(self.columnCount()):
                value = (
                    self.sourceModel()
                    .index(sourceRow, column, sourceParent)
                    .data(self.filterRole())
                )
                if value == self.filter_value:
                    return True
            return False
    
        def setFilterRegExp(self, filter):
            self.filter_value = None
            super().setFilterRegExp(filter)
    
    
    class UI(QtWidgets.QMainWindow):
        def __init__(self):
            super(UI, self).__init__()
            uic.loadUi("tableview.ui", self)
            self.tableView.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
    
            db = QtSql.QSqlDatabase.addDatabase("QSQLITE")
            db.setDatabaseName("book.db")
            db.open()
    
            self.model = QtSql.QSqlTableModel(self)
            self.model.setTable("card")
            self.model.select()
    
            self.proxy = FilterProxyModel(self)
            self.proxy.setSourceModel(self.model)
    
            self.tableView.setModel(self.proxy)
            self.model.select()
    
            self.edit.clicked.connect(self.edit_items)
            self.refresh.clicked.connect(self.refresh_table)
    
            r = self.model.record()
            column_names = [r.field(i).name().title() for i in range(r.count())]
    
            self.comboBox.addItems([x for x in column_names])
    
            self.horizontalHeader = self.tableView.horizontalHeader()
            self.horizontalHeader.sectionClicked.connect(
                self.tableView_horizontalHeader_sectionClicked
            )
            self.lineEdit.textChanged.connect(self.lineEdit_textChanged)
    
        def tableView_horizontalHeader_sectionClicked(self, logicalIndex):
            menu = QtWidgets.QMenu(self)
    
            values = []
    
            for row in range(self.model.rowCount()):
                value = self.model.index(row, logicalIndex).data(self.proxy.filterRole())
                values.append(value)
    
            action_all = QtWidgets.QAction("All", self)
            action_all.setData(None)
            menu.addAction(action_all)
            menu.addSeparator()
    
            for value in sorted(list(set(values))):
                action = QtWidgets.QAction(str(value), self)
                action.setData(value)
                menu.addAction(action)
    
            headerPos = self.tableView.mapToGlobal(self.horizontalHeader.pos())
            posY = headerPos.y() + self.horizontalHeader.height()
            posX = headerPos.x() + self.horizontalHeader.sectionPosition(logicalIndex)
    
            action = menu.exec_(QtCore.QPoint(posX, posY))
    
            if action is not None:
                self.proxy.setFilterKeyColumn(logicalIndex)
                self.proxy.filter_value = action.data()
    
        def lineEdit_textChanged(self):
            search = QtCore.QRegExp(
                self.lineEdit.text(), QtCore.Qt.CaseInsensitive, QtCore.QRegExp.RegExp
            )
            self.proxy.setFilterKeyColumn(self.comboBox.currentIndex())
            self.proxy.setFilterRegExp(search)
    
        def edit_items(self):
    
            if not self.model.rowCount():
                return
            index = self.tableView.currentIndex()
            if index.isValid():
                row = index.row()
            else:
                row = 0
    
            name_line = QtWidgets.QLineEdit(readOnly=True)
            age_edit = QtWidgets.QSpinBox()
            gender_combo = QtWidgets.QComboBox()
            genders = "M", "F"
            gender_combo.addItems(genders)
            updateButton = QtWidgets.QPushButton("Update")
    
            mapper = QtWidgets.QDataWidgetMapper()
            mapper.setSubmitPolicy(QtWidgets.QDataWidgetMapper.ManualSubmit)
            mapper.setModel(self.tableView.model())
            mapper.addMapping(name_line, 0)
            mapper.addMapping(age_edit, 1)
            mapper.addMapping(gender_combo, 2)
            mapper.setCurrentIndex(row)
    
            dialog = QtWidgets.QDialog()
            dialog.setWindowTitle("Edit Window")
    
            layout = QtWidgets.QVBoxLayout(dialog)
    
            formLayout = QtWidgets.QFormLayout()
            layout.addLayout(formLayout)
            formLayout.addRow("Name", name_line)
            formLayout.addRow("Age", age_edit)
            formLayout.addRow("Gender", gender_combo)
            layout.addWidget(updateButton)
            updateButton.clicked.connect(dialog.accept)
    
            if dialog.exec_():
                mapper.submit()
    
        def refresh_table(self):
            print("refresh")
    
    
    def main():
        app = QtWidgets.QApplication(sys.argv)
        w = UI()
        w.show()
        sys.exit(app.exec_())
    
    
    if __name__ == "__main__":
        main()