Is there a way to distinguish internally defined children from externally defined ones in QML? - qt

I have defined a MyElement element (in the MyElement.qml file) as the following:
Rectangle {
Item {
}
Component.onCompleted: {
console.warn(children.length)
}
}
Let's call the Item element defined within MyElement the internal child. Now, I'm using the MyElement element in the following way:
MyElement {
Item {
}
}
Here another one Item element is used as a child of MyElement. Let's call this Item element the external child. To understand my question below one should clearly understand the difference between internal and external children.
The output for the presented code will be 2, i.e. both Item elements are calculated as children.
In the future I want to iterate in the block Component.onCompleted only over external children, not over internal (external children go after internal). But for this I have to know a children index from which I have to start (in the given example this index is 1). Is there a way to get this index or, in other words, the number of internal children? Thanks.

There is no internal mechanism in Qt to distinguish internal children from those which are defined outside of the own QML definition.
My workaround is as follow
//MyElement.qml
Rectangle {
id: root
readonly property Item last_item: lastone
Item {
id: someitem
}
Item {
id: lastone
}
Component.onCompleted: {
var external_started = false;
for(var i = 0 ; i < root.children.length ; ++i)
{
if(external_started)
console.log(root.children[i].toString(), 'is external');
else if(root.children[i]===last_item)
external_started = true;
}
}
}
and
MyElement {
Item {
objectName: 'I am external'
}
}
It's a dirty hack but it works.
I'm saving a reference to the last item in a readonly property called last_item and that will distinguish my last item in qml definition.
Other items which are added outside of the qml file, will be placed after this item in the children list.

Related

QML Question about Calling Nested Variables

So I want to call a variable from an object within an object several levels down. The only way I've figured out how to do it successfully is as follows:
//object doing the calling
Text {
text: lv1.lv1Out
}
//object containing the variable I want
Rectangle {
id: lv1
property var lv1Out: lv2Out
Rectangle {
id: lv2
property var lv2Out: variableIWant
Rectangle {
id: lv3
property var variableIWant: 1
}
}
}
Basically I have to define variables at every level, and define the variable I want all the way out of the containing object tree. Is there a more elegant way of doing this? Calling the following doesn't work for me:
Text {
text: lv1.lv2.lv3.variableIWant;
}
Calling a.b.c for nested definitions will not work because children object are contained in parent object but not as variable/property. For this type of inspection you need to use attribute children[i] - in your case children of children and loop over children.
But in your case it is is unnecessary. You have children object named by id so can can do:
Text {
text: lv3.variableIWant;
}

Qml - passing property value between two components

This is a simplification of the situation I am dealing with in main.qml file:
Component {
id: component1
property string stringIneedToPass: "Hello"
Text { text: stringIneedToPass }
}
Component {
id: component2
Rectangle {
id: myRectangle
property string stringIneedToReceive = component1.stringIneedToPass; //this doesn't work
}
}
Obviously my situation is more complicated. But in the end I just need to understand how this kind of transfer should be done!
Thank you all!
First of all, a Component element cannot have properties. Components are either loaded from files, or defined declaratively, in the latter case they can contain only one single root element and an id.
Second - you cannot do assignment in the body of an element, only bindings.
Third - you cannot reference properties defined inside an element inside a component from the outside, as that object doesn't exist until the component is instantiated. Such objects can only be referenced from inside.
Other than that, it will work as expected, if you can reference it, you can bind or assign it to a property, depending on what you want.
So you can simply have the string property external:
property string stringIneedToPass: "Hello"
Component {
id: component1
Text {
text: stringIneedToPass
}
}
Component {
id: component2
Rectangle {
id: myRectangle
property string stringIneedToReceive: stringIneedToPass
}
}

StackView: storing components along with their items

Consider the following code:
StackView {
id: stack
}
Component {
id: componentFooBar
FooBar {}
}
...
stack.push({item: componentFooBar})
In the example above, according to the Qt source and documentation, after componentFooBar is pushed to the stack its inner item (FooBar) is loaded and pushed to the top of the inner stack. And if I want to access stack head (either by pop() or get(0)), I'll get FooBar instance, not its Component. So for Qml StackView there's no invariant like
stack.push(X)
X === stack.pop()
Is there any way to access pushed Components? The only idea I have for now is to write some wrappers for push()/pop()/get() methods and store Components in some separate stack:
StackView {
property var componentStack: []
function _push (x) {
push(x)
componentStack.push(x)
}
function _pop (x) {
pop(x)
return componentStack.pop()
}
}
But for me it looks like an hack, also I have a lot of code using default push()/pop() methods. Is there any other solution?
Create an array of your components, such as here:
StackView {
id: stackView
}
property variant myObjects: [component1.createObject(), component2.createObject(), component3.createObject()]
Component {
id: component1
}
Component {
id: component2
}
Component {
id: component3
}
Then when pushing to StackView make sure to use destroyOnPop property:
stackView.push({item: myObjects[componentIndex], destroyOnPop: false})
By following these two steps, StackView does not take ownership by your objects. Therefore, you can work with objects directly and still push/pop many times to StackView.

