Skip to content

Practical QML: Exposing QObject Subclasses to QML

How can we pass an instance of a QObject subclass from a Qt/C++ list model to a QML list view? How can we expose a property from a Qt/C++ class to QML, where the type of the property is derived from QObject?

We want to develop a contact list in QML, where the contact data comes from a Qt/C++ model. The finished contact list looks as following:
contact_list
The following code shows how the list view is implemented in QML:

ListView
{
    anchors { fill: parent; margins: 8 }

    delegate: Column 
    {
        height: 56
        Text
        {
            text: contact.firstName + " " + contact.lastName
            color: "blue"
            font.pixelSize: 20
        }
        Text
        {
            text: contact.address.zipCode + " " + contact.address.city
            color: "blue"
            font.pixelSize: 16
        }
    }

    model: ContactModel {}
}

The QML ListView adheres to Qt’s normal model-view-delegate pattern. It gets all its data from the model, ContactModel, which is implemented in Qt/C++ in our case. The model provides contact as its single role. The delegate presents each contact in two lines. The first line shows the first and last name. The second line shows the zip code and the city.
The delegate uses the contact object provided by the model and reads the values of the contact‘s properties firstName, lastName and address. The property address is itself an object with its own properties zipCode and city. In QML lingo, address is an attached property of contact.
When the QML engine instantiates the first item of the contact list from the delegate component (which is basically a factory class for creating contact items), it conceptually creates the following QML item.

Column
{
    height: 56
    Text { ... } // name as before
    Text { ... } // address as before
    Contact {
       id: contact
       firstName: "Susanne"; lastName: "Meier"
       Address {
           id: address
           zipCode: "86332"; city: "Ettal"
       }
    }
}

The other items of the contact list are handled similarly.
Now that we know what we want in QML, we must figure out how to implement our wishes in Qt/C++. Let us work our way top-down, from the ContactModel class over the Contact class to the Address class.
Whenever we want to instantiate a C++ class like ContactModel in QML, we must register the class as a QML type. We add the line

    qmlRegisterType<ContactModel>("com.embeddeduse.models", 1, 0, "ContactModel");

to the main() function of our program. The function qmlRegisterType() is declared in the header QtQml/qqml.h. It tells the QML engine that the C++ class ContactModel is used under the name of ContactModel (the last argument of the function) in QML and that this new QML component is found in version 1.0 of the library “com.embeddeduse.models”. In order to use the C++ class ContactModel like any other built-in QML component in our QML code, we have to add the statement

import com.embeddeduse.models 1.0

to our QML file.
The class ContactModel derives from QAbstractListModel. The contact model stores its contacts in the member variable m_contacts of type QList<Contact *>. We must implement the three pure virtual functions data(), parent() and rowCount() and the virtual function roleNames().

QVariant ContactModel::data(const QModelIndex &index, int role) const
{
    if (index.row() < 0 || index.row() >= rowCount() || !index.isValid())
        return QVariant();

    if (role == ROLE_CONTACT) {
        QVariant v;
        v.setValue(static_cast<QObject *>(m_contacts[index.row()]));
        return v;
    }
    return QVariant();
}

QModelIndex ContactModel::parent(const QModelIndex &child) const
{
    Q_UNUSED(child);
    return QModelIndex();
}

int ContactModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return m_contacts.count();
}

QHash ContactModel::roleNames() const
{
    static QHash roles;
    if (roles.isEmpty())
        roles[ROLE_CONTACT] = "contact";
    return roles;
}

The functions parent() and rowCount() are straightforward and need no further explanation. Our first shot at implementing the data() function might look “simpler” than the solution above.

    if (role == ROLE_CONTACT) {
        return m_contacts[index.row()];
    }

