qtscopeqmlqt5

What is the scope of ID in QML?


The documentation said we cannot have the same ID in one file. That means we can have the same ID in a different file, right? I don't know the scope of ID in QML, so I wrote the code as following to test it.

//a.qml
Item {
  id: a_item
  x:20;
  y:b_item.x // cannot access this id
  y:b1.x1 // can access
  Item {
    id:a1 
    x:20
    Component.onCompleted : a1.x //this a1 is a.qml's a1 not the a1 in main.qml
  }
}
//b.qml
Item {
  id: b_item
  x:20;
  property int x1: 30;
}
//main.qml
Item {
  a {
    id:a1
    Component.onCompleted : b1.x = 1 //can access
  }
  b {
   id:b1
  }
  function() {
    a_item.x = 1; // cannot access this id
  }
}

My question:

  1. What is the scope of ID in QML? In my test, the result shows that an Item cannot access the ID of its children and its sibling's children, but can access its parent or sibling, right?

  2. The same ID in different files just as I show in my code, no error and it worked. But how can I differentiate them?


Solution

  • The canonical answer would be:

    The scope of ids is the component scope.

    And the component scope is:

    Each QML component in a QML document defines a logical scope. Each document has at least one root component, but can also have other inline sub-components. The component scope is the union of the object ids within the component and the component's root object's properties.

    Which itself is not overly informative on what the scope exactly is and how can you make optimal use of it. A tad more informative:

    In QML, component instances connect their component scopes together to form a scope hierarchy. Component instances can directly access the component scopes of their ancestors.

    Basically, each id in a qml file is implemented sort of like a property of that source's root item. Except it cannot be accessed via someobj.someId, only via someId.

    Which means that this id can be accessed by any object that exists in the branch that extends from the root object thanks to qml's dynamic scoping.

    That is as long as it is not shadowed by an identically named id or property.

    a_item will be visible in a.qml as well as any object that exists in the branch its root Item grows.

    It won't be visible from main.qml as that object is further down the tree, where a_item is not defined.

    In the same line of thought, b1 can be accessed from a.qml because b1 is defined in main.qml which is where a.qml is instantiated. But b_item will not be visible from there.

    In fact, since a1 and b1 are defined in main.qml which is the root of the entire application tree, those two ids will be visible from every object of the application, as long as it is a part of the object tree and as long as the identifiers are not shadowed. Note that they will not be visible from singletons or parent-less objects, as those are not part of the application object tree.

    obj tree                a1  b1  a_item  b_item
    main.qml                D   D   X       X
            a.qm            V   V   D       X
                Item a1     V   V   V       X
            b.qml           V   V   X       D
    
    D - defined here, V - visible here, X - not available
    

    The same is true for properties, although dynamic scoping only works for properties that are defined for the qml file root element, unlike ids which are visible even if they are on a different sub-branch, which is why in the first sentence of this answer I put it as "implemented sort of a property of that source's root item":

    Obj
      Obj
        Obj
          id: objid
          property objprop
      CustomObj
    

    So objid will be visible in CustomObj, but objprop will not be, since it is not an id and not defined in the root object. The id is identical to doing this:

    Obj
      property objid : _objid
      Obj
        Obj
          id: _objid
    

    All ids from a given sources are visible in the qml source root object's context and subsequently everything else that will eventually drop down to this context as it lookup fails to resolve the identifier in the "higher" contexts.

    Finally, keep in mind the subtle trap - it is only possible to use ids across sources if you know for certain that your application will instantiate the objects in a compatible context tree.

    For example:

    A.qml {
      id: objA
      B { } // objA will be visible to this object
    }
    
    main.qml
      A {
        B {} // objA will NOT be visible to this object
      }
      B {} // objA will NOT be visible to this object
    

    The trap continues - context tree comes before object tree - the context in which an object is created matters, and cannot be changed once set (puts certain limits on reparenting depending on context dependencies).

    // ObjA.qml
    Item {
      id: objA
      Component {
        id: cm
        ObjB {}
      }
      function create() { cm.createObject(objA) }
    }
    
    // ObjB.qml
    Item {
      Component.onCompleted: console.log(objA)
    }
    
    // main.qml
      Component {
        id: cm
        Rect {}
      }
    
      Obj {
        anchors.fill: parent
        MouseArea {
          anchors.fill: parent
          acceptedButtons: Qt.LeftButton | Qt.RightButton
          onClicked: {
            if (mouse.button === Qt.LeftButton) {
              cm.createObject(parent)
            } else {
              parent.create()
            }
          }
        }
      }
    

    As this practical example illustrates, even though in both cases the newly created object is parented to the same object that has the objA identifier, the object created in main.qml cannot resolve it, because it is created in a context where objA is still not defined, but it works if the object is created in the context of objA, and it will work even if it is burred even higher up the tree.

    To put it in a more generic way, an id becomes visible in the context of the source's root object and remains visible in every subsequent sub-context until it is shadowed by an identically named object. Visibility cannot reach down the tree to contexts that exist before the context the id is defined in. Note the subtle difference - a_item refers to an Item whereas a1 refers to an a. And since a1 is visible inside a.qml it will always refer to that one instance of a that is in main.qml, regardless of which instance of a you might be in, whereas a_item will refer to a different object for each different instance of a. a_item is "relative" and will be different in every different instance of a but a1 is absolute and will always refer to a specific instance of a. This is the case because a1 is a concrete instance whereas a_item is a type / prototype.

    // Obj.qml
    Item {
      id: obj
      Component.onCompleted: console.log(obj === oid)
    }
    
    // main.qml    
      Obj { } // false
      Obj { id: oid } // true 
    

    Dynamic scoping of ids can be quite useful and cut the time it takes to implement a workaround to get access to the stuff you need. Which is also why it is a very good idea to give the id descriptive names rather than just main.

    For example, if you have a manager that manages a number of views, each with a number of objects in them, you can quickly get access the respective view for each object and also get access to the manager without having to implement any additional stuff. The rule of thumb is that the manager must come first, then each view should be created in the context of the manager, not necessarily directly in it, but in it nonetheless, and each object should be created in the context of a view. And of course take care not to shadow over things. If you break that rule things will not resolve properly.

    View.qml { id: view }
    
    manager
      view1
        object // view is view1
      view2
        object // view is view2
      view3
        object // view is view3
    

    Naturally, this makes sense only in specific purpose designs where you know what the general structure of the context tree is gonna be like. If you are making generic elements that may go just about anywhere, you should absolutely not be relying on accessing ids across sources, and you should implement a more generic usage interface via properties, aliases and whatnot.