javascriptqtqmlqtquick2qquickitem

How to transform a QQuickItem's x or y property based on its current value without using a QML function


I am using Qt 5.9.3 commercial version.

Scenario
I have logic to be executed in Qt QML part of the my code. On click of some random button. I want to move a QML rectangle to another position. The other position is a calculated (x, y) position based on current position of the rectangle.

Following is sort of what I want to do:

Code:

import QtQuick 2.9
import QtQuick.Window 2.2

Window {
    visible: true
    width: 800
    height: 600
    title: qsTr("Hello World")

    property bool some_boolean_flag: false

    Rectangle {
        id: my_rect_1
        color: "blue"
        width: parent.width / 4
        height: parent.height / 4
        z:1

        property real current_x
        property real current_y

        x: {
            if (some_boolean_flag) {
                current_x = current_x + current_x/10
                return current_x
            }
            else {
                var default_x = parent.width/6
                current_x = default_x
                return current_x
            }
        }
        y: {

            if (some_boolean_flag) {
                current_y = current_y + current_y/10
                return current_y
            }
            else {
                var default_y = parent.height/6
                current_y = default_y
                return current_y
            }
        }
    }

    Rectangle {
        id: my_rect_2
        color: "yellow"
        width: parent.width/5
        height: parent.height/5
        x: parent.width - parent.width/4
        y: parent.height - parent.height/4
        z:2

        MouseArea {
            anchors.fill: parent
            onClicked: {
                // swapping the boolean flag between true and false
                some_boolean_flag = !some_boolean_flag
            }
        }
    }
}

The objective of this question:
Note that I want to do the calculation declaratively. I can easily create a QML function and do this declaring some global properties which is of course easy but doing this calculation is not the point of the question. My objective is to learn how to to do this declaratively. How to apply a calculation to a QQuickItem::x based on the QQuickItem::x's current value itself..

If you run the example code in this question, you will see that my_rect_1 just keeps swapping between 2 positions back and forth which is not what the logic really intends. The intention of the logic is to keep incrementing my_rect_1::x and my_rect_1::y by a certain factor which would cause my_rect_1 to get moved rightwards and downwards as you keeping clicking on the other rectangle that is my_rect_2.

Other issue
is that I keep getting the following error at runtime when I click on my_rect_2.

qrc:/main.qml:12:5: QML Rectangle: Binding loop detected for property "x"

I understand QML is complaining about the circular dependency of the property x in my_rect_1. How can I avoid this and achieve a declarative way of transforming x and y?

Potential solution I found from my searches
Looks like Translate QML Type can be useful in the logic I want to implement but how?

How can I achieve this behaviour without writing a QML function which does this transformation separately outside my_rect_1?


Solution

  • Well, "declaratively" would simply involve bindings, which are in their essence pretty much functions, with the added benefit of automatic reevaluation upon changes in the values involved in the expressions.

    Here is a trivial example that will show a red dot on screen, and when clicking on the window, the dot will move to a location that is derived by the dot's current location and the clicked location, putting it right in the center of the imaginary line between the two coordinates:

    ApplicationWindow {
      id: main
      width: 640
      height: 480
      visible: true
    
      property real xpos: 10
      property real ypos: 10
    
      Rectangle {
        id: rect
        x: xpos
        y: ypos
        width: 10
        height: 10
        radius: 5
        color: "red"
      }
    
      MouseArea {
        anchors.fill: parent
        onClicked: {
          xpos = (xpos + mouseX) * .5
          ypos = (ypos + mouseY) * .5
          console.log(ypos)
        }
      }
    }
    

    Note that the xpos and ypos properties are practically redundand, and the same thing can be achieved without that, but it would involve manually manipulating the dot coordinates, thus its position will not be declared in a purely "declarative way". This way it is bound to the property values, and will keep on following them regardless of what process controls the values.

    As for this part of your code:

      x: {
          if (some_boolean_flag) {
              current_x = current_x + current_x/10
              return current_x
          }
          else {
              var default_x = parent.width/6
              current_x = default_x
              return current_x
          }
      }
    

    It is the binding expression that defines the property value. If you assign a value to a property manually, it breaks the existing binding. What you do is you assign a value to the property in the expression that is supposed to do that automatically - pure nonsense, which also explains why it doesn't work as expected.

    The appropriate format would be:

      x: {
          if (some_boolean_flag) {
              return current_x + current_x/10
          }
          else {
              var default_x = parent.width/6
              return default_x
          }
      }
    

    Which can actually be distilled down to:

      x: some_boolean_flag ? current_x + current_x/10 : parent.width/6      
    

    and so on... And as you see, using this format removes the binding loop warnings and now you have behavior that matches the code logic intent.