Как получить экземпляр делегированного компонента из GridView или ListView в QML - программирование
Подтвердить что ты не робот

Как получить экземпляр делегированного компонента из GridView или ListView в QML

Моя головоломка, как правило, заключается в следующем: через какое-то действие вне GridView я хочу определить координаты конкретного элемента делегата в GridView, основываясь только на определенном элементе модели или индексе, выбранном ранее.

У меня есть GridView с рядом элементов в модели. Делегат GridView создает эскиз каждого элемента. При нажатии на нее открывается подробный полноэкранный вид элемента. Мне нужен приятный переход, который показывает, что миниатюра расширяется со своего места в GridView, и когда подробное представление отклоняется, сжимается обратно в GridView на месте.

Фокус в том, что подробный вид сам по себе является делегатом ListView, поэтому вы можете размещать страницы между подробными просмотрами по одному экрану за раз. Это означает, что решение, которое просто изменяет размер элемента делегирования GridView или что-то не будет работать. Кроме того, поскольку вы можете ссылаться на любой элемент в ListView, возврат к GridView должен выполняться только на основе информации, доступной в модели или индексе элемента модели (например, я не могу хранить координаты MouseArea, используемые для запуска подробный обзор или что-то еще).

Анимация расширения довольно проста, так как элемент делегата имеет MouseArea для обработчика кликов, который знает свое собственное размещение, поэтому его можно передать функции, запускающей анимацию. Это наоборот, я не могу понять: из элемента модели/индекса в ListView, как я могу определить координаты связанного элемента в GridView?

Я не могу найти что-либо в документах, которые, похоже, позволят вам получить доступ к экземпляру элемента делегата из экземпляра элемента модели или даже индекса. GridView имеет indexAt(), который возвращает индекс, основанный на координатах. Я думаю, что я мог бы покончить с реверсом, но, похоже, он не существует.

Вот более конкретный пример. Извинения за длину; это самый короткий пример кода, который я мог бы придумать, чтобы точно описать мою проблему:

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";
                }
            }
        }
    }
}

Любые идеи? Возможно, я просто ошибаюсь. Если то, что я пытаюсь сделать конкретно, невозможно, есть ли другой способ, которым я должен это делать? Спасибо!


Изменить: некоторые идеи, которые у меня были (ничто из того, что я считаю возможным):

  • Сохраните список всех созданных элементов делегата, используя Component.onCompleted и Component.onDestruction. Чтобы быть полезным, это должна быть карта элемента модели или индекса = > делегировать элемент. Проблема заключается в том, что основные типы документов (особенно variant) показывают, что этот вид карты невозможно создать в чистом QML. Таким образом, похоже, что это будет означать создание этой карты как класса С++ и использование этого в QML с onCompleted/onDestruction в компоненте делегата, чтобы поддерживать его в актуальном состоянии. Кажется немного опасным и тяжелым для чего-то, что должно быть просто.

  • Этот список рассылки, кажется, указывает, что свойство Flickable contentItem может использоваться для перечисления элементов делегата. Затем я нашел этот пост, назвав это неправильной практикой. Я все еще смотрю в нее, но я скептически отношусь к этому, это будет законное решение. Кажется, слишком тяжело работать надежно.

Это все, что у меня есть.

4b9b3361

Ответ 1

После некоторого расследования выяснится, что contentItem содержит экземпляр делегатов для Flickable. Как я уже сказал выше, я скептически отношусь к тому, что это действительно лучший способ сделать это или даже хороший период пути, но он, похоже, работает. Я выложу полный код для этого взломанного решения ниже, но я все еще надеюсь на лучший способ. Очень важным битом является новая функция getDelegateInstanceAt() в 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 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";
                }
            }
        }
    }
}

Скажите, пожалуйста, более надежный способ!

Ответ 2

На всякий случай кто-то заинтересован в том, как это сделать в С++, вот быстрый фрагмент:

//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();
}

Важно отметить (и основную причину отправки этого сообщения), что доступ к children() из QQuickItem вызовет метод QObject::children(), который не обязательно возвращает те же объекты, что и QQuickItem::childItems(). Может быть, кто-то найдет это полезным, это наверняка поможет мне четыре дня назад...:)

Ответ 3

После всего лишь нескольких месяцев программирования QML я придумал это решение, похожее на одно из OP, но я все равно отправлю его в надежде, что это поможет другим людям разобраться в этом.

В этом примере есть три компонента: a GridView для отображения визуальных компонентов (viewer), Item, который содержит список QObjects (данных) и Component, которые заключают в себя цветные Rectangle (делегат).

Средство просмотра принимает данные и показывает, что они создают делегаты. Следуя этой схеме, самым простым способом изменить содержимое делегата является доступ к его данным. Чтобы получить доступ к данным делегата, сначала нужно обратиться к объекту listmodel, к которому в этом примере можно получить доступ через grid.children[1] (не уверен, почему он не находится в grid.children[0], но это всего лишь деталь), и наконец, получить доступ к правому делегату через grid.children[1].list_model[index]. Его можно удобно упаковать внутри функции getChild().

Последняя рекомендация - дать значимый objectName почти все с самого начала разработки. Эта практика облегчает отладку и даже, если она зла, позволяет получить доступ к данным из 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)
            }
        }
    }
}

Ответ 4

Для типа ListView класса QML следующая простая функция может получить экземпляр делегата с определенным индексом:

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

Ниже приведен пример тестирования QML, который использует указанную выше функцию с добавлением кода проверки ошибок и регистрации на основе 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" }
    }
}

Несмотря на то, что вышеупомянутая функция хорошо работает с этими простыми объектами "fruitModel" и "fruitView", ее, возможно, необходимо будет усовершенствовать при работе с более сложными экземплярами ListModel и ListView.