Search code examples
qtqmlqabstractitemmodelpyside6qabstractlistmodel

How is the concept of a "parent" meaningful in a QAbstractListModel?


I have a subclass of QAbstractListModel that is the model for a QML ListView (using PySide6). Each row of the list has a checkbox in it, and when the user checks/unchecks a box, it updates a boolean in that row of the listmodel using my override of setData(), which works as expected. I also have Buttons that should select/clear all of the checkboxes in the list.

My model subclass provides the following method to select all, which can be called when the user hits the Button or when other things happen in the application:

def select_all(self):
    for i in range(self.rowCount()):
        row = self._rows[i]
        row['selected'] = True

        # Emitting this for each row works...
        self.dataChanged.emit(self.index(i), self.index(i), [])
        
    # ... whereas emitting just one signal for ALL rows does NOT work
    # self.dataChanged.emit(self.index(0), self.index(self.rowCount()), [])

As you can see from the comments, I need to emit dataChanged for each row in order for the checkboxes in the ListView to be updated. Emitting the signal once and using the topLeft and bottomRight parameters does not update the state of the checkboxes in the ListView (but the model data is correctly updated).

According to the documentation, the dataChanged signal provided by QAbstractItemModel (and inherited by QAbstractListModel) has this caveat:

If the items are of the same parent, the affected ones are those between topLeft and bottomRight inclusive. If the items do not have the same parent, the behavior is undefined.

It seems likely that I'm running into the scenario where the rows in my model do NOT have the same parent, and therefore, "the behavior is undefined." I suppose that makes sense, because I never do anything to establish a parent/child relationship for any of my rows. I also saw this answer which implies that Qt behaves differently when the topLeft and bottomRight indices are the same. So, I would like to understand this better, and I have a few questions:

  1. Am I correct that this parent concept is the reason this does work when emitted for each row and does not work for all rows?
  2. Is the concept of a parent/child relationship only meaningful for tree-like models that would extend QAbstractItemModel instead of QAbstractListModel? Does it make any kind of sense for a list?
  3. If it does make sense for a list, then what would be the "parent" and what would be the "child?" How would I configure a subclass of QAbstractListModel such that dataChanged can be emitted once to update multiple rows?

Thanks!


Solution

  • While the base implementation of Qt item views just updates indiscriminately the view whenever the topLeft and bottomRight indexes doesn't match (so you can just provide two "random", but still sibling indexes), the indexes must not only share a common parent (which for one and two dimensional models is always an invalid index), but also both valid.

    With this line, the second index is not valid:

    self.dataChanged.emit(self.index(0), self.index(self.rowCount()), [])
    

    This is because the indexes are always 0-based, and the index of the last row is actually rowCount - 1. While they theoretically are siblings, since they share the same parent (the parent of a root index is invalid, as it is the parent of an invalid index), they are not both valid.

    So, the correct syntax is:

    self.dataChanged.emit(self.index(0), self.index(self.rowCount() - 1), [])
                                                                   ^^^^
    

    Note that the roles argument is optional and already defaults to an empty list (QVector in C++), so unless you actually want to specify the changed roles, you don't need to provide that.