chazomaticus
chazomaticus

Reputation: 15766

How to get an instantiated delegate component from a GridView or ListView in QML

My conundrum, phrased generally, is: through some action outside the GridView, I want to figure out the coordinates of a particular delegate item in the GridView based only on a particular model item or index selected before.

I've got a GridView with a number of items in the model. The GridView's delegate creates a thumbnail view of each item. When clicked, it brings up a detailed, fullscreen view of the item. I'd like a nice transition that shows the thumbnail expanding out from its place in the GridView, and when the detailed view is dismissed, shrinking back down into the GridView in place.

The trick is, the detailed view is itself a delegate of a ListView so you can page between detailed views one screen at a time. This means a solution that simply resizes the GridView's delegate item or something won't work. Also, since you can page to any item in the ListView, returning to the GridView has to be done based only on information available in the model or the model item's index (e.g. I can't store the coords of the MouseArea used to launch the detailed view or something).

The expansion animation is fairly easy, as the delegate item has a MouseArea for the click handler that knows its own placement, so that can be passed out to the function that starts the animation. It's the reverse I can't figure out: from the model item/index in the ListView, how do I figure out the coordinates of the related item in the GridView?

I can't find anything in the docs that would seem to let you have access to a delegate item instance from a model item instance or even an index. The GridView has indexAt() that returns the index based on coordinates. I think I could make do with the reverse, but it doesn't appear to exist.

Here's a more concrete example. Apologies for the length; this is the shortest example code I could come up with that accurately describes my problem:

import QtQuick 1.1

Item {
    id: window
    width: 400
    height: 200

    state: "summary"
    states: [
        State { name: "summary"; },
        State { name: "details"; }
    ]
    transitions: [
        Transition { from: "summary"; to: "details";
            SequentialAnimation {
                PropertyAction { target: animationRect; property: "visible"; value: true; }
                ParallelAnimation {
                    NumberAnimation { target: animationRect; properties: "x,y"; to: 0; duration: 200; }
                    NumberAnimation { target: animationRect; property: "width"; to: 400; duration: 200; }
                    NumberAnimation { target: animationRect; property: "height"; to: 200; duration: 200; }
                }
                PropertyAction { target: detailsView; property: "visible"; value: true; }
                PropertyAction { target: summaryView; property: "visible"; value: false; }
                PropertyAction { target: animationRect; property: "visible"; value: false; }
            }
        },
        Transition { from: "details"; to: "summary";
            SequentialAnimation {
                PropertyAction { target: summaryView; property: "visible"; value: true; }

                // How to animate animationRect back down to the correct item?

                PropertyAction { target: detailsView; property: "visible"; value: false; }
            }
        }
    ]

    Rectangle {
        id: animationRect
        z: 1
        color: "gray"
        visible: false

        function positionOverSummary(summaryRect) {
            x = summaryRect.x; y = summaryRect.y;
            width = summaryRect.width; height = summaryRect.height;
        }
    }

    ListModel {
        id: data
        ListElement { summary: "Item 1"; description: "Lorem ipsum..."; }
        ListElement { summary: "Item 2"; description: "Blah blah..."; }
        ListElement { summary: "Item 3"; description: "Hurf burf..."; }
    }

    GridView {
        id: summaryView
        anchors.fill: parent
        cellWidth: 100
        cellHeight: 100

        model: data
        delegate: Rectangle {
            color: "lightgray"
            width: 95; height: 95;

            Text { text: summary; }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    var delegateRect = mapToItem(window, x, y);
                    delegateRect.width = width; delegateRect.height = height;

                    animationRect.positionOverSummary(delegateRect);
                    detailsView.positionViewAtIndex(index, ListView.Beginning);
                    window.state = "details";
                }
            }
        }
    }

    ListView {
        id: detailsView
        anchors.fill: parent
        visible: false
        orientation: ListView.Horizontal
        snapMode: ListView.SnapOneItem

        model: data
        delegate: Rectangle {
            color: "gray"
            width: 400; height: 200;

            Column {
                Text { text: summary; }
                Text { text: description; }
            }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    // How do I get the coordinates to where animationRect should return?

                    summaryView.positionViewAtIndex(index, GridView.Visible);
                    window.state = "summary";
                }
            }
        }
    }
}

