Search code examples
python-3.xpyside6pyqt6

PySide6 QSortFilterProxyModel filter proxy removes peristent editor


I have a QTableView containing a column with checkboxes. I use a delegate for the checkbox because I want to align it in the center of the column. The one which is generated via the CheckStateRole isn't. The checkbox editor is made permanent for this column.

To sort and filter the table I use a QSortFilterProxyModel. Sorting works and filtering with wildcards also. But if I remove the wildcard filter, the checkboxes of the formerly filtered rows are not drawn. Instead the column shows 0 or 1.

The code is:

import sys
import pandas as pd
from PySide6.QtCore import Qt, Property, Signal, QAbstractTableModel
from PySide6.QtCore import QSortFilterProxyModel, QSignalBlocker
from PySide6.QtWidgets import (QCheckBox, QHBoxLayout, QStyledItemDelegate,
                               QTableView, QWidget, QApplication, QMainWindow,
                               QVBoxLayout, QLineEdit, QFrame)


class CenterCheckBox(QWidget):
    toggled = Signal(bool)

    def __init__(self, parent):
        super().__init__(parent)
        layout = QHBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)
        self.check = QCheckBox()
        layout.addWidget(self.check, alignment=Qt.AlignmentFlag.AlignCenter)
        self.check.setFocusProxy(self)
        self.check.toggled.connect(self.toggled)
        # set a 0 spacing to avoid an empty margin due to the missing text
        self.check.setStyleSheet('color: red; spacing: 0px;')
        self.setAutoFillBackground(True)

    @Property(bool, user=True)   # note the user property parameter
    def checkState(self):
        return self.check.isChecked()

    @checkState.setter
    def checkState(self, state):
        with QSignalBlocker(self.check) as blocker:
            self.check.setChecked(state)


class MyCheckboxDelegate(QStyledItemDelegate):
    def __init__(self, parent):
        super().__init__(parent)

    def createEditor(self, parent, option, index):
        check = CenterCheckBox(parent)
        check.toggled.connect(lambda: self.commitData.emit(check))
        return check

    def setModelData(self, editor, model, index):
        model.setData(index, editor.checkState)


class TableModel(QAbstractTableModel):
    def __init__(self, data):
        super().__init__()
        print(data)
        self._data = data

    def rowCount(self, index=None):
        return self._data.shape[0]

    def columnCount(self, parent=None):
        return self._data.shape[1]

    def data(self, index, role=Qt.ItemDataRole.DisplayRole):
        if index.isValid():
            if role == Qt.ItemDataRole.DisplayRole or role == Qt.ItemDataRole.EditRole:
                if self._data.columns[index.column()]=='Delete':
                    return ''
                value = self._data.iloc[index.row(), index.column()]
                return str(value)


class MyWindow(QMainWindow):
    def __init__(self, *args):
        QMainWindow.__init__(self, *args)
        self.frame = QFrame(self)
        self.verticalLayout = QVBoxLayout(self.frame)
        self.lineEdit = QLineEdit(self.frame)
        self.verticalLayout.addWidget(self.lineEdit)
        self.table_view = QTableView(self.frame)
        self.verticalLayout.addWidget(self.table_view)

        table_model = TableModel(pd.DataFrame([['1', '1'],
                                               ['0', '0'],
                                               ['2', '0']]))
        proxy_model = QSortFilterProxyModel()
        proxy_model.setSourceModel(table_model)
        self.table_view.setModel(proxy_model)
        self.table_view.setSortingEnabled(True)
        self.lineEdit.textChanged.connect(proxy_model.setFilterWildcard)

        self.table_view.setItemDelegateForColumn(1, MyCheckboxDelegate(self))
        for row in range(0, proxy_model.rowCount()):
            self.table_view.openPersistentEditor(proxy_model.index(row, 1))

        self.setCentralWidget(self.frame)


app = QApplication(sys.argv)
window = MyWindow()
window.show()
sys.exit(app.exec())

I already searched in internet for such a behaviour, but nothing found. What I can do to see the checkboxes again after clearing the wildcard filter? May be to call openPersistentEditor again, but how to know when?

BTW: Is there a description of the interaction between delegates, views, models and so on? Until now I only found documentation about the classes and a very basic description of the model view architecture.


Solution

  • First of all, I strongly suggest you to carefully read the whole Model/View Programming Guide.

    When using proxy models, any filter change will result in possibly invalidating current indexes whenever they are removed.

    Your first call to openPersistentEditor() will create editors for existing indexes, but altering the filter of the proxy will also result in destroying those indexes, which will automatically delete their related editors.

    When the filter is changed (possibly "unfiltering" previously hidden indexes), new indexes will be shown. It doesn't matter that they virtually existed before that: to the QTableView's point of view they are new indexes, which means that new persistent editors must be created for them.

    A possible solution, then, is to do what you already do within initialization, which is to call openPersistentEditor() for any new index. If you think about it, it makes sense: you're opening editors that didn't exist before, so you shall do the same whenever a "new" index is being shown (aka, restored by the filter).

    This is easily achieved by connecting to the QAbstractItemModel rowsInserted signal.

    class MyWindow(QMainWindow):
        def __init__(self, *args):
            # you should really use super!!!
            super().__init__(*args)
            ...
            proxy_model.rowsInserted.connect(self.updateEditors)
    
        def updateEditors(self):
            model = self.table_view.model()
            for row in range(model.rowCount()):
                index = model.index(row, 1)
                if not self.table_view.isPersistentEditorOpen(index):
                    self.table_view.openPersistentEditor(index)
    

    Note that a more appropriate and dynamical solution should use a QTableView subclass, which will:

    • set the delegate on its own;
    • implement a similar updateEditors() method;
    • override setModel(), possibly disconnect the rowsInserted signal if a previous model existed, call the default implementation, and eventually connect the signal of the new model (if it exists) to the related method/slot;

    While the above may result more complex, its benefit is that it will always work consistently, no matter if you're using the source mode, a proxy, or even a nested proxy.