Skip to content
  • Categories
  • Recent
  • Tags
  • Popular
  • Users
  • Groups
  • Search
  • Get Qt Extensions
  • Unsolved
Collapse
Brand Logo
  1. Home
  2. Qt Development
  3. General and Desktop
  4. [PyQt] Emitting a layoutChanged signal with a QSortFilterProxyModel and QAbstractTableModel
Forum Updated to NodeBB v4.3 + New Features

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

Scheduled Pinned Locked Moved Solved General and Desktop
9 Posts 3 Posters 5.5k Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • S Offline
    S Offline
    Snoober
    wrote on last edited by
    #1

    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)
    
    1 Reply Last reply
    0
    • Christian EhrlicherC Offline
      Christian EhrlicherC Offline
      Christian Ehrlicher
      Lifetime Qt Champion
      wrote on last edited by
      #2

      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.

      Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
      Visit the Qt Academy at https://academy.qt.io/catalog

      S 1 Reply Last reply
      2
      • Christian EhrlicherC Christian Ehrlicher

        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.

        S Offline
        S Offline
        Snoober
        wrote on last edited by Snoober
        #3

        @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.

        JonBJ 1 Reply Last reply
        0
        • S Snoober

          @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.

          JonBJ Offline
          JonBJ Offline
          JonB
          wrote on last edited by
          #4

          @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.

          1 Reply Last reply
          0
          • Christian EhrlicherC Offline
            Christian EhrlicherC Offline
            Christian Ehrlicher
            Lifetime Qt Champion
            wrote on last edited by
            #5

            @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.

            Qt Online Installer direct download: https://download.qt.io/official_releases/online_installers/
            Visit the Qt Academy at https://academy.qt.io/catalog

            1 Reply Last reply
            2
            • S Offline
              S Offline
              Snoober
              wrote on last edited by Snoober
              #6

              @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.

              JonBJ 1 Reply Last reply
              0
              • S Snoober

                @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.

                JonBJ Offline
                JonBJ Offline
                JonB
                wrote on last edited by JonB
                #7

                @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.

                S 1 Reply Last reply
                0
                • S Offline
                  S Offline
                  Snoober
                  wrote on last edited by
                  #8

                  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().

                  1 Reply Last reply
                  0
                  • JonBJ JonB

                    @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.

                    S Offline
                    S Offline
                    Snoober
                    wrote on last edited by
                    #9

                    @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.

                    1 Reply Last reply
                    0

                    • Login

                    • Login or register to search.
                    • First post
                      Last post
                    0
                    • Categories
                    • Recent
                    • Tags
                    • Popular
                    • Users
                    • Groups
                    • Search
                    • Get Qt Extensions
                    • Unsolved