如何从 QML 中的 GridView 或 ListView 获取实例化的委托组件

Posted

技术标签:

【中文标题】如何从 QML 中的 GridView 或 ListView 获取实例化的委托组件【英文标题】:How to get an instantiated delegate component from a GridView or ListView in QML 【发布时间】:2012-02-20 19:24:03 【问题描述】:

一般来说,我的难题是:通过 GridView 之外的一些操作,我想仅根据之前选择的特定模型项或索引来计算 GridView 中特定委托项的坐标。

我有一个 GridView,其中包含模型中的许多项目。 GridView 的委托创建每个项目的缩略图视图。单击时,它会显示该项目的详细全屏视图。我想要一个很好的过渡,显示缩略图从它在 GridView 中的位置展开,当详细视图被关闭时,缩回到 GridView 中。

诀窍在于,详细视图本身是 ListView 的委托,因此您可以一次在一个屏幕的详细视图之间进行分页。这意味着一个简单地调整 GridView 的委托项目大小的解决方案,否则将无法正常工作。此外,由于您可以分页到 ListView 中的任何项目,因此返回到 GridView 必须仅基于模型中可用的信息或模型项目的索引(例如,我无法存储用于启动的 MouseArea 的坐标详细视图之类的)。

展开动画相当简单,因为代理项有一个用于单击处理程序的 MouseArea,它知道自己的位置,因此可以将其传递给启动动画的函数。反过来我想不通:从ListView中的model item/index,如何找出GridView中相关item的坐标?

我在文档中找不到任何似乎可以让您从模型项实例甚至索引访问委托项实例的内容。 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.onCompletedComponent.onDestruction 保存所有创建的委托项目的列表。为了有用,它应该是模型项或索引 => 委托项的映射。麻烦的是,basic types docs(尤其是variant)似乎表明这种地图不可能在纯 QML 中创建。所以听起来这意味着将这个映射创建为一个 C++ 类,并在 QML 中与委托组件中的 onCompleted/onDestruction 一起使用它以使其保持最新。对于本应简单的事情,似乎有点危险和笨拙。

This mailing list post 似乎表明 Flickable 的 contentItem 属性可用于枚举委托项目。然后我发现this post 认为这是不好的做法。我仍在研究它,但我怀疑这将是一个合法的解决方案。看起来太老套了,无法可靠地工作。

到目前为止,这就是我所拥有的。

【问题讨论】:

【参考方案1】:

对于 QML 类型的 ListView,下面的简单函数可以获取特定索引处的委托实例:

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 实例时可能需要进一步增强。

【讨论】:

【参考方案2】:

经过短短几个月的 QML 编程,我想出了这个解决方案,类似于 OP 的解决方案,但无论如何我都会发布它,希望它可以帮助其他人解决这个问题。

此示例中基本上包含三个组件:GridView 用于显示可视组件(查看器),Item 包含 QObjects(数据)列表和 Component,其中包含有颜色的 @987654325 @(代表)。

查看器获取数据并显示它创建委托。按照这种模式,更改委托内容的最简单方法是访问其数据。要访问委托的数据,必须首先到达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)
            
        
    

【讨论】:

【参考方案3】:

以防万一有人对如何在 C++ 中实现这一点感兴趣,这里有一个快速的 sn-p:

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

请务必注意(以及发布此内容的主要原因),从QQuickItem 访问children() 将调用QObject::children() 方法,该方法不一定返回与QQuickItem::childItems() 相同的对象。 也许有人觉得这很有用,它肯定会在四天前帮助我...... :)

【讨论】:

【参考方案4】:

经过一番调查,事实证明contentItem 确实拥有 Flickable 的实例化代表。正如我上面所说,我怀疑这真的是做到这一点的最佳方式,甚至是一个好的方式时期,但它似乎确实有效。我将在下面发布这个 hacky 解决方案的完整代码,但我仍然希望有更好的方法。真正重要的是 GridView 中的新 getDelegateInstanceAt() 函数。

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

请告诉我还有更健壮的方法!

【讨论】:

只是想知道您是否遇到过更好的解决方案或更好的实现来从另一个 QML 文件访问模型的委托,或者您是否仍在使用上述方法? 不,据我所知,这是“官方”的做法。 似乎“黑魔法”试图克服的问题之一是子列表被 GridView 的兄弟姐妹污染。我认为您可以将 GridView 包装在一个 Rectangle 中,从而简化您的子列表。然后简单的 summaryView.contentItem.children[i] 似乎效果很好。

以上是关于如何从 QML 中的 GridView 或 ListView 获取实例化的委托组件的主要内容,如果未能解决你的问题,请参考以下文章

Qml:如何为所有 GridView 项目设置属性

PySide2/QML 填充和动画 Gridview 模型/委托

QML - 如何适应和对齐 GridView 的项目?

在 QML 中单击时如何在 GridView 中显示较大版本的选定图像

如何在 QML 中处理来自父 Flickable 的 gridview contentY 和 Y 位置

QML:在 GridView 中加载图像