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

Why is QStandardItemModel forcing me to use pointers to my data? My data is const vector of vectors



  • I have a large collection of objects "A" that was specifically designed to be contiguous in memory for cache efficiency reasons and comes in the form of a const vector of vectors. I want to show this data as a collection of QListViews (UX) but it is also important that these QListViews share the same selection model so I was very inclined to have a single QStandardItemModel for all the groups of items. I saw this could possibly be achieved with the QListView::setModel and QListView::setModelColumn combo.

    So I proceed to write my view model like:

    class MyViewModel final : public QStandardItemModel
    {
    public:
        explicit      MyViewModel(std::vector<std::vector<A>> const& groups, QObject* parent = nullptr);
        QModelIndex   index(int row, int column, const QModelIndex& parent) const override;
        QModelIndex   parent(const QModelIndex& child) const override;
        int           rowCount(const QModelIndex& parent) const override;
        int           columnCount(const QModelIndex& parent) const override;
        QVariant      data(const QModelIndex& index, int role) const override;
    
        std::vector<std::vector<A>> const& m_groups;
    }
    
    QModelIndex MyViewModel::index(int row, int column, const QModelIndex& parent) const
    {
       auto const validColumn = column >= 0 && column < static_cast<int>(m_groups.size());
       if (validColumn)
       {
          auto const validRow = row >= 0 && row < static_cast<int>(m_groups[row].items.size());
          if (validRow)
          {
             return createIndex(row, column, nullptr);
          }
       }
       return m_nullParent;
    }
    
    QModelIndex MyViewModel::parent(const QModelIndex& index) const
    {
       if (!index.isValid() || index.row() == 0)
       {
          return m_nullParent;
       }
       return createIndex(0, index.column(), nullptr);
    }
    
    bool MyViewModel::isValidColumn(const QModelIndex & index) const
    {
       auto const column = index.column();
       return column >= 0 && column < static_cast<int>(m_groups.size());
    }
    
    int MyViewModel::rowCount(const QModelIndex& index) const
    {   
       if (!index.isValid())
       {
          return 1;
       }
       // This section is never reached as all index seem to be invalid
    
       auto const column = index.column();
       if (isValidColumn(index))
       {
          return static_cast<int>(m_groups[column].items.size());
       }   
       return m_groups.size();
    }
    
    int MyViewModel::columnCount(const QModelIndex& index) const
    {
       if (!index.isValid())
       {
          return static_cast<int>(m_groups.size());
       }
       return 1;
    }
    

    The problem is that I can't get MyViewModel::rowCount to work correctly. All I ever get as a QModelIndex parameter is invalid so I'm not able to tell how many items each column has.

    From everything I've seen in other examples, a parent data pointer (void*) is attached to the createIndex command and it's casted back in the QStandardItemModel::rowCount method using the "internalPointer" so this query can be performed. Now you can see that firstly I have no pointer to group (and it would be potentially invalidated on every push), secondly, my data is handed as a const so even if I had the pointer of the "items" vector (which would be dangerous) I would have to remove the const - all of it looks convoluted.

    Now I really don't want to change the original data structure but I can't seem to find any other way to correctly implement MyViewModel::rowCount which seems to be the only problem I have so far.


  • Moderators

    You're inheriting the wrong class for this. QStandardItemModel is a concrete model implementation that operates on QStandardItems for manipulating and storing data. Reimplementing stuff like rowCount, index and parent in it will lead to all sorts of nastiness as you're breaking internal state of the model.

    Since you already have your data structure and don't need any extra storage provided by QStandardItem you should rather inherit QAbstractItemModel which lets you provide your own data storage.



  • @Chris-Kawa said in Why is QStandardItemModel forcing me to use pointers to my data? My data is const vector of vectors:

    Hi Chris, Thanks for your reply.

    I've made the changes but I'm still not able to find a way of implementing rowCount without using the internalData pointer.

    Say my data looks like (vector of vector):
    group1 : item1_1, item1_2, item1_3
    group2 : item2_1, item2_2

    So my first question is: do I need to create a QModelIndex for the groups? if so, how? Because my data is packed in vectors I don't really have the concept of a parent.

    QModelIndex MyViewModel::index(int row, int column, const QModelIndex& parent) const
    {
       auto const validColumn = column >= 0 && column < static_cast<int>(m_groups.size());
       if (validColumn)
       {
          auto const validRow = row >= 0 && row < static_cast<int>(m_groups[column].items.size());
          if (validRow)
          {
             return createIndex(row, column, nullptr);
          }
       }
       return m_nullParent;
    }
    
    QModelIndex MyViewModel::parent(const QModelIndex& index) const
    {
       //I'm unsure how this should work 
       if (!index.isValid() || (index.row() == 0 && index.column() == 0))
       {
          return m_nullParent;
       }
    
       if (index.row() == 0 && index.column() > 0)
       {
          createIndex(0, 0, nullptr);
       }
    
       return createIndex(0, index.column(), nullptr);
    }
    
    int MyViewModel::rowCount(const QModelIndex& parent) const
    {   
       if (!index.isValid())
       {
          return 1;
       }
       // This section is never reached as all index seem to be invalid (-1, -1)
       auto const column = index.column();
       if (isValidColumn(index))
       {
          return static_cast<int>(m_groups[column].items.size());
       }   
       return m_groups.size();
    }
    
    int MyViewModel::columnCount(const QModelIndex& index) const
    {
       if (!index.isValid())
       {
          return static_cast<int>(m_groups.size());
       }
       return 1;
    }
    

  • Moderators

    A Qt models are designed to store N-dimensional data structures. Views show a slice of that N-dimensional data. You have a two dimensional data set (groups of items). So the task is to make a model that would represent your 2 dimensional data in a way the view can look at.

    A list view needs a flat 1 dimensional list. In your case this translates to a group vector.
    A model has rows and columns. A single column from this 2 dimensional grid is used as the flat list for the view. You choose which with the QListView::setModelColumn method you mentioned.

    As for parents in the model. Parents and children are used to model a tree structure. You want a flat grid of depth 1 in which each column represents a group. This imposes a restriction for your data - vectors in each group need to be the same length. To visualize this a list view expects a model like this:

    item1_1  item2_1  item3_1
    item1_2  item2_2  item3_2
    item1_3  item2_3  item3_3
    

    and what you have is

    item1_1  item2_1  item3_1
    item1_2           item3_2
    item1_3  
    

    so this won't work here. You'll need to artificially "pad" them in the model. If you want to avoid that restriction you'll have to use some other way to feed the views e.g a separate model for each group or proxy models.

    So how to implement the model methods?

    • parent - this returns a parent for an item in a tree. Since your tree has only one level you just always return an invalid index (the "invisibile root").

    • rowCount - this returns the number of elements in a group. As mentioned above this is a bit problematic in your case, because you have different number of elements in each vector, but assuming you can "pretend" all are the same size then for an invalid index (the "invisible root") rowCount should return the length of longest of your inner vectors. For any valid index you return 0 because you only have one level in your tree.

    • columnCount - this returns the number of groups, so for an invalid index (the "invisible root") this is simply the size of your outer vector. For any valid index this is again 0, because it's a one level tree.

    • index - this is used to represent a single item in the model. When parent is invalid (which is the "invisible root" of data) column is the group index and row is the element of a vector in that group. Return an index with a pointer to the [column][row] element of your data. If it's one of the "padded" elements you can use a nullptr as the internal pointer. If parent is valid then it means you're being asked for a child of any of your data elements. You have none since this is not a tree so return an invalid index in that case.



  • @Chris-Kawa said in Why is QStandardItemModel forcing me to use pointers to my data? My data is const vector of vectors:

    Thanks for your response! It is definitely shedding some light but now I'm getting into a different issue.

    First of all, I wanted to mention that the reason why I'm trying to have the multiple QListViews, share the same data model is to be able to share the selection model between them too. Initially, I had every QListView have its own QAbstractItemModel instance and I found it very convoluted and quirky to share their selection, especially because multi-selection is needed and shift/control-clicking was tricky to get right.

    So I've gone with the padding solution like this:

    QModelIndex MyViewModel::index(int row, int column, const QModelIndex& parent) const
    {
       if (parent.isValid())
       {
          return m_nullParent;
       }
       auto const validColumn = column >= 0 && column < static_cast<int>(m_groups.size());
       if (validColumn)
       {
          auto const validRow = row >= 0 && row < static_cast<int>(m_groups[column].items.size());
          if (validRow)
          {
             return createIndex(row, column, nullptr);
          }
       }
       return m_nullParent;
    }
    
    QModelIndex MyViewModel::parent(const QModelIndex& parent) const
    {   
       return m_nullParent;
    }
    
    int MyViewModel::rowCount(const QModelIndex& parent) const
    {
       if (parent.isValid())
       {
          return 0;
       }
       size_t maxRowCount = 0;
       for (auto const& group : m_groups)
       {
          maxRowCount = std::max(maxRowCount, group.items.size());
       }   
       return maxRowCount;
    }
    
    int MyViewModel::columnCount(const QModelIndex& parent) const
    {
       if (parent.isValid())
       {
          return 0;
       }
       return static_cast<int>(m_groups.size());
    }
    

    But now the QListView asserts in QIconModeViewBase::addLeaf line 3142.

    if (vi->isValid() && vi->rect().intersects(area) && vi->visited != visited) {
       QModelIndex index  = _this->dd->listViewItemToIndex(*vi);
       Q_ASSERT(index.isValid()); //<-here
    

    Seems like the QListView doesn't like having a row count / created indices mismatch? Then I thought I could fully create these padded indices but then my UI would just like really off as it would think it has all these items when doing the drawing...

    So maybe the only way of doing this would be would the proxy model but then I would be going back to square one as I've read it's also convoluted the share the same selection model between different proxy models



  • @toglia3d said in Why is QStandardItemModel forcing me to use pointers to my data? My data is const vector of vectors:

    I had every QListView have its own QAbstractItemModel instance and I found it very convoluted and quirky to share their selection

    It shouldn't be hard at all. You just connect to view->selectionModel(),&QItemSelectionModel::secelctionChanged and replicate the selection in the other views. Each view has a mono-dimensional model so it should be super easy


  • Moderators

    @toglia3d said:

    Seems like the QListView doesn't like having a row count / created indices mismatch?

    Yes, that's the restriction I mentioned. You really need to have these indices there in this setup, even if they don't represent any actual data. The model needs to function as it were a full rectangular grid.

    So maybe the only way of doing this would be would the proxy model

    I'm not sure what problems you actually had but as @VRonin said, it doesn't seem too hard to have separate selection models syncing.

    Btw. I'm just throwing ideas around but since you want to display all the groups and have their selection synced can't you just use one table view instead of a series of synced list views? Obviously this wouldn't work if they're not next to each other or you want to have independent scrolling but I thought I'd ask.



  • @Chris-Kawa said in Why is QStandardItemModel forcing me to use pointers to my data? My data is const vector of vectors:
    I've gone back to my initial implementation of having multiple QAbstractViewModels. It's not that it is difficult to implement it's just convoluted having these 2 models in sync, especially because sometimes the global (my model) wants to control the selection and some it's the QListView that wants to control the selection. And there are so many cases to take into account - shift/control clicks between listview, box selections etc, then there's the drag and drop between groups etc.

    For this, I've implemented a unidirectional data flow where I capture all the selection events from the QListView update my selection model and send the signals back to the QListViews.


Log in to reply