Any ideas? It's possible I'm just going about this the wrong way. If what I'm trying to do specifically is impossible, is there some other way I should be architecting this? Thanks!


Edit: Some ideas I've had (none of which I believe are feasible):

That's all I've got so far.

Upvotes: 18

Views: 50386

Answers (4)

jonathanzh
jonathanzh

Reputation: 1454

For QML type ListView, the following simple function can get the delegate instance at a specific index:

function getDelegateInstanceAt(index) {
    return contentItem.children[index];
}

Following is a QML test example that makes use of the above function, with added error checking and logging code, based on Qt 5.5:

import QtQuick 2.0
import QtQuick.Controls 1.2

Rectangle {
    width: 400
    height: 200

    ListView { id: fruitView
        width: parent.width
        height: parent.height / 2
        anchors.left: parent.left
        anchors.top: parent.top

        model: fruitModel

        delegate: TextInput {
            text: fruit_name
        }

        // Function to get the delegate instance at a specific index:
        // =========================================================
        function getDelegateInstanceAt(index) {
            console.log("D/getDelegateInstanceAt[" + index + "]");

            var len = contentItem.children.length;
            console.log("V/getDelegateInstanceAt: len[" + len + "]");

            if(len > 0 && index > -1 && index < len) {
                return contentItem.children[index];
            } else {
                console.log("E/getDelegateInstanceAt: index[" + index + "] is invalid w.r.t len[" + len + "]");
                return undefined;
            }
        }
    }

    Rectangle {
        width: parent.width
        height: parent.height / 2
        anchors.left: parent.left
        anchors.bottom: parent.bottom

        Button {
            anchors.centerIn: parent
            text: "getDelegateInstanceAt(1)"

            onClicked: {
                // Code to test function getDelegateInstanceAt():
                var index = 1;
                var myDelegateItem1 = fruitView.getDelegateInstanceAt(index);
                if(myDelegateItem1) {
                    console.log("I/onClicked: found item at index[" + index + "] fruit_name[" + myDelegateItem1.text + "]"); // Should see: fruit_name[Banana_1]
                } else {
                    console.log("E/onClicked: item at index[" + index + "] is not found.");
                }
            }
        }
    }

    ListModel { id: fruitModel
        ListElement { fruit_name: "Apple_0" }
        ListElement { fruit_name: "Banana_1" }
        ListElement { fruit_name: "Cherry_2" }
    }
}

Although the above function works well with these simple "fruitModel" and "fruitView" objects, it may need to be further enhanced when dealing with more complex ListModel and ListView instances.

Upvotes: 1

Avio
Avio

Reputation: 2708

After just a few months of QML programming I've come up with this solution, similar to the one of the OP, but I'll post it anyway in the hope that it could help other people sorting this thing out.

There are basically three components in this example: a GridView to show visual components (viewer), an Item that contains a list of QObjects (data) and a Component that enclose a colored Rectangle (delegate).

