Delegate supplied as property => how to handle instantiation?
-
I created a QML type that receives its delegate as a (default)
Componentproperty, so it can handle various kinds of models; the model is avariantproperty.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.onCompletedhandler 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
Itemand 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.onCompletedhandler to the object that instantiates the delegate, along these lines:Component.onCompleted: delegate.Component.completed.connect(function () { code here })
This actually runscode hereas 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
Menuwhose internalListViewcreatesMenuItemdelegates only as needed, so that the menu can be arbitrarily long without waiting for thousands of items to be created.ListViewknows how to do this (using itscacheBufferproperty), but unfortunately,Menuis written in such a way that it manages all child objects (ActionorMenuItem) and feeds a complete set of instantiated items to its embeddedListViewobject in the form of a fully built object model.]Here is a non-
Menucode example to illustrate my issue, which is, how can I connect signals from instantiated delegates to the view that instantiated them, when the delegateComponentis supplied to the view as an opaque property, not declared in the same .qml file as the view?First, here's the delegate, a
RowLayoutcontaining aTimerthat 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
ListViewto 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 theListViewcreates and destroys delegate objects as the rows enter and exit the view area. This code works perfectly, because the view's delegateComponentis declared inline with its ownonExpiredsignal handler. (I'm not concerned about the fact that, for some reason,ListViewnever 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
ListViewreusable, 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
onExpiredsignal handler) an integral part ofMyListView, and removing it from the mainline. Problem is,MyListView's delegate is an opaqueComponentproperty. How can I attach a signal handler to every instance that is created from it, properly encapsulated inside theMyListViewtype?If the property were a simple
Item, I could use aConnectionsobject to attach a handler to its signal, but here it's aComponentthat gets instantiated byListViewin logic that is outside my control, so I can't target that property with aConnectionsobject. One way to take control would be to wrap the delegate in aLoader. This gets theexpiredevent handler out of the mainline (but note, I had to tweak it slightly, to remove the explicitdelegateproperty 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
indexproperty. (If the model were aStringListor C++ model, it would also be missing itsmodelData,model, and other role properties thatMyListViewshouldn'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
expiredsignal failing to connect, and theonChildrenChangedhandler doesn't know the vertical row-index of the new child item (which could be relevant) because thechildrenarray is ordered from oldest to newest — at least, not without peeking at the item'sserialproperty, which is an artifact of this sampleMyDelegateimplementation, 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 = 1as 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.