Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

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 a variant 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 the Item. 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 runs code here as expected, but I don’t know how to access the instantiated object.

    Anyone able to help?


  • Qt Champions 2017

    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 internal ListView creates MenuItem 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 its cacheBuffer property), but unfortunately, Menu is written in such a way that it manages all child objects (Action or MenuItem) and feeds a complete set of instantiated items to its embedded ListView 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 delegate Component 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 a Timer 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 the ListView creates and destroys delegate objects as the rows enter and exit the view area. This code works perfectly, because the view's delegate Component is declared inline with its own onExpired 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 subclass ListView:

    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 of MyListView, and removing it from the mainline. Problem is, MyListView's delegate is an opaque Component property. How can I attach a signal handler to every instance that is created from it, properly encapsulated inside the MyListView type?

    If the property were a simple Item, I could use a Connections object to attach a handler to its signal, but here it's a Component that gets instantiated by ListView in logic that is outside my control, so I can't target that property with a Connections object. One way to take control would be to wrap the delegate in a Loader. This gets the expired event handler out of the mainline (but note, I had to tweak it slightly, to remove the explicit delegate 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 a StringList or C++ model, it would also be missing its modelData, model, and other role properties that MyListView 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 the onChildrenChanged handler doesn't know the vertical row-index of the new child item (which could be relevant) because the children array is ordered from oldest to newest — at least, not without peeking at the item's serial property, which is an artifact of this sample MyDelegate implementation, which is none of MyListView'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?



  • @dheerendra See my following post in this thread.


Log in to reply