The viewer takes the data and shows it creating delegates. Following this pattern, the easiest way to change the content of a delegate, is to access its data. To access the data of a delegate, one must first reach the listmodel object that, in this example, can be accessed through grid.children[1] (not sure why it's not at grid.children[0], but this is just a detail), and finally access the right delegate through grid.children[1].list_model[index]. This can be conveniently packaged inside a getChild() function.

The final recommendation is to give a meaningful objectName to almost everything since the beginning of development. This practice ease the debugging a lot and, even if it's evil, allows to access data from C++.

GridView
{
    id:                 grid
    objectName:         "grid"

    cellWidth:          50
    cellHeight:         50

    anchors.left:       parent.left
    anchors.top:        parent.top
    anchors.margins:    100

    width:              100
    height:             100

    model:              listmodel.list_model

    delegate:           delegate
    focus:              false
    interactive:        false

    function getChild(index)
    {
        var listmodel = grid.children[1].list_model
        var elem = listmodel[index]

        return elem
    }

    Component.onCompleted:
    {
        for (var idx = 0; idx < grid.children.length; idx++)
        {
            console.log("grid.children[" + idx + "].objectName: " + grid.children[idx].objectName)
        }

        var elem = getChild(2)
        elem.model_text += " mod"
        elem.model_color = "slateblue"
    }

    Item
    {
        id: listmodel
        objectName: "listmodel"

        // http://www.w3.org/TR/SVG/types.html#ColorKeywords

        property list<QtObject> list_model:
        [
            QtObject
            {
                objectName:                             "rectmodel" + model_idx
                property int        model_idx:          1
                property string     model_text:         "R" + model_idx
                property color      model_color:        "crimson"
                property bool       model_visible:      true
            },
            QtObject
            {
                objectName:                             "rectmodel" + model_idx
                property int        model_idx:          2
                property string     model_text:         "R" + model_idx
                property color      model_color:        "lawngreen"
                property bool       model_visible:      true
            },
            QtObject
            {
                objectName:                             "rectmodel" + model_idx
                property int        model_idx:          3
                property string     model_text:         "R" + model_idx
                property color      model_color:        "steelblue"
                property bool       model_visible:      true
            },
            QtObject
            {
                objectName:                             "rectmodel" + model_idx
                property int        model_idx:          4
                property string     model_text:         "R" + model_idx
                property color      model_color:        "gold"
                property bool       model_visible:      true
            }
        ]
    }

    Component
    {
        id:         delegate

        Rectangle
        {
            id:                     delegaterect
            objectName:             "delegaterect"

            width:                  grid.cellWidth
            height:                 grid.cellHeight

            color:                  model_color
            visible:                model_visible

            Component.onCompleted:
            {
                console.log("delegaterect.children[0].objectName: " + delegaterect.children[0].objectName + " - " + delegaterect.children[0].text)
            }

            Text
            {
                id:                 delegatetext
                objectName:         "delegatetext"
                anchors.centerIn:   parent
                text:               qsTr(model_text)
            }
        }
    }
}

Upvotes: 0

tadelv
tadelv

Reputation: 114

Just in case anyone is interested into how to accomplish this in C++, here is a quick snippet:

//get the reference to GridView somehow (QObject *obj) (omitted for brevity)
QQuickItem *x = qobject_cast<QQuickItem *>(obj);
if (x->property("contentItem").isValid()) {
    QQuickItem *o = qvariant_cast<QQuickItem *>(x->property("contentItem"));
    qDebug() << "Extracting content item " << o->metaObject()->className() << " from " << x->metaObject()->className();
    qDebug() << "item has ch count " << o->childItems().count();
}

It is important to note (and the main reason for posting this), that accessing children() from QQuickItem will invoke the QObject::children() method, which does not necessarily return same objects as QQuickItem::childItems(). Maybe someone finds this useful, it would surely help me four days ago... :)

Upvotes: 2

chazomaticus
chazomaticus

Reputation: 15766

After some investigation, it turns out that contentItem does hold the instantiated delegates for a Flickable. As I said above, I'm skeptical that this is really the best way to do this, or even a good way period, but it does seem to work. I'll post the full code for this hacky solution below, but I'm still hoping there's a better way. The really important bit is the new getDelegateInstanceAt() function in the GridView.

import QtQuick 1.1

