8

I would like to know if there is any macro or way how to register Qt model as property of QObject.

For example, I have AnimalModel (http://doc.qt.io/qt-5/qtquick-modelviewsdata-cppmodels.html#qabstractitemmodel).

I Know I can pass it to root context of QuickView

QuickView view;
view.rootContext()->setContextProperty("myModel", &model);

In case I have QObject registered via Qml macros, I can pass this object to view too:

view.rootContext()->setContextProperty("obj", pDataObject);

But what If I want to have QObject which holds model of any data?

For example:

class DataObject : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
    Q_PROPERTY(QString color READ color WRITE setColor NOTIFY colorChanged)
    ...

    AnimalModel m_modelAnimals;

    //Is this possible in any way?
    //Q_PROPERTY(AnimalModel modelAnimals READ modelAnimals NOTIFY modelAnimalsChanged)
};

Every example I found until now shows how to pass QAbstractListModel to root context. But none how to use it as QObject property.

(I know there is QQmlListProperty but QQmlListProperty doesn't support partial refresh. It's always necessary to rebuild all Qml objects)

dtech
  • 47,916
  • 17
  • 112
  • 190
Ludek Vodicka
  • 1,610
  • 1
  • 18
  • 33

1 Answers1

20
//Is this possible in any way?
//Q_PROPERTY(AnimalModel modelAnimals READ modelAnimals NOTIFY modelAnimalsChanged)

Yes it is, didn't you try? Of course, it will not be a AnimalModel but a AnimalModel *, but as long as the model inherits QAbstractListModel, that's all you need. You don't even need the NOTIFY part, as changes, internal to the model will be automatically reflected anyway. modelAnimalsChanged only makes sense when you replace the entire model with a different model, and naturally, to shut up QML's warnings about using a property without a notify signal. A cleaner way to do the latter when the model object doesn't change is to just return a AnimalModel * from a slot or a Q_INVOKABLE.

If you want a truly flexible model, you can make one that stores QObject *, then from QML you can create arbitrary objects with arbitrary properties, and add to the model. Then from the model you have a single object role which returns the object, and you can query and use the object to retrieve the properties it holds. Whereas a "classical" list model implementation will define a model with a static, fixed schema, using this approach allows to have "amorphous" objects in the model with different properties.

Naturally, this requires some type safety, for example have a property int type for each object in such a model, and based on it you can determine the available properties for the object. My usual approach is to have a Loader for a delegate, and have it pass the object as a data source to different QML UI implementations visualizing that object type that it instantiates. This way you have both different objects in the model, and different QML items as view delegates.

The last step to making the ultimate "jack of all trades" list/model object is to implement QQmlListProperty and Q_CLASSINFO("DefaultProperty", "container") for it, allowing you to both compose the list/model dynamically, or using QML's declarative syntax. Also note that with this solution, you can add to or remove from such a model, even remove declaratively instantiated objects.

Also, depending on your usage scenario, you may have to either qmlRegisterType() or qmlRegisterUncreatableType() for the model.

OK, on a second glance, it looks like by "model of any data" you didn't mean schema-less models but simply different schema models. In that case, instead of returning an AnimalModel *, you can use a QAbstractListModel * or even a QObject * - it will work in QML anyway, as it employs dynamism through the meta system. But at any rate, schema-less models are that much more powerful and flexible, and they don't need C++ code to be defined, it can all work from QML alone.

class List : public QAbstractListModel {
    Q_OBJECT
    QList<QObject *> _data;

    Q_PROPERTY(int size READ size NOTIFY sizeChanged)
    Q_PROPERTY(QQmlListProperty<QObject> content READ content)
    Q_PROPERTY(QObject * parent READ parent WRITE setParent)
    Q_CLASSINFO("DefaultProperty", "content")
public:
    List(QObject *parent = 0) : QAbstractListModel(parent) { }
    int rowCount(const QModelIndex &p) const { Q_UNUSED(p) return _data.size(); }
    QVariant data(const QModelIndex &index, int role) const {
        Q_UNUSED(role)
        return QVariant::fromValue(_data[index.row()]);
    }
    QHash<int, QByteArray> roleNames() const {
        static QHash<int, QByteArray> roles = { { Qt::UserRole + 1, "object" } };
        return roles;
    }
    int size() const { return _data.size(); }
    QQmlListProperty<QObject> content() { return QQmlListProperty<QObject>(this, _data); }

public slots:
    void add(QObject * o) { insert(o, _data.size()); }

    void insert(QObject * o, int i) {
        if (i < 0 || i > _data.size()) i = _data.size();
        beginInsertRows(QModelIndex(), i, i);
        _data.insert(i, o);
        o->setParent(this);
        sizeChanged();
        endInsertRows();
    }

    QObject * take(int i) {
        if ((i > -1) && (i < _data.size())) {
            beginRemoveRows(QModelIndex(), i, i);
            QObject * o = _data.takeAt(i);
            o->setParent(0);
            sizeChanged();
            endRemoveRows();
            return o;
        } else qDebug() << "ERROR: take() failed - object out of bounds!";
        return 0;
    }

    QObject * get(int i) {
        if ((i > -1) && (i < _data.size())) return _data[i];
        else  qDebug() << "ERROR: get() failed - object out of bounds!";
        return 0;
    }

    void internalChange(QObject * o) { // added to force sort/filter reevaluation
      int i = _data.indexOf(o);
      if (i == -1) {
        qDebug() << "internal change failed, obj not found";
        return;
      } else {
        dataChanged(index(i), index(i));
      }
    }

signals:
    void sizeChanged();
};