This does not even compile, because neither a constructor QVariant(Contact *) nor a constructor QVariant(QObject *) exist. The QVariant constructor is called because data() returns a QVariant.
The compile error is nothing but a reminder that we must make our Contact class known to Qt’s meta-object system. Otherwise, QVariant cannot handle it. Our first reflex may be to use qRegisterMetaType(). However, it would require our class to have a copy constructor, which would require QObject to have a copy constructor – which it doesn’t have (the documentation of the Q_DISABLE_COPY macro gives the reason).
This is normally the point where people give up and come up with not-so-nice workarounds. But not us! Fortunately, QVariant provides an alternative way to “register” subclasses of QObject. The template function void setValue(const T& value) allows us to store values of type T in a QMetaType object and hence in a QVariant object, if type T is not supported by QVariant. Its counterpart T value() const allows us to read the stored value again. QMetaType contains special code to handle QObjects. As long as we only want to use properties, signals, slots and invokable functions of our custom class derived from QObject, we are fine. With the help of setValue(), we can wrap our Contact pointer – cast to a pointer of its base class QObject – in a QVariant and pass it to QML. This finally explains these five lines from ContactModel::data() of how to pass a Contact object to the QML view.

    if (role == ROLE_CONTACT) {
        QVariant v;
        v.setValue(static_cast<QObject *>(m_contacts[index.row()]));
        return v;
    }

The function ContactModel::roleNames() is much easier to explain. It associates the C++ roles (given as enum constants) with the QML property names. In Qt 5, we must reimplement the virtual function roleNames() to set this association. In Qt 4, we could use the function setRoleNames(), which is not available in Qt 5 any more.
The most difficult part of our little project is done. Let us now look at the code of the Contact class. The relevant part of its declaration looks as follows:

class Contact : public QObject
{
    Q_OBJECT
    Q_DISABLE_COPY(Contact)

    Q_PROPERTY(QString firstName READ firstName WRITE setFirstName NOTIFY firstNameChanged)
    Q_PROPERTY(QString lastName READ lastName WRITE setLastName NOTIFY lastNameChanged)
    Q_PROPERTY(QString phoneNumber READ phoneNumber WRITE setPhoneNumber NOTIFY phoneNumberChanged)
    Q_PROPERTY(Address *address READ address WRITE setAddress NOTIFY addressChanged)

// Implementation of the getters, setters and signals of the properties
...

The class Address is implemented very similar to Contact – except that it doesn’t have a property like address of a type derived from QObject. It simply defines the QString properties street, zipCode and city together with all the necessary boiler-plate getters, setters and notification signals.
The code of Contact as given above compiles without any complaints. However, it doesn’t work properly. The contact items lack the line with the address. Moreover, the QML engine emits the error message

QMetaProperty::read: Unable to handle unregistered datatype 'Address*' for property 'Contact::address'

because it doesn’t know anything about our Address class. From our discussion about implementing ContactModel::data(), we know that we cannot register Address with QVariant. Similar to our solution for data(), we could camouflage the address property as a QObject pointer. However, this would require some ugly casting in the property’s setter.
Fortunately, there is a pretty nice solution and we know it already. We simply register Address as a QML component in main():

    qmlRegisterType<Address>("com.embeddeduse.models";, 1, 0, "Address");

No runtime errors any more. We have exposed a property from C++ to QML, where the type of the property is a subclass of QObject. Of course, we cannot only read data from such a property, but we can also write data to such a property. We demonstrate this by adding a MouseArea to the Text, which presents the zip code and city.

Text
{
    text: contact.address.zipCode + " " + contact.address.city
    color: "blue"
    font.pixelSize: 16
    MouseArea
    {
        anchors.fill: parent
        onReleased: contact.address.city = "Heidelberg"
    }
}

Whenever the user clicks on the line with the zip code and city name, the city name changes to “Heidelberg”. The city name is instantly changed in the list view, because the setter function for the address‘s city property emits a signal notifying its observers about the changed city name.
So in the end, everyone lives happily in Heidelberg – not a bad result at all.

Leave a Reply

Your email address will not be published. Required fields are marked *