Delegate supplied as property => how to handle instantiation?
-
I created a QML type that receives its delegate as a (default)
Component
property, so it can handle various kinds of models; the model is avariant
property.I want this type to be able to react to instantiations of this delegate with a handler that has access to the instantiated object (to wire up its signals). With a delegate declared locally, that would be a simple matter of sticking a
Component.onCompleted
handler on it, but in this scenario, the delegate is declared elsewhere and passed in as a property.I have tried two methods, neither one satisfactory:
-
Wrap the delegate in a locally declared
Item
and stick the completion handler on theItem
. Problem: bound role properties (modelData
,model
,index
) don’t pass through to the “inner” delegate without a lot of ugly wrapping code. -
Add a
Component.onCompleted
handler to the object that instantiates the delegate, along these lines:Component.onCompleted: delegate.Component.completed.connect(function () { code here })
This actually runscode here
as expected, but I don’t know how to access the instantiated object.
Anyone able to help?
-
-
Any sample code on this concept ? This will help us to help you.
-
Sure!
[Aside: My actual goal is to create a subclass of
Menu
whose internalListView
createsMenuItem
delegates only as needed, so that the menu can be arbitrarily long without waiting for thousands of items to be created.ListView
knows how to do this (using itscacheBuffer
property), but unfortunately,Menu
is written in such a way that it manages all child objects (Action
orMenuItem
) and feeds a complete set of instantiated items to its embeddedListView
object in the form of a fully built object model.]Here is a non-
Menu
code example to illustrate my issue, which is, how can I connect signals from instantiated delegates to the view that instantiated them, when the delegateComponent
is supplied to the view as an opaque property, not declared in the same .qml file as the view?First, here's the delegate, a
RowLayout
containing aTimer
that counts down 10 seconds and then signals its expiry. (Don't worry too much about this code; it's just a tool for building my view.)MyDelegate.qml
import QtQuick 2.0 import QtQuick.Layouts 1.12 RowLayout { signal expired() property int serial property int limit: 10 property int countdown: limit width: parent.width clip: true Text { id: t1 Layout.alignment: Qt.AlignVCenter text: "Delegate #" + serial; } Rectangle { Layout.alignment: Qt.AlignCenter Layout.fillHeight: true border { color: "black" } color: "red" implicitWidth: (countdown / limit) * (parent.width - t1.width - t2.width) } Text { id: t2 Layout.alignment: Qt.AlignVCenter | Qt.AlignRight text: countdown + " seconds" } Timer { interval: 1000 repeat: true running: true onTriggered: { if (--countdown === 0) { expired() running = false } } } Component.onCompleted: console.log("delegate #" + serial + " completed") Component.onDestruction: console.log("delegate #" + serial + " destroyed") }
Next, I want a
ListView
to manage these delegates so that each row shrinks in height when its timer expires. The following code works nicely; the rows flatten upon expiry, and you can flick around and see how theListView
creates and destroys delegate objects as the rows enter and exit the view area. This code works perfectly, because the view's delegateComponent
is declared inline with its ownonExpired
signal handler. (I'm not concerned about the fact that, for some reason,ListView
never destroys delegate #0, so it stays flattened and is not recreated.)main.qml
import QtQuick 2.12 import QtQuick.Controls 2.12 import QtQuick.Window 2.12 Window { visible: true width: 600 height: 400 title: qsTr("ListView with Delegates That Signal") ListView { anchors.fill: parent ScrollBar.vertical: ScrollBar { } model: 400 cacheBuffer: 0 delegate: MyDelegate { serial: index height: 50 onExpired: height = 1 } } }
All good so far. But now, say I want to make this delegate-flattening
ListView
reusable, so I'm putting it in its own .qml file that takes its model and delegate as opaque properties. First step is to subclassListView
:MyListView.qml
import QtQuick 2.0 import QtQuick.Controls 2.12 ListView { ScrollBar.vertical: ScrollBar { } cacheBuffer: 0 // I want to handle delegate.expired events here }
main.qml
import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 600 height: 400 title: qsTr("ListView with Delegates That Signal") MyListView { anchors { fill: parent } model: 400 delegate: MyDelegate { serial: index height: 50 onExpired: height = 1 // move this to MyListView! } } }
That works fine, but it hasn't accomplished my objective of making the delegate-flattening behavior (the
onExpired
signal handler) an integral part ofMyListView
, and removing it from the mainline. Problem is,MyListView
's delegate is an opaqueComponent
property. How can I attach a signal handler to every instance that is created from it, properly encapsulated inside theMyListView
type?If the property were a simple
Item
, I could use aConnections
object to attach a handler to its signal, but here it's aComponent
that gets instantiated byListView
in logic that is outside my control, so I can't target that property with aConnections
object. One way to take control would be to wrap the delegate in aLoader
. This gets theexpired
event handler out of the mainline (but note, I had to tweak it slightly, to remove the explicitdelegate
property label):MyListView.qml
import QtQuick 2.0 import QtQuick.Controls 2.12 ListView { default property Component insideDelegate ScrollBar.vertical: ScrollBar { } cacheBuffer: 0 delegate: Loader { height: insideDelegate.height width: parent.width sourceComponent: insideDelegate onLoaded: item.expired.connect(function () { item.height = 1 }) } }
main.qml
import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 600 height: 400 title: qsTr("ListView with Delegates That Signal") MyListView { anchors { fill: parent } model: 400 MyDelegate { serial: index height: 50 } } }
This almost works, but the log is full of warnings and all the rows say "Delegate #0", because the delegate no longer sees its bound
index
property. (If the model were aStringList
or C++ model, it would also be missing itsmodelData
,model
, and other role properties thatMyListView
shouldn't have to care about.) Now I have to do some crazy nesting, just to make all necessary bound properties accessible to the delegate:MyListView.qml
import QtQuick 2.0 import QtQuick.Controls 2.12 ListView { default property Component insideDelegate ScrollBar.vertical: ScrollBar { } cacheBuffer: 0 delegate: Loader { height: insideDelegate.height width: parent.width property int _index: index sourceComponent: Loader { property int index: _index sourceComponent: insideDelegate onLoaded: item.expired.connect(function () { item.height = 1 }) } } }
That's really messy, and any further logic I might add will have to dig through layers of child objects to examine the "inside" delegates. Let's go back a step, undo that nesting, and try watching for delegates as they are instantiated and added to the view:
MyListView.qml
import QtQuick 2.0 import QtQuick.Controls 2.12 ListView { ScrollBar.vertical: ScrollBar { } cacheBuffer: 0 Connections { target: contentItem onChildrenChanged: { var newItem = contentItem.children[contentItem.children.length - 1] newItem.expired.connect(function () { newItem.height = 1 }) } } }
main.qml
import QtQuick 2.12 import QtQuick.Window 2.12 Window { visible: true width: 600 height: 400 title: qsTr("ListView with Delegates That Signal") MyListView { anchors { fill: parent } model: 400 delegate: MyDelegate { serial: index height: 50 } } }
That almost works, but for some reason child #1 gripes about its
expired
signal failing to connect, and theonChildrenChanged
handler doesn't know the vertical row-index of the new child item (which could be relevant) because thechildren
array is ordered from oldest to newest — at least, not without peeking at the item'sserial
property, which is an artifact of this sampleMyDelegate
implementation, which is none ofMyListView
's business.So, this is all very frustrating. What I wish I could do is to go back to the simplest version of MyListView.qml and add
delegate.onExpired: height = 1
as a single line of code, but of course that's bad syntax.What do you suggest?
-
Any sample code on this concept ? This will help us to help you.
@dheerendra See my following post in this thread.