Then, after you qmlRegisterType<List>("Core", 1, 0, "List"); you can use it pretty much any way you want to - it will hold any QObject or derived, naturally including QMLs QtObject It can directly be used as a model to drive a ListView. You can populate it dynamically using the slots or declarative, like this:

List {
    QtObject { ... }
    QtObject { ... }
    List {
        QtObject { ... }
        QtObject { ... }
    }
}

It will also handle object ownership, and you can easily nest it, producing in essence a compartmentalized tree model - note that you can't declaratively do that with QML's ListModel. You may want to add a parentChanged signal and implement a setter that emits it if you want to bind against a changing parent, it was not necessary in my case.

As of how to use it with a view, you can either use the objectName property or an int type property or basically any means to discern between different object types, and use a Loader for the delegate:

Loader {
    // using component in order to capture context props and present to the variable delegate
    sourceComponent: Qt.createComponent(obj.objectName + ".qml")
    // if that is not needed simply use
    // source: obj.objectName + ".qml"
    // or setSource to pass specific properties to delegate properties
    // Component.onCompleted: setSource(obj.objectName + ".qml", {/*prop list*/})
}

Update:

Here is also the gist of the implementation for a simple and just as dynamic and generic sorting and filtering proxy to go with this model for enhanced usability.

dtech
  • 47,916
  • 17
  • 112
  • 190
  • Thank you for very complex answer with a lot of interesting ideas. You were right that the root problem was in invalid `Q_PROPERTY` type. With `AnimalModel*` it works great. Regarding model of `QObject*`, this is something I already have (stackoverflow.com/questions/35065627/…). But can you please post example or be more specific about `Q_CLASSINFO` and `qmlRegisterType` use case? It seems interesting but I'm not sure how exactly to use it in this case. – Ludek Vodicka Feb 03 '16 at 07:07
  • @LudekVodicka - I added some code, also took the liberty to re-title your question so that the code is more accessible to people who look for a similar solution. – dtech Feb 03 '16 at 10:53
  • This is absolutely masterpiece. I'm learning Qml last month and half and did not have idea it can be combined in such great way. Thank you for explanation and examples. The only question which comes to my mind. Why QHashMap as static ptr instead of static only? It's possible to define it as `static QHash roles = { { ObjectRole, "object" } };` but it needs c++11. Was this the reason? – Ludek Vodicka Feb 03 '16 at 11:45
  • Yes, it is for backward compatibility. And yes, this approach is very flexible and powerful, you can create very complex projects using only this as your C++ code, and do everything else in QML - it is dynamic, can use code generated on the fly and is a lot faster to develop in. I really wonder why something so simple and yet so powerful was not incorporated in QML to begin with. – dtech Feb 03 '16 at 11:56
  • Perfect. This is exactly the reason why I'm trying to develop by my own. It's something I expected that Qml will have from the start. It seems that current Qt/Qml state is prepared only for simple UI development but nothing more complicated. Thank you for all answers. – Ludek Vodicka Feb 03 '16 at 16:33
  • Do you have any idea why such a model is not included in Qt directly? It seems so useful. Is there a problem with it e.g. regarding memory management? http://stackoverflow.com/q/43809751 – Georg Schölly May 05 '17 at 16:29
  • Yes, it is indeed extremely useful, because of the possibility to define entire tree models in declarative, and especially because you can create the objects in QML without recompilation. Great for prototyping, I do the objects in QML first, when finished and tested, I implement in C++, in order to avoid the huge memory overhead that comes with the instantiation QML objects. Still, nothing too severe unless you deal with tens or hundreds of thousands of objects. I haven't encountered any problem with memory management whatsoever, it works fine despite my initial doubts. – dtech May 05 '17 at 17:22
  • As of why it is not available by default in Qt - who knows, my guess is for the same reason `QtObject` can't have children, even though it is just a `QObject` that can. Qt suffers from a case of bad internal design, which is not that much apparent if you do "by the book Qt", but becomes a big annoyance and hindrance if you try to do something its makers did not envision. When you do that, all hell breaks loose, bugs, hardcoded limitations, inflexibility in the behavior. – dtech May 05 '17 at 17:25
  • For the last part of the post, QML has now `DelegateChooser` component which allow you to choose the correct model based on model property too. – Moia Jun 19 '20 at 08:20
  • @Moia - sure, qml has introduced a lot of auxiliary components since then, but as a rule of thumb, I'd rather not use some big and complex class for something that can be written in a couple of lines, I prefer simplicity and full control over the behavior. This is especially true for qml, where there is a tendency for stock components to be needlessly bloated and over-complicated, while at the same time inflexible and rather limited in what they can do and how. – dtech Jun 29 '20 at 04:10
  • That's an interesting approach. It could help me a lot...but how about to use it with a QSortFilterProxyModel with filter based on role ? Should we subclass QSortFilterProxyModel and have a filtering based on QObject properties instead of role ? – Zyend Apr 13 '21 at 13:59
  • @Zyend it doesn't make sense to use a role, because in this context, you have only one single role. The answer, linked at the bottom, has the gist of an implementation that accepts a JS function, and the model items get passed to those, so you have the object properties, methods as well as whatever happens to be in scope from qml. This is a lot more powerful and flexible than role based sorting and filtering. – dtech Apr 13 '21 at 15:07