Ricardo Rodrigues Ricardo Rodrigues - 3 months ago 11
C++ Question

How to implement a Qt-like Property system?

I've been using Qt for a while now and I find fascinating how their Property system works.

QPushButton *button = new QPushButton; // inherits a QObject
button->setProperty("down", true);
button->setProperty("angle", 35.0);

QVariant value = button->property("angle");


I started wondering how I could implement it. What makes it possible to be so easy to use?

Answer

Once you have a suitable variant class, it's easy. All you need is a map from names to variants:

#include <map>
#include <string>
#include <cassert>
#include <boost/variant.hpp>

class WithProperties {
public:
    using variant = boost::variant<std::string, int, double, bool>;
    template <typename T> T property(const char * name) const {
        auto it = m_properties.find(name);
        if (it != m_properties.end()) return boost::get<T>(it->second);
        return T{};
    }
    void setProperty(const char * name, const variant & value) {
        m_properties[name] = value;
    }
    std::vector<std::string> propertyNames() const {
        std::vector<std::string> keys;
        keys.reserve(m_properties.size());
        for (auto prop : m_properties)
            keys.push_back(prop.first);
        return keys;
    }
private:
    std::map<std::string, variant> m_properties;
};

int main() {
    WithProperties prop;
    prop.setProperty("down", true);
    prop.setProperty("angle", 35.0);
    prop.setProperty("name", std::string{"foo"});
    assert(prop.property<bool>("down") == true);
    assert(prop.property<double>("angle") == 35.0);
    assert(prop.property<std::string>("name") == "foo");
}

If you're wondering how it's done using Qt's types, it's even easier, because QVariant implements helpful operator== and constructor that knows how to deal with all basic C value types.

#include <QtCore>

class WithProperties {
public:
    QVariant property(const char * name) const {
        auto it = m_properties.find(name);
        if (it != m_properties.end()) return *it;
        return QVariant{};
    }
    void setProperty(const char * name, const QVariant & value) {
        m_properties[name] = value;
    }
    QList<QByteArray> propertyNames() const {
        return m_properties.keys();
    }
private:
    QMap<QByteArray, QVariant> m_properties;
};

int main() {
    WithProperties prop;
    prop.setProperty("down", true);
    prop.setProperty("angle", 35.0);
    prop.setProperty("name", "foo");
    Q_ASSERT(prop.property("down") == true);
    Q_ASSERT(prop.property("angle") == 35.0);
    Q_ASSERT(prop.property("name") == "foo");
}

Qt's property system does one more thing: it uses the staticly-named properties declared using Q_PROPERTY. These are available via metadata, and are integrated with dynamic properties as seen above. You could implement it as follows (this is not copied from Qt code):

#include <QtCore>
#include <cstring>

QMetaProperty findMetaProperty(const QMetaObject * obj, const char * name) {
    auto count = obj->propertyCount();
    for (int i = 0; i < count; ++i) {
        auto prop = obj->property(i);
        if (strcmp(prop.name(), name) == 0)
            return prop;
    }
    return QMetaProperty{};
}

class WithProperties {
    Q_GADGET
    Q_PROPERTY(QString name READ name WRITE setName)
    QString m_name;
public:
    QString name() const { return m_name; }
    void setName(const QString & name) { m_name = name; }
    QVariant property(const char * name) const {
        auto metaProperty = findMetaProperty(&staticMetaObject, name);
        if (metaProperty.isValid())
            return metaProperty.readOnGadget(this);
        auto it = m_properties.find(name);
        if (it != m_properties.end()) return *it;
        return QVariant{};
    }
    void setProperty(const char * name, const QVariant & value) {
        auto metaProperty = findMetaProperty(&staticMetaObject, name);
        if (metaProperty.isValid())
            return (void)metaProperty.writeOnGadget(this, value);
        m_properties[name] = value;
    }
    QList<QByteArray> dynamicPropertyNames() const {
        return m_properties.keys();
    }
private:
    QMap<QByteArray, QVariant> m_properties;
};

int main() {
    WithProperties prop;
    prop.setProperty("down", true);
    prop.setProperty("angle", 35.0);
    prop.setProperty("name", "foo");
    Q_ASSERT(prop.property("down") == true);
    Q_ASSERT(prop.property("angle") == 35.0);
    Q_ASSERT(prop.property("name") == "foo");
    Q_ASSERT(prop.dynamicPropertyNames().size() == 2);
}
#include "main.moc"