Item {
    id: window
    width: 400
    height: 200

    state: "summary"
    states: [
        State { name: "summary"; },
        State { name: "details"; }
    ]
    transitions: [
        Transition { from: "summary"; to: "details";
            SequentialAnimation {
                PropertyAction { target: animationRect; property: "visible"; value: true; }
                ParallelAnimation {
                    NumberAnimation { target: animationRect; properties: "x,y"; to: 0; duration: 200; }
                    NumberAnimation { target: animationRect; property: "width"; to: 400; duration: 200; }
                    NumberAnimation { target: animationRect; property: "height"; to: 200; duration: 200; }
                }
                PropertyAction { target: detailsView; property: "visible"; value: true; }
                PropertyAction { target: summaryView; property: "visible"; value: false; }
                PropertyAction { target: animationRect; property: "visible"; value: false; }
            }
        },
        Transition { from: "details"; to: "summary";
            id: shrinkTransition
            property variant destRect: {"x": 0, "y": 0, "width": 0, "height": 0}

            SequentialAnimation {
                PropertyAction { target: summaryView; property: "visible"; value: true; }
                PropertyAction { target: animationRect; property: "visible"; value: true; }
                PropertyAction { target: detailsView; property: "visible"; value: false; }
                ParallelAnimation {
                    NumberAnimation { target: animationRect; property: "x"; to: shrinkTransition.destRect.x; duration: 200; }
                    NumberAnimation { target: animationRect; property: "y"; to: shrinkTransition.destRect.y; duration: 200; }
                    NumberAnimation { target: animationRect; property: "width"; to: shrinkTransition.destRect.width; duration: 200; }
                    NumberAnimation { target: animationRect; property: "height"; to: shrinkTransition.destRect.height; duration: 200; }
                }
                PropertyAction { target: animationRect; property: "visible"; value: false; }
            }
        }
    ]

    Rectangle {
        id: animationRect
        z: 1
        color: "gray"
        visible: false

        function positionOverSummary(summaryRect) {
            x = summaryRect.x; y = summaryRect.y;
            width = summaryRect.width; height = summaryRect.height;
        }

        function prepareForShrinkingTo(summaryRect) {
            x = 0; y = 0;
            width = 400; height = 200;
            shrinkTransition.destRect = summaryRect;
        }
    }

    ListModel {
        id: data
        ListElement { summary: "Item 1"; description: "Lorem ipsum..."; }
        ListElement { summary: "Item 2"; description: "Blah blah..."; }
        ListElement { summary: "Item 3"; description: "Hurf burf..."; }
    }

    GridView {
        id: summaryView
        anchors.fill: parent
        cellWidth: 100
        cellHeight: 100

        model: data
        delegate: Rectangle {
            // These are needed for getDelegateInstanceAt() below.
            objectName: "summaryDelegate"
            property int index: model.index

            color: "lightgray"
            width: 95; height: 95;

            Text { text: summary; }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    var delegateRect = mapToItem(window, x, y);
                    delegateRect.width = width; delegateRect.height = height;

                    animationRect.positionOverSummary(delegateRect);
                    detailsView.positionViewAtIndex(index, ListView.Beginning);
                    window.state = "details";
                }
            }
        }

        // Uses black magic to hunt for the delegate instance with the given
        // index.  Returns undefined if there's no currently instantiated
        // delegate with that index.
        function getDelegateInstanceAt(index) {
            for(var i = 0; i < contentItem.children.length; ++i) {
                var item = contentItem.children[i];
                // We have to check for the specific objectName we gave our
                // delegates above, since we also get some items that are not
                // our delegates here.
                if (item.objectName == "summaryDelegate" && item.index == index)
                    return item;
            }
            return undefined;
        }
    }

    ListView {
        id: detailsView
        anchors.fill: parent
        visible: false
        orientation: ListView.Horizontal
        snapMode: ListView.SnapOneItem

        model: data
        delegate: Rectangle {
            color: "gray"
            width: 400; height: 200;

            Column {
                Text { text: summary; }
                Text { text: description; }
            }
            MouseArea {
                anchors.fill: parent
                onClicked: {
                    summaryView.positionViewAtIndex(index, GridView.Visible);

                    var delegateInstance = summaryView.getDelegateInstanceAt(index);
                    var delegateRect = window.mapFromItem(summaryView,
                        delegateInstance.x - summaryView.contentX,
                        delegateInstance.y - summaryView.contentY
                    );
                    delegateRect.width = delegateInstance.width;
                    delegateRect.height = delegateInstance.height;

                    animationRect.prepareForShrinkingTo(delegateRect);
                    window.state = "summary";
                }
            }
        }
    }
}

Please tell me there's a more robust way!

Upvotes: 11

Related Questions