Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Two way binding C++ model in QML

Tags:

c++

qt

qml

qtquick2

I'm trying to learn more about QtQuick and QML. My current goal is to understand how to bind data from a C++ model to my view. So far I've been able to setup the model in my QML and get data from the model but I can't figure out how to update my data.

How do I setup two way binding for my C++ model? Below is the code I've written so far.

message.h

class Message : public QObject
{
    Q_OBJECT

    Q_PROPERTY(QString author READ getAuthor WRITE setAuthor NOTIFY authorChanged)
    Q_PROPERTY(QString message READ getMessage WRITE setMessage NOTIFY messageChanged)

    Q_SIGNALS:
        void authorChanged(QString author);
        void messageChanged(QString message);

    public:
        Message(QObject *parent = 0);

        QString getAuthor();
        void setAuthor(QString author);

        QString getMessage();
        void setMessage(QString message);

    private:
        QString _author;
        QString _message;
};

message.cpp

#include "message.h"

Message::Message(QObject *parent) : QObject(parent)
{
}

QString Message::getAuthor()
{
    return _author;
}

void Message::setAuthor(QString author)
{
    if(author != _author)
    {
        _author = author;
        emit authorChanged(author);
    }
}

QString Message::getMessage()
{
    return _message;
}

void Message::setMessage(QString message)
{
    if(message != _message)
    {
        _message = message;
        emit messageChanged(message);
    }
}

main.qml

import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.0
import com.butts.messaging 1.0

ApplicationWindow {
    visible: true
    width: 640
    height: 480
    title: "Test"

    Message {
        id: testMessage
        author: "Batman"
        message: "Hello World!"
    }

    Flow {
        TextField {
            text: testMessage.message
        }

        Label {
            text: testMessage.message
        }
    }
}

main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include "message.h"

int main(int argc, char *argv[])
{
    qmlRegisterType<Message>("com.butts.messaging", 1, 0, "Message");

    //Message msg = Message();

    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);

    QQmlApplicationEngine engine;
    engine.load(QUrl(QLatin1String("qrc:/main.qml")));

    return app.exec();
}

P.S. I'm a giant noob at this so feel free to point out any other issues (formatting, standards, etc.) I have in my code, I need to learn somehow lol

Edit 1

After reading @derM's answer I changed my code to achieve what I wanted

TextField {
    id: editor

    //Binding model -> view
    text: testMessage.message

    //Binding model <- view
    Binding {
        target: testMessage
        property: "message"
        value: editor.text
    }
}

Label {
    id: display

    //Binding model -> view
    text: testMessage.message
}
like image 660
amura.cxg Avatar asked Dec 20 '16 00:12

amura.cxg


2 Answers

Twoway binding is a complicated matter in QML, as it usually works as some assignment.

So, if you bind a property with propertyname: valuetobeboundto and later assign something to propertyname again, this binding will be lost.

As a workaround there are two ways: The use of Binding-Objects or to not use binding, but handle all the property-change-signals (which your model ideally properly emits) manually.

For the first, you can find a detailed instruction here. Here they use the one Binding-Object for each direction. The good thing is, those Bindings will not be overridden, by assignment of a new Binding.

Consider:

Row {
    spacing: 2
    Rectangle {
        id: r0
        width: 50
        height: 30
    }

    Rectangle {
        id: r1
        width: 50
        height: 30
        color: b2.pressed ? 'red' : 'blue'
    }

    Button {
        id: b2
    }

    Button {
        id: b3
        onPressed: r1.color = 'black'
        onReleased: r1.color = 'green'
    }

    Binding {
        target: r0
        property: 'color'
        value: b2.pressed ? 'red' : 'blue'
    }


    Binding {
        target: r0
        property: 'color'
        value: (b3.pressed ? 'black' : 'green')
    }
}

At the beginning the value of r1 is bound to the state of b2, but as soon as b3 has been pressed once, r1 won't be updated by a click on b2 anymore. For r0 the updating will be done by the two Binding-Objects, and therefore the Binding won't be lost. However, you can see, how the binding works: When ever the state of the Button changes, the Binding will be updated. So the press AND the release of b2 will fire signals, that will be handled by the first Binding and the same goes for the press AND relase of b3.

Now coming to the two-way binding. Here it is important to avoid Binding-Loops.

Row {
    Button {
        id: count0
        property int count: 0
        onClicked: count += 1
        text: count
    }

    Button {
        id: count1
        property int count: 0
        onClicked: count += 1
        text: count
    }

    Binding {
        target: count0
        property: 'count'
        value: count1.count
    }

    Binding {
        target: count1
        property: 'count'
        value: count0.count
    }
}

While this example is perfectly fine. The changing of count0.count will trigger a change of count1.count. Now it is checked, if count0.count would need an update, but the value is already the right, so the recursion ends, and no binding-loop occures.

Changing the second Binding to

    Binding {
        target: count1
        property: 'count'
        value: count0.count + 1
    }

drastically changes the situation: Now with each change of count0.count, count1.count needs to be raised. The first Binding then tries to set count0.count to the same value as count1.count but there is just no way that both Binding will be satisfied, and no change is needed to be done, after the other Binding did it's work. It will result in a binding-loop. Luckily those are detected pretty fine in QML, so a lock is avoided.

Now there is only one last thing to take care of: Consider this Component-Definition:

// TestObj.qml
Item {
    width: 150
    height: 40
    property alias color: rect.color
    Row {
        spacing: 10
        Rectangle {
            id: rect
            width: 40
            height: 40
            radius: 20
            color: butt.pressed ? 'green' : 'red'
        }
        Button {
            id: butt
            text: 'toggle'
        }
    }
}

Here we have an internal binding of the color-property, by using the propertyname: valueToBeBoundTo-Syntax. This means, the internal binding might be overwritten by any external assignemtn of the color-property. Replace this binding by a Binding-Object, and you should be fine.

The same would go the other way around: color is externally bound to some value, and then you handle a signal internally and assign a value to it, the external binding would be lost, if not created by a Binding-Object.

This is only a general overview. There are way more details that might alter the behavior of Binding. But I think I have shown, how you can create a Two-Way-Binding and mentioned quite some pitfalls, you might encounter.

like image 51
derM Avatar answered Oct 06 '22 16:10

derM


This works for me, with Qt Quick Controls 2:

TextField {
    id: txt

    text: testMessage.message

    onTextChanged: testMesage.message = txt.text
}
like image 28
user240515 Avatar answered Oct 06 '22 15:10

user240515