Writing Qt Settings with Signals and QML Property Binding


Tags: , , ,

I wrote an article about this topic in the past here. However, I had the chance to work more on the topic so this is a follow-up to that article: a quick way to write typed settings in a Qt/QML app with support for signals, binding and invokable methods from QML.

Let’s say you want to persistently save some data in a Qt application and you are OK with the formats supported by QSettings. QSettings is a great class that handles multi-threading, multi-process contexts and many other aspects very well. However, there are things that I do not particularly like about it in its basic form. First of all it is not well typed: it is mostly based on QVariant, which is good in some cases but less good in others. Second, you don’t get signals when the value associated to a key is changed. Third: you cannot use it very simply in QML (the Settings QML element works well with QSettings, but it is not very practical when you need both). So, I wrote a couple of macros to extend QSettings to get what I need.

Typed Settings

In the past, I was used to write a single central class to handle settings, something like this:

struct MySettings
{
    QString readString1()
    { return m_settings.value("string1", "string1").toString(); }
    QSize readSize()
    { return m_settings.value("size", QSize(100, 100)).toSize(); }
    double readTemperature()
    { return m_settings.value("temperature", -1).toDouble(); }
    QByteArray readImage()
    { return m_settings.value("image", QByteArray()).toByteArray(); }
    void storeString1(const QString& s1)
    { m_settings.setValue("string1", s1); }
    void storeSize(const QSize& size)
    { m_settings.setValue("size", size); }
    void storeTemperature(double t)
    { m_settings.setValue("temperature", t); }
    void setImage(const QByteArray& image)
    { m_settings.setValue("image", image); }
    [...]
}

To make it available to QML, I used to inherit QObject, make methods Q_INVOKABLE or use a wrapper. And no signal is emitted here. So I thought of a quicker syntax:

L_DECLARE_SETTINGS(LSettingsTest, new QSettings("settings.ini", QSettings::IniFormat))
L_DEFINE_VALUE(QString, string1, QString("string1"))
L_DEFINE_VALUE(QSize, size, QSize(100, 100))
L_DEFINE_VALUE(double, temperature, -1)
L_DEFINE_VALUE(QByteArray, image, QByteArray())
L_DEFINE_VALUE(LQTSerializeTest, customVariant, QVariant::fromValue(LQTSerializeTest()))
L_END_CLASS

These macros allow me to have typed entries in a declarative way, without having to write a lot of code: I just need the type, the name and the default value.

Custom types can also be added (just like when using QSettings directly). You just need the streaming operators and you are done:

struct LQTSerializeTest
{
    QString s;
    QImage img;
    int i;
};
Q_DECLARE_METATYPE(LQTSerializeTest)

bool operator==(const LQTSerializeTest& t1, const LQTSerializeTest& t2)
{ return t1.s == t2.s && t1.i == t2.i && t1.img == t2.img; }

QDataStream& operator<<(QDataStream& out, const LQTSerializeTest& v)
{ return out << v.s << v.i << v.img; }

QDataStream& operator>>(QDataStream& in, LQTSerializeTest& v)
{
    in >> v.s;
    in >> v.i;
    in >> v.img;
    return in;
}

Using Settings in QML to Read, Write and Bind

QSettings does not seem to be callable from QML. You can use the Settings QML element for this. However, this means having two different places where the same keys are placed, and it wouldn’t be comfortable to use the Settings element from C++. With the macros above you can write something like this:

L_DECLARE_SETTINGS(LQuickSettings, new QSettings(QSL("settings.ini"), QSettings::IniFormat))
L_DEFINE_VALUE(int, appWidth, 200)
L_DEFINE_VALUE(int, appHeight, 200)
L_DEFINE_VALUE(int, appX, 100)
L_DEFINE_VALUE(int, appY, 100)
L_END_CLASS

and read/set in QML:

import QtQuick 2.12
import QtQuick.Window 2.12
import com.luke 1.0

Window {
    property bool doBind: false

    visible: true

    Component.onCompleted: {
        x = settings.appX
        y = settings.appY
        width = settings.appWidth
        height = settings.appHeight
        doBind = true
    }

    Connections {
        target: settings
        onAppWidthChanged: (w) => console.log("App width saved:", w)
        onAppHeightChanged: (h) => console.log("App height saved:", h)
    }

    Binding { when: doBind; target: settings; property: "appWidth"; value: width }
    Binding { when: doBind; target: settings; property: "appHeight"; value: height }
    Binding { when: doBind; target: settings; property: "appX"; value: x }
    Binding { when: doBind; target: settings; property: "appY"; value: y }
}

Not only you can read/set, you can also connect the changed signals and use the values in QML bindings adding JavaScript handlers.

Note here that while you can create as many instances of the class LQuickSettings as you want, signals are emitted only by a specific instance: the one obtained from the notifier() static method of LQuickSettings. So remember to only use that notifier from the QML thread and create other instances for other uses.

Note also that the same rules of QSettings apply when working with multiple threads: methods are reentrant but not thread-safe.

QSettings is reentrant. This means that you can use distinct QSettings object in different threads simultaneously. This guarantee stands even when the QSettings objects refer to the same files on disk (or to the same entries in the system registry). If a setting is modified through one QSettings object, the change will immediately be visible in any other QSettings objects that operate on the same location and that live in the same process.

https://doc-snapshots.qt.io/qt6-dev/qsettings.html#accessing-settings-from-multiple-threads-or-processes-simultaneously

Repository

These macros are all included in the lqtutils project, and in particular in the lqtutils_settings.h header.

Please report bugs to that project. Bye! 😉

Leave a Reply

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