3
votes

I'm a looking for a way how to pass larger amount of QObjects to QML as Model and than use Repeater {} to draw them. This solution have to meet following criteria:

  1. when new object is added to list, only this item will be refreshed
  2. when any of passed object is changed, Qml content have to be automatically refreshed
  3. list have to be universal, no hard-coded property names

Solution 1.

I know I can use QQmlListProperty<QObject>. Unfortunately in case that object is added/removed all other objects are refreshed in Qml. In case of more complex/larger amount of objects this is very unwieldy.

This solution meets 2) and 3). When object is updated via setter and called notifyChange(), qml automatically update the content and it's possible to use it on any QObject

Solution 2.

QAbstractList with implemented roles as described in documentation (http://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel).

This solution meets 1) but not 2) and 3). When new object is added and beginInsertRows/endInsertRows is called, only one item is correctly refreshed. But it's necessary to prepare Model object for each QObject and Model have to be manually updated when QObject changed

Solution 3.

I tried to implement QAbstractListModel which internally holds list of QObjects pointers: QList<boost::shared_ptr<AnimalObject>> m_animals;.

When roleNames() method isn't implemented Qml doesn't query for data via data() method at all. So it seems that it's not possible to use default Qt::DisplayRole role for returning QObject from QAbstractList

When roleNames() method is implemented with single role, for example "object" and data() returns inner QObject as QVariant, it's possible to access it from QML:

QVariant AnimalModel2::data(const QModelIndex & index, int role) const {
    if ( index.row() < 0 || index.row() >= m_animals.count() )
        return QVariant();

    auto ptrAnimal = m_animals[index.row()];
    if ( role == ObjectRole )
    return qVariantFromValue(ptrAnimal.get());
}

QHash<int, QByteArray> AnimalModel2::roleNames() const {
    QHash<int, QByteArray> roles;
    roles[ObjectRole] = "object";
    return roles;
}

and this is how to access QObject from QML

Repeater {
  model: myModel
  Text {
    text: "[" + model.object.name + "]";
  }
}

This solution meets all requirements. Unfortunately it's necessary to access this QObject with model.object.property instead of more straightforward model.property. Although it's not a big deal, would be great to have direct object access.

Question:

My Question is. Are these three methods the only possible solutions for this problem, or are there any other method I completely missed?

Is there any clean way how to create list of QObjects, pass it to QML with full support of adding/removing/updating objects from C++ and directly updating them in QML?

PS: I described all these methods here because I believe this can be useful for many others.It took me a few hours to figure out how to do all of this.

Edit

Suggested solution 1.

As suggested @Velkan, it's possible to use QMetaObject::property and QMetaObject::propertyCount to dynamically extend QAbstractListModel for object's properties. Unfortunately this means to implement also individual refresh for each object/property via QDynamicPropertyChangeEvent signals.

Unfortunately for large number of objects and properties this solution probably would be very inefficient. For anyone interested in this solution, here is a snippet with QMetaObject::property test:

QVariant AnimalModel4::data(const QModelIndex & index, int role) const {
    if ( index.row() < 0 || index.row() >= m_animals.count() )
        return QVariant();

    auto ptrAnimal = m_animals[index.row()];

    const QMetaObject * pMeta = ptrAnimal->metaObject();
    int propertyNo= role - (Qt::UserRole + 1);

    QMetaProperty propMeta = pMeta->property(propertyNo);
    QVariant value = propMeta.read(ptrAnimal.get());
    return value;
}

QHash<int, QByteArray> AnimalModel4::roleNames() const {

    QHash<int, QByteArray> roles;
    if ( m_animals.size() == 0 )
        return roles;

    int role= Qt::UserRole + 1;
    const QMetaObject * pMeta = m_animals.front()->metaObject();
    for ( int propertyNo= 0; propertyNo< pMeta->propertyCount(); propertyNo++ )
    {
        QMetaProperty propMeta = pMeta->property(propertyNo);
        roles[role++] = propMeta.name();
    }
    return roles;
}
1
"When roleNames() method isn't implemented Qml doesn't query for data via data() method at all." That's not true. The default role names are here: doc.qt.io/qt-5/qabstractitemmodel.html#roleNamesMitch
Also, I'm not sure what this means: "When new object is added and beginInsertRows/endInsertRows is called, only one item is correctly refreshed. But it's necessary to prepare Model object for each QObject and Model have to be manually updated when QObject changed" An actual, physical example would help out a lot here, as always.Mitch
@Mitch: default role names aren't used from QML Model. To be able to access model.propertyName, it's necessary to implement "propertyName" role. Default roles are useless here. Without overriding and implementing roleNames() method in data() isn't executed at allLudek Vodicka
@Mitch: to your second post. Example is in attached Qt doc. What do you need to see more? When objects are modified after are already added to AnimalModel, there is no automatic refresh of AnimalModel. You can try it by copy&pasting mentioned exampleLudek Vodicka
Probably you can implement roleNames() by getting property names with QMetaObject::property and QMetaObject::propertyCount: doc.qt.io/qt-5/qmetaobject.html#propertyCount . Without roleNames() only the standard roles (like 'display' or 'edit') work. Automatic individual refresh you can do on dynamic properties, because they generate a QDynamicPropertyChangeEvent event when changed. For static properties, there is QMetaProperty::notifySignal that can be connected to something, But it's a lot of connections: each property of each object.Velkan

1 Answers

0
votes

This solution meets all requirements. Unfortunately it's necessary to access this QObject with model.object.property instead of more straightforward model.property. Although it's not a big deal, would be great to have direct object access.

If your problem is with model.object.property and your solution is a shorter model.property, then an equally good solution would be object.property which is entirely functional. You can directly use roleName from within the delegate object.

As for the generic list/model class, I already addressed that in that other question.