How to find QML item by its string id?

I have a string id of an object I need to find in QML tree.
For example:
var idToFind = "myBtnId"
Can I do something like the following?
var objectThatINeed = myMainWindow.findObjectById(idToFind)
As far as I understand I can use objectName for this purpose (at least from C++). Can I still reuse existing ids somehow without introducing the names?
I want to use this object as a parent for some other dynamically created controls.
No, you have to use objectName or some other property.
The id Attribute:
Once an object instance is created, the value of its id attribute cannot be changed. While it may look like an ordinary property, the id attribute is not an ordinary property attribute, and special semantics apply to it; for example, it is not possible to access myTextInput.id in the above example.
If you know the IDs of all items you want beforehand, you can create an object map for them and use that to look them up. For example:
Item {
property var idMap: ({ foo:foo, bar:bar })
Rectangle { id:foo }
Item { id:bar }
function findItemById(id) {
return idMap[id];
}
Component.onCompleted: console.log(findItemById('foo'), findItemById('bar'))
// qml: QQuickRectangle(0x1479e40) QQuickItem(0x148fbf0)
}
QJSValue MyEngine::getQmlObjectById(const QString& id) {
QV4::ExecutionEngine *v4 = QV8Engine::getV4(m_JSEngine);
QV4::Scope scope(const_cast<QV4::ExecutionEngine *>(v4));
QV4::ScopedString name(scope, v4->newString(id));
return QJSValue(v4, v4->currentContext->getProperty(name));
}
Based on the answer from Phrogz, if you don't want an explicit map, you can assign the components to properties and then reference these with the [] operator:
Item {
id: page
property Component foo: Rectangle { color: "yellow" }
property Component bar: Item { }
Component.onCompleted: console.log(page["foo"], page["bar"])
//qml: QQuickRectangle(0x...) QQuickItem(0x...)
}

How to access dynamically/randomly loaded Repeater items in QML?

The answer provided by #TheBootroo here: link
provides a way to load and change between QML files/screens/views. But when doing it like this how can one use signal and slots?
One can access the items created by the repeater by using the Repeater::itemAt(index) method, but since I don't know in what order the items are loaded I don't know what index screen2, screen3, screen4 etc. is at.
Is there any way to solve this or do one have to instantiate all the screens in memory at start up?
My code below:
main.qml:
//List of screens
property variant screenList: [
"main",
"screen2",
"screen3",
...
]
//Set this to change screen
property string currentScreen: "main"
Repeater {
id: screens
model: screenList
delegate: Loader {
active: false;
asynchronous: true
anchors.fill: parent
source: "%1.qml".arg(modelData)
visible: (currentScreen === modelData)
onVisibleChanged: {
loadIfNotLoaded()
}
Component.onCompleted: {
loadIfNotLoaded()
}
function loadIfNotLoaded () {
//To load start screen
if(visible && !active) {
active = true;
}
}
}
}
Connections {
target: screens.itemAt(indexHere)
//screen is here the string passed with the signal from screen2.
onChangeScreen: currentScreen = screen
}
Button {
id: button1
text: "Go To Screen 2"
onClicked: {
currentScreen = "screen2"
}
}
And in screen2.qml:
signal changeScreen(string screen)
Button {
text: "Go To Main"
onClicked: {
changeScreen("main")
}
}
One can access the items created by the repeater by using the Repeater::itemAt(index) method, but since I don't know in what order the items are loaded I don't know what index screen2, screen3, screen4 etc. is at.
The order of the items the Repeater instantiates is actually defined - the items will be instantiated in the order of the model, so in your case "main" will be the first, then "screen2" and so on. Now, each item inside of the Repeater is a Loader - the Repeater will create 3 Loaders in a defined order. The Loaders themselves load their source item on demand.
I think the only thing missing in your code is that the Connections refers to the Loader, but you need it to refer to the item the Loader creates. So change the Connections to
Connections {
target: screens.itemAt(indexHere).item
onChangeScreen: currentScreen = screen
}
Notice the additional .item.
After fixing this, there is one additional problem though: The Repeater hasn't yet instantiated its items when the Connections element is created, so screens.itemAt(indexHere) will return null. One way to fix this would be to use a currentIndex property instead of a currentScreen property and use that in the binding for target, and set currentIndex after the Repeater has instantiated its items:
property int currentIndex: -1
...
Connections {
target: screens.itemAt(currentIndex).item
...
}
Component.onCompleted: currentIndex = 0
Even easier would probably be to put the Connections element inside the Repeater's delegate, i.e. have 3 Connections instead of one (assuming you have 3 screens).

Resources