How to create a generic object model for use in QML? - c++

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)

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

Related

How to create custom Quick QML Item with predefided states

I am quite new to Qt, so I am probably asking a pretty obvious question.
I would like to create a super type for all of my custom QML GUI elements that I want to create in C++.
This super type is supposed to add predefined states to a QML Item. Something alike to this:
import StatedGuiElement 1.0
import QtQuick 2.0
Item {
width: 300; height: 200
StatedGuiElement {
id: aStatedGuiElement
anchors.centerIn: parent
width: 100; height: 100
//some visible Custom Gui Elements
states:[
State {
name: "A_STATE"
},
State {
name: "ANOTHER_STATE"
}]
}
I know how to create a simple custom Item from this tutorial (http://doc.qt.io/qt-5/qtqml-tutorials-extending-qml-example.html). I guess the states could be defined by using an enum in the C++ class which inherits from QQuickItem. However, this tutorial does not show how to create more complex Qt Quick elements like the state list.
class StatedGuiElement : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QString name READ name WRITE setName)
//pass States via Q_PROPERTY?
public:
//define Enum for fixed States here?
//ENUM STATES {A_STATE, ANOTHER_STATE}
StatedGuiElement( QQuickItem *parent = 0);
QString name() const;
void setName(const QString &name);
private:
QString m_name;
//Some List of States?
signals:
public slots:
};
So the questions I am wondering about are as follows:
Is it even possible to predefine QML State types and use them in multiple elements?
How do I add complex QML types like State Lists in a C++ class such as StatedGuiElement?
First you create your StatedGuiElement as a QQuickItem subclass.
Then you crate a StatedGuiElement.qml, import the package that contains the C++ element, make a StatedGuiElement {} inside, add your states in QML to it, then you can use StatedGuiElement in your project. It will be the one with the QML extra stuff predefined in it.
This assumes the element actually has things you need to implement in C++. If not then it would not make sense to have a C++ element at all. I am not sure whether old C++ state classes will work with QML, probably not, and using the QML states from C++ will be anything but convenient, so you really should do the states in QML, on top of whatever C++ stuff you may have.
It is possible to define your properties once and use them im multiple elements, if you nest your QML elements inside a super-type QML element where you have all your states defined. Child elements can access the parent parameters.
Alternatively, you can also just simply set the context property for each QML which should use the data like this in C++:
QQuickView view;
QStringList data;
// fill the list with data via append()
view.rootContext()->setContextProperty("dataList", QVariant::fromValue(data));
// now the QML can freely use and access the list with the variable name "dataList"
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
In the QML-side, you can also declare a custom property which holds the names of the states.
Item {
property variant state_list: ["element1", "element2", "element3"]
// or if you defined a list in the C++ part as a context property
// you can use this instead:
// property variant state_list: dataList
states: [
State {
name: state_list[0]
},
State {
name: state_list[1]
},
// and so on
]
}
If you need a property that is a list of elements, i..e. states being a list of State objects, then you can do this in C++ using the QQmlListProperty type.
You need a QObject derived type for the list element type.
Example
class Entry : public QObject
{
// the list entry element's API
};
class MyItem : public QQuickItem
{
Q_OBJECT
Q_PROPERTY(QQmlListProperty<Entry> entries READ entries)
public:
QQmlListProperty<Entry> entries() const {
return QQmlListProperty<Entry>(this, m_entries);
}
private:
QList<Entry*> m_entries;
};
Register both with qmlRegisterType()
In QML
MyItem {
entries: [
Entry {
},
Entry {
}
]
}

Send object from c++ to qml. What about free memory?

There is a model inherited from QAbstractListModel, I use it in qml. One of the properties of this model are parameters, they are specific to the element type of this model. That is one element the parameters of this class TemperatureParam, DifrentParamType another, a third still something... How can I pass an object to qml and to be sure that the memory is freed after use? The code below now works as I need to, but it seems to me that it's not safe.
Param class is so trivial:
class QuickTemperatureParam : public QObject
{
Q_OBJECT
Q_PROPERTY(float param1 READ param1 WRITE setParam1)
//...
};
Model class (Here's what I'm asking):
class SectionsModel : public QAbstractListModel
{
//...
QVariant data(const QModelIndex &idx, int role = Qt::DisplayRole) const override
{
//...
int type = getType( idx );
if (type == 1)
{
auto p = new QuickTemperatureParam( idx );
p->deleteLater(); // This is all right or no?
return qVariantFromValue(p);
}
else if (type == 2)
//...
}
};
QML something like this:
ListView {
model: sectionsModel
delegate: Rectangle {
color: model.statusColor
ItemDelegate {
text: model.title
highlighted: ListView.isCurrentItem
onPressed:
switch ( model.type )
{
case SectionType.Temperature:
temperatureParam.openItem(model)
break;
case SectionType.Lighting:
lightingParam.open(model)
break;
}
}
}
}
Popup {
id: temperatureParam
function openItem(model)
{
var p = model.param
params.itemAt(0).range.from = params.itemAt(1).range.from = p.min
params.itemAt(0).range.to = params.itemAt(1).range.to = p.max
params.itemAt(0).range.setValues( p.dayMin, p.dayMax )
params.itemAt(1).range.setValues( p.nightMin, p.nightMax )
open()
}
}
According to the documentation:
When data is transferred from C++ to QML, the ownership of the data
always remains with C++. The exception to this rule is when a QObject
is returned from an explicit C++ method call: in this case, the QML
engine assumes ownership of the object, unless the ownership of the
object has explicitly been set to remain with C++ by invoking
QQmlEngine::setObjectOwnership() with QQmlEngine::CppOwnership
specified.
Generally an application doesn't need to set an object's ownership explicitly. As you can read here, by default, an object that is created by QML has JavaScriptOwnership.
Objects returned from C++ method calls will be set to JavaScriptOwnership also but this applies only to explicit invocations of Q_INVOKABLE methods or slots.
Because the method data is not an explicit C++ method call, you should consider to set the object ownership to QQmlEngine::JavaScriptOwnership calling setObjectOwnership()
So, in general:
Don't use QQmlEngine::CppOwnership if you want QML to destroy the object. The associated data will be deleted when appropriate (i.e. after the garbage collector has discovered that there are no more live references to the value)
A QSharedPointer probably wouldn't work. You have more information here.

