When using using Package, State and ParentChange to re-parent a delegate to one of several views, the delegate seems to momentarily have a null parent while bindings are re-evaluated. This causes errors if the delegate has bindings to parent.width or similar.
This is disturbing because Qt's published Package example displays the same problem if the initial state is changed to the other alternative
(To reproduce: copy ..../Qt/Examples/Qt-5.14.1/quick/views/package/{view.qml,Delegate.qml} and change the initial upTo value from 0 to 7 in view.qml and then run "qmlscene view.qml".).
Below is a simplification of Qt's example, with a constant state. The active State uses a ParentChange which defines width; this somehow causes the delegate to momentarily have a null parent, and the binding for width:parent.width gets an error:
test.qml line 26: TypeError: Cannot read property 'width' of null
What causes this, or rather, how to avoid it? And, how could the Package mechanism ever work given that Qt's example has the same problem as noted above?
import QtQuick 2.0
import QtQml.Models 2.1
Rectangle {
width: 300; height: 400
ListModel {
id: myModel
ListElement { display: "One" }
ListElement { display: "Two" }
ListElement { display: "Three" }
}
DelegateModel {
id: visualModel
model: myModel
delegate:
Package {
Rectangle { id: rectA;
width: 40; height: 25; Package.name: 'pkgA'
}
Rectangle { id: rectB;
width: 40; height: 25; Package.name: 'pkgB'
}
Rectangle {
id: wrapper
width: parent.width; // ERROR HERE: parent is sometimes null!
height: 25
color: 'lightsteelblue'
state: 'state1'
states: [
State {
name: 'state1'
ParentChange {
target: wrapper; parent: rectB
width: rectB.width; height: rectB.height // **CAUSES ERROR**
}
}
]
Component.onCompleted: {
console.log("wrapper onCompleted: parent is",parent);
}
onParentChanged: {
console.log("wrapper parent changed to",parent);
}
}
}
}
ListView {
width: 300; height: 200
model: visualModel.parts.pkgA
}
ListView {
width: 300; height: 200
model: visualModel.parts.pkgB
}
}
Ok, finally figured this out. You are starting with this:
width: parent.width
which is a Qt binding expression. It will update whenever parent or parent.width changes. And when the wrapper Rectangle first is created, parent starts rightly as null - it has no visual parent yet.
But then you do a ParentChange like this:
ParentChange {
target: wrapper; parent: rectB
width: rectB.width; height: rectB.height // **CAUSES ERROR**
}
The width assignment in the ParentChange is removing your previous binding and placing a new one of rectB.width width on it. This is why console logging on the original binding stops firing at this point. That binding has been removed.
So, bottom line, you are misinterpreting what is happening. Parent is null to start with and gets set via the state initial value before Component.onCompleted fires. Then when you do the ParentChange, you are wiping out the original binding so the debugging you went through was misleading.
So back to your original question, to stop the error, just remove the width and height bindings altogether from wrapper and let the ParentChange apply the ones there which won't throw errors.
Related
I have a nested listview structure where the delegate listview will contain another listview. I would like the nested listview's delegate to refer to itself because the nested listview will contain the same type of item as itself, but this doesn't appear to work.
Component {
id: subSequenceComponent
ItemDelegate {
id: subSequenceItemDelegate
property var id: edit.id
ColumnLayout {
Text{
text: edit.name
}
ListView {
width: 180; height: 200
model: items.subModelFromId(subSequenceItemDelegate.id)
delegate: subSequenceComponent
}
}
}
}
This works:
Component {
id: subSequenceComponent
ItemDelegate {
id: subSequenceItemDelegate
property var id: edit.id
ColumnLayout {
Text{
text: edit.name
}
ListView {
width: 180; height: 200
model: items.subModelFromId(subSequenceItemDelegate.id)
delegate: Text{
text: edit.name
}
}
}
}
}
Is there a way to reuse the same delegate you are a part of?
It may be connected to this bug. Basically, QML has some checks that are supposed to prevent accidental infinite recursions, but they are not particularly well implemented, and trigger false positives even for scenario where nesting is intended and there is no danger of infinite recursion.
If that is the case, then you can trick that check by using an additional Loader that will load the component from a string, which will not catch the nesting recursion.
I have a screen with some rectangles which can contain text. The text content of these rectangles should be allowed to change through clicking on buttons in the screen where this component is used. The problem I am having is how to know in the screen which uses this component which instance is selected. I thought about solving this via emitting a signal, which transmits the id of the instance as reference, but it seems this does not work. How could this be accomplished? Here my custom rectangle component
Rectangle {
id: root
width: 50
height: 50
color: "#000000"
anchors.verticalCenter: parent.verticalCenter
border.color: "#555555"
property int value: 0
signal sendId(Item)
Text {
id: displayed_text
color: "#ffffff"
text: root.value
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: 15
}
MouseArea {
id: mouseArea
anchors.fill: parent
onClicked: {
root.border.color="#222222"
root.sendId(root.id)
}
}
}
and here the file where other buttons should change the content of the custom component:
property Item selected: myRectangle
function changeSelected(value) {
selected.value=5
}
function setSelected(it) {
root.selected=it
}
MyRectangle {
id: myRectangle
Component.onCompleted: {
myRectangle.sendId.connect(tempNumber.setSelected)
}
}
MyRectangle {
id: myRectangle1
Component.onCompleted: {
myRectangle1.sendId.connect(tempNumber.setSelected)
}
}
MyRectangle {
id: myRectangle2
Component.onCompleted: {
myRectangle2.sendId.connect(tempNumber.setSelected)
}
}
root.sendId(root.id)
The id is not a regular property. Don't use it as such. The purpose of the id is to get you a reference, with which you can refer to a particular object, so all you really need is:
root.sendId(root)
And if root is your qml file root object, sendId(root) would work too as long as sendId is not shadowed, root members can be referenced directly, keep in mind this only applies to the root object, it won't work for a object that is a direct or indirect parent but not root.
It is recomended practice to abstain from giving everything an id - only use ids when you really need to reference a particular object and no other way exists.
Another thing you are missing is that unlike JS functions, you do have to provide some typing for a signal. You can still use var and pass anything, but usually it is more efficient to narrow down the scope. So you need type and identifier:
signal sendId(Item item)
This way you can access item in the signal handlers, so you can avoid the awkward imperative connection syntax, so instead you can simply:
MyRectangle {
onSendId: tempNumber.setSelected(item)
}
However, I'd say your design is not optional. Signals are supposed to be employed when you aim for generality and reuse. Your usage scenario is more specific, thus the usage of signals can be avoided altogether:
// Rect.qml
Rectangle {
width: 50
height: 50
color: manager.selected === this ? "red" : "blue"
MouseArea {
anchors.fill: parent
onClicked: manager.selected = parent
}
}
// main.qml
Window {
id: manager
visible: true
width: 600
height: 300
property Item selected: null
Row {
spacing: 2
Repeater {
model: 10
delegate: Rect {}
}
}
}
As the following example shows, you can directly access objects by id as long as they can be found down the object tree. The same applies to properties, however while the id will work for any object down the tree, properties will only work if they are declared in the root object of the particular qml file.
The index property of my delegate component is not recognized when it is used in console.log() function:
onClicked: {
identities.qml_del_account(index);
console.log(index);
}
/*Application output:*/
qrc:/Accounts2.qml:74: ReferenceError: index is not defined
line 74 is this:
console.log(index);
Why does it work for the first line but fails on the second? Both lines are located within the same javascript function.
Full QML code is:
Identities {
id: identities
}
ListView {
id: list_identities
width: list_area.width
height: 100
model: identities
delegate: Rectangle {
id: identities_delegate
height: 40
width: parent.width
Text {
id: identities_item
height: parent.height
anchors.left: parent.left
width: 100
text: email
}
Image {
source: "qrc:/images/dots-menu.png"
id: toolbtn_img
anchors.right: parent.right
width: 24
height: 24
MouseArea {
width: parent.width
height: parent.height
onClicked: {
identities.qml_del_account(index);
console.log(index);
}
}
}
}
}
The model is defined in C++ and it contians the function qml_del_account() which works fine and I am not complaining about it.
My guess is: qml_del_account deletes the model's entry for the current delegate, which is subsequently removed, so the log is executed on an no longer existing object model entry context.
Try to reverse the order of log and call to the model's function.
In general I would also recommend to improve the readability by referring to model data in delegates through the model accessor, e.g. model.index
How to make some reusable QML object, which can inject another object?
I've ever tried to use Component & Loader , but seems not what I want. (It still encapsulate the whole QML type and lacks of elasticity, hard to reuse)
Usage example:
Card.qml
import QtQuick 2.0
import QtQuick.Layouts 1.3
Rectangle {
default property var innerObject
property string titleText: "[Hello Untitled Title]"
id: root
color: "#fff"
ColumnLayout {
anchors.fill: parent
Rectangle {
id: header
height: 10
width: parent.width
color: "#666"
RowLayout {
Text { text: titleText; color: "#fff" }
}
}
// How to inject innerObject in here ?
}
}
main.qml
import QtQuick 2.0
import QtQuick.Layouts 1.3
Card {
titleText: "Image Information"
ColumnLayout { /* .......*/ } // innerObject
}
Card {
titleText: "Image Viewer"
Rectangle { /* .......*/ } // innerObject
}
The answer I linked works like this:
Main.qml
Card {
titleText: "Image Viewer"
innerObject: Rectangle {
Component.onCompleted: {
console.log(parent.objectName)
}
}
}
Card.qml
Rectangle {
property string titleText: "[Hello Untitled Title]"
default property alias innerObject : innercolumn.children
id: root
color: "#fff"
ColumnLayout {
id: innercolumn
objectName: "column"
anchors.fill: parent
Rectangle {
id: header
height: 10
width: parent.width
color: "#666"
RowLayout {
Text { text: titleText; color: "#fff" }
}
}
}
}
I also want to suggest a solution based on default property and reparenting:
The Item which can embed another Item:
MyItem.qml
import QtQuick 2.7
import QtQuick.Layouts 1.2
Rectangle {
id: root
default property Item contentItem: null
border {
width: 1
color: "#999"
}
ColumnLayout {
anchors.fill: parent
Rectangle {
Layout.fillWidth: true
height: 30
color: "lightgreen"
}
Item {
id: container
Layout.fillWidth: true
Layout.fillHeight: true
}
}
onContentItemChanged: {
if(root.contentItem !== null)
root.contentItem.parent = container;
}
}
Can be used as below:
main.qml
import QtQuick 2.7
import QtQuick.Window 2.0
Window {
visible: true
width: 600
height: 600
MyItem{
width: 400
height: 400
anchors.centerIn: parent
Text {
text: "Hello!"
anchors.centerIn: parent
}
}
}
But I still agree with #ddriver that Loader is the best solution for this case
It is not mandatory that you use a Loader with a component. You can just go:
Loader {
source: "Something.qml"
}
When the source is something that can be loaded synchronously, you can directly use the loader's item for stuff like bindings, without worrying about whether or not it is created. If you load over network, you have to delay the bindings until the item is completed and use either a Binding element or Qt.binding() to do it respectively in a declarative or imperative manner.
In your case, a loader would be appropriate, and the property for the inner dynamic object outta be a Component. This way you can populate it either with an inline component, or with Qt.createComponent() from existing source.
property Component innerObject
...
innerObject: Component { stuff }
...
innerObject: Qt.CreateComponent(source)
Of course, there are even more advanced ways to do it, for example, the "generic QML model object" I have outlined here. It allows to quickly and easily create arbitrary data structure trees both declaratively and imperatively, and since the object is also a model, you can directly use listviews or positioner elements with repeaters to layout the gui without actually writing the UI code each and every time.
Also, from your main.qml code example - you cannot have more than one root element in a qml file.
Edit: The default property approach actually works if the element is moved to its own qml file, so also basically you could just:
default property alias innerObject: innerColumn.children
where innerColumn is the id of your ColumnLayout. Also, innerObject could be whatever legal name, since as a default property, it will not actually be used.
There is also the option to not use a default property, which is useful when the root item still needs to have its own children, but still have the ability to redirect declarative objects to be children of a sub-object:
property alias content: innerColumn.children
// and then
content: [ Obj1{}, Obj2{}, Obj3{} ] // will become children of innerColumn
I know that there is tons of topic similar like this, I try to implement answer from them and I still have no results.
I take some sample project from qt creator to play with this. I play with changing visibility of qml files ( treat every file as other screen). After lunching 3rd screen I want to make the second one invisible.
Here Is the code where I want change property in it:
MyLuncherList.qml
import QtQuick 2.0
Rectangle {
Item
{
id:ei
visible:false
clip: true
property url itemUrl
onItemUrlChanged:
{
visible = (itemUrl== '' ? false : true);
}
anchors.fill: parent
anchors.bottomMargin: 40
Rectangle
{
id:bg
anchors.fill: parent
color: "white"
}
MouseArea
{
anchors.fill: parent
enabled: ei.visible
//takes mouse events
}
Loader
{
focus:true
source: ei.itemUrl
anchors.fill: parent
}
}
}
and here is the code where I want to make a action
View2.qml
import QtQuick 2.0
Rectangle {
width: 100
height: 62
Text
{
text: "second screen"
}
MyLuncherList
{
id:luncherList
}
Rectangle
{
x: 50
y: 30
width: 120
height: 60
color: "red"
MouseArea
{
anchors.fill: parent
id: mouseAreaWhichHides
onClicked:
{
luncherList.ei.itemUrl = '';
}
}
}
}
and I got the error: qrc:///View2.qml:29: TypeError: Type error
which point on this line luncherList.ei.itemUrl = '';
Type error says that I make some mismatch with Type, but I’m not even sure, if I do this access process in properly way, so I’m asking how to change property of
ei.itemUrl
from
View2.qml
in working way.
The ei element won't be available directly in other QML file.
You can use an alias to do it.
property alias callUrl: ei.itemUrl
and call it from other QML file
luncherList.callUrl='file:///home/user/file.jpg'