Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

[PyQt] Emitting a layoutChanged signal with a QSortFilterProxyModel and QAbstractTableModel



  • I am unable to have rows added to my table view after adding my proxy model (I will need to filter and sort). I am using a QAbstractTableModel and had overridden data(), rowCount(), and columnCount(). These were all based on a list structure which contained my data. I would emit a layoutChanged signal and rows would be added or removed as the length of my list changed.

    After adding a proxy model, any existing data is updated, but rows are not added as the length of my data list is increased. I have written a simple program with the same structure which shows the problem that I am having:

    import sys
    import typing
    
    from PyQt5.QtWidgets import QApplication, QWidget, QTableView, \
            QLineEdit, QPushButton, QHBoxLayout, QVBoxLayout
    from PyQt5.QtCore import QAbstractTableModel, \
            QSortFilterProxyModel, QModelIndex, Qt
    
    
    class MainWindow(QWidget):
        def __init__(self):
            super().__init__()
            self.data = ["some", "data"]
            self.entry = QLineEdit()
            self.table = TableView(self.data)
            self.init_ui()
    
        def init_ui(self):
            top_layout = QVBoxLayout()
    
            entry_layout = QHBoxLayout()
            add_btn = QPushButton("Add to data")
            add_btn.clicked.connect(self.on_click_add_btn)
            entry_layout.addWidget(self.entry)
            entry_layout.addWidget(add_btn)
    
            top_layout.addLayout(entry_layout)
            top_layout.addWidget(self.table)
    
            self.setLayout(top_layout)
    
        def on_click_add_btn(self):
            self.table.model().layoutAboutToBeChanged.emit()
            self.data.insert(0, self.entry.text())
            self.table.model().layoutChanged.emit()
            self.entry.clear()
    
    
    class TableView(QTableView):
        def __init__(self, data_list):
            super().__init__()
            self.data_list = data_list
            self.setSortingEnabled(True)
            self.proxy_model = ProxyModel(self)
            self.table_model = TableModel(self, self.data_list)
            self.proxy_model.setSourceModel(self.table_model)
            self.setModel(self.proxy_model)
    
    
    class TableModel(QAbstractTableModel):
        def __init__(self, parent, data_list):
            super().__init__(parent)
            self.data_list = data_list
    
        def data(self, index: QModelIndex, role: int = ...) -> typing.Any:
            if role == Qt.DisplayRole:
                return self.data_list[index.row()]
    
        def rowCount(self, parent=None, *args, **kwargs):
            return len(self.data_list)
    
        def columnCount(self, parent=None, *args, **kwargs):
            return 1
    
    
    class ProxyModel(QSortFilterProxyModel):
        def __init__(self, parent=None):
            super().__init__(parent)
    
        def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
            left_data = self.sourceModel().data(left, Qt.DisplayRole)
            right_data = self.sourceModel().data(right, Qt.DisplayRole)
            return left_data.upper() < right_data.upper()
    
    
    if __name__ == "__main__":
        app = QApplication(sys.argv)
        main_window = MainWindow()
        main_window.show()
        exit_code = app.exec()
        sys.exit(exit_code)
    

  • Lifetime Qt Champion

    You have to emit the signal on the model where you change the data, not on the one in the view. And layoutAboutToBeChanged is a little bit too much - begin/endInsterRows() is suffice.



  • @Christian-Ehrlicher said in [PyQt] Emitting a layoutChanged signal with a QSortFilterProxyModel and QAbstractTableModel:

    You have to emit the signal on the model where you change the data, not on the one in the view. And layoutAboutToBeChanged is a little bit too much - begin/endInsterRows() is suffice.

    I tried this before but I would end up with a segmentation fault. The segmentation fault happens after I click the table widget, then add data, and click the widget again.

    I also get "QSortFilterProxyModel: index from wrong model passed to mapFromSource" while I have items clicked in the table.

    EDIT: From another post I tried calling invalidateFilter() on the proxy model (via QTableView.model()) instead of layoutChanged signal and it seems to work.



  • @Snoober
    As per another thread we had recently in this forum with similar-ish issue, not sure whether it applies to yours.

    Emitting a "layout changed" causes the whole table view to be redrawn. This can hide a logic error in inserting rows etc. I don't know how this might relate to your invalidateFilter() instead. It's up to you whether you wish to investigate further.


  • Lifetime Qt Champion

    @Snoober said in [PyQt] Emitting a layoutChanged signal with a QSortFilterProxyModel and QAbstractTableModel:

    I also get "QSortFilterProxyModel: index from wrong model passed to mapFromSource" while I have items clicked in the table.

    Then you should fix this and pass the index for the correct model.



  • @Christian-Ehrlicher @JonB

    I don't understand because these issues are present in the simplified example above, and I do not believe there are any logic errors or incorrectly passed indexes. Please correct me if I am wrong.



  • @Snoober
    @Christian-Ehrlicher will know much quicker than me, but I fancy having a go!

    I also get QSortFilterProxyModel: index from wrong model passed to mapFromSource while I have items clicked in the table.

    Your code:

    class ProxyModel(QSortFilterProxyModel):
        def lessThan(self, left: QModelIndex, right: QModelIndex) -> bool:
            left_data = self.sourceModel().data(left, Qt.DisplayRole)
            right_data = self.sourceModel().data(right, Qt.DisplayRole)
    

    Indexes have to be mapped between the proxy model & the source model --- mapToSource() et al. I'm thinking which model do the left & right arguments QModelIndex belong to? Don't you need to https://doc.qt.io/qt-5/qsortfilterproxymodel.html#mapToSource from here?

    Hmm, I'm not at all sure I'm right here :( Perhaps we need @Christian-Ehrlicher after all :) Somewhere I guess a wrong-model index is being passed to mapFromSource() internally as a result of what's happening here, and I'd want to understand why.



  • For anyone else that finds this post:

    I am not sure if this is the "right" way to do things but it is working for me. It is assumed you have a QSortFilterProxyModel set as your view model, and the QSortFilterProxyModel source model is set to your table model.

    When adding data where you know the start and end indexes, use QAbstractTableModel.beginInsertRows() and endInsertRows(). For removing rows use beginRemoveRows() and endRemoveRows().

    I have some cases where the rows being removed have multiple indexes that are not in-sequence (for instance, the user selects multiple rows not in-sequence and chooses to delete them). It might be best to "group" the indexes that are in-sequence and remove them with beginRemoveRows()/endRemoveRows(), but in my case I am just resetting the model with beginResetModel() and endResetModel().

    When data is changed and the model might need to re-evaluate sorting and filtering use QSortFilterProxyModel.invalidate().

    When data is changed and the model might need to re-evaluate filtering use QSortFilterProxyModel.invalidateFilter().



  • @JonB I'm 99% sure that you do not need to map indexes in this example as the lessThan() function needs to figure out sorting before proxy indexes are determined (since the proxy indexes are dependent on sorting).

    I wrote my solution in the other reply just posted. I don't completely understand the nuances between emitting layoutChanged signal and the various other ways I have found to update the model*. But I believe the other methods I have found are more appropriate (i.e. beginInsertRows(), or beginResetModel()). In any case they work :)

    *I am pretty sure the other methods (begin/endInsertRows() and begin/endResetModel() emit signals themselves "properly" so it's probably the way to go.


Log in to reply