Which are the possibile ways to pass a list of QObject to QML context?

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:
when new object is added to list, only this item will be refreshed
when any of passed object is changed, Qml content have to be automatically refreshed
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;
}
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.

Qt invoking qml function with custom QObject type

I want to invoke a qml method from c++ passing as a parameter a custom type.
This is my custom type (*Note that all the code I post is not exactly what i'm using, i've extracted the relevant parts, so if you try to compile it is possible that some changes have to be made)
Custom.h
class Custom : public QObject
{
Q_OBJECT
Q_PROPERTY(QString ssid READ getSSID WRITE setSSID)
public:
Q_INVOKABLE const QString& getSSID() {
return m_ssid;
}
Q_INVOKABLE void setSSID(const QString& ssid) {
m_ssid = ssid;
}
}
private:
QString m_ssid;
};
Q_DECLARE_METATYPE(NetworkInfo)
Custom.cpp
Custom::Custom(): QObject() {
setSSID("ABC");
}
Custom::Custom(const Custom & other): QObject() {
setSSID(other.getSSID());
}
This is where I call the qml method
void MyClass::myMethod(const Custom &custom) {
QMetaObject::invokeMethod(&m_item,
"qmlMethod", Q_ARG(QVariant, QVariant::fromValue(custom)));
}
where QQuickItem& m_item
I have declared Custom as QML and QT meta types
qmlRegisterType<Custom>("com.mycustom", 1, 0, "Custom");
When i try to access the parameter inside the qml
import com.mycustom 1.0
function(custom) {
console.log(custom.ssid)
}
I get
qml: undefined
If i do console.log(custom) I get
qml: QVariant(Custom)
so, i'd say my type is correctly imported into Qt (I have used it without problems instantiating it directly in Qml with
Custom {
....
}
Now, the question :)
Why can't I access the ssid property ?
Since you seem to have registered everything correctly, you might be encounting a bug that sometimes happen with QML where objects seem to be stuck in a QVariant wrapping. Try the following and see if it work.
import com.mycustom 1.0
property QtObject temp
function(custom) {
temp = custom;
console.log(temp.ssid);
}
Sometimes forcing your instance to be a QtObject will remove the annoying "QVariant wrapper" and solve the issue.

Qt Drag and Drop Treeview : what am I missing

I have a standard treeview which is viewing a subclass of QStandardItemModel.
The items in the model are also subclassed form QStandardItem. The items have an additional object pointer which I use to store a pointer to an instance of my data class, a "stage" (itself a QObject). All items have either a pointer to a stage or subclass of it, or a NULL pointer in the _object.
class MyStandardItem : public QStandardItem
{
public:
MyStandardItem();
MyStandardItem(const QString &text);
MyStandardItem(const QIcon &icon, const QString &text);
~MyStandardItem();
void object(QObject *object) {_object = object;}
QObject *object(){return _object;}
private:
QObject *_object;
};
I want to move items around in the treeview, subject to some restrictions. I have given the treeview the correct policies:
view->setAcceptDrops(true);
view->setDragEnabled(true);
view->setDropIndicatorShown(true);
view->setDragDropMode(QAbstractItemView::InternalMove);
And in my model I provide the following:
Qt::DropActions MyStandardItemModel::supportedDropActions() const
{
return Qt::MoveAction;
}
Qt::ItemFlags MyStandardItemModel::flags(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = Qt::ItemIsEnabled | Qt::ItemIsSelectable;
MyStandardItem *item = dynamic_cast<MyStandardItem*>(itemFromIndex(index));
if(!item || !item->object())
{
return defaultFlags;
}
Stage *stage = dynamic_cast<Stage*>(item->object());
switch (stage->type())
{
case Stage::STAGEA:
return Qt::ItemIsDropEnabled | defaultFlags;
break;
case Stage::STAGEB:
case Stage::STAGEC:
return Qt::ItemIsDragEnabled | defaultFlags;
break;
}
return defaultFlags;
}
The dragging behaviour looks ok. But when I then click on a dragged item in the treeview, the object pointer of the selected item is junk:
void Project::model_clicked(const QModelIndex& index)
{
MyStandardItem *item = static_cast<MyStandardItem*>(_tree_model->itemFromIndex(index));
if(!item || !item->isValid())
return;
QObject *object = item->object();
if(!object)
return;
// object is junk
Stage *stage = static_cast<Stage*>(object);
// and of course stage is junk
}
do I need to implement dropMimeData or something similarly special for the drop for my subclassed MyStandardItem? Since I'm only moving I expected the object pointers to be intact.
If I do need to implement dropMimeData, what is the mimetype of the dragged data? I know I can see it using the model selection, but logically I should be able to get the data from the mimedata.
Your help most appreciated!
Well I found the answer to my own question.
the data is "moved" by Qt inserting into the required position in the model and then deleting it.
This means it is necessary to implement a clone() member to be used by dropMimeData (which must be reimplemented too as far as I can see)
This means widgets must be stored as pointers within objects to allow easy movement within the tree (otherwise the data has to be manually copied between widgets as there's no default copy for QObjects (by design)