0

Although I already figured it out, it took quite a while. If you know a less convoluted solution - please share.

I'd like to provide a custom View that works with any type of model (with arbitrarily named fields):

  • model: 3
  • model: [ "red", "yellow", "green" ]
  • model: [ {color: "red"}, {color: "yellow"}, {color: "green"} ]
  • model: ListModel{ ListElement{color: "red"}; ListElement{color: "green"} }
  • subclassed QAbstractItemModel and such...

To do that I decided to provide a property Component delegate that user would populate with a visual Item that would be inserted deep into my View.

However, the View also needed to access some information from that same delegate because otherwise it would either promote unneeded code duplication, or unneeded complexity of proxy model.

MyView {
     mymodel: ["red", "blue"]
     mydelegate: Text {
         text: "color=" + modelData.name
         must_also_somehow_set_color_of_MyView_element_to: modelData.color
     }
}

...where MyView should be something like:

Column {
    Repeater {
        model: mymodel
        delegate: Rectangle {
            color: somehow_get_color_from_mydelegate
            Text {} // <--- instantiate mydelegate here with data from mymodel
        }
    }
}

Doing so seems easy but a naive attempts didn't work. The solution posted as an answer did for me.

Jack White
  • 896
  • 5
  • 7

2 Answers2

0

The trick is that model which is visible to Repeater or Instantiator's delegate contains everything that is visible to said delegate including index and modelData.

Said model can be used as a parameter to ListModel.append() to create a ListModel with a single element.

It can then be assigned as a model to a Repeater which will load the delegate with a copy of all context properties that QML normally derives from the element of View's model including index, modelData and model itself.

Upon instantiation, Repeater/Instantiator fires onItemAdded/onObjectAdded signal, which can be used as a cue to obtain a reference to the instance of View's delegate

User of the View can be asked to provide a root property in the delegate that would store obtained/calculated value which can be used by a View

Alternatively, the View can contain another Repeater or Instantiator and request another delegate to obtain values it itself needs. Or a container type can encapsulate both View's data and visual Item.

Example:

MyView.qml

import QtQuick 2.0
import QtQml.Models 2.1
Column {
    property var mymodel
    property Component mydelegate

    Repeater {
        model: mymodel
        delegate: Rectangle {
            width: childrenRect.width; height: childrenRect.height

            //color: model.color //works if "mymodel" has role "color"
            property var myitem: null //this will hold instance of mydelegate
            color: ( null !== myitem
                     && undefined !== myitem.bgcolor ) 
                     ? myitem.bgcolor : "transparent" //reads property of mydelegate

            // <-- mydelegate will be injected here
            Repeater {
                delegate: mydelegate
                model: ListModel{
                    Component.onCompleted: append(model)
                }
                onItemAdded: myitem = item
                onItemRemoved: myitem = null
            }
        }
    }
}

UsageExamples.qml

import QtQuick 2.0
import QtQuick.Window 2.0
Window{
    visible: true
    Row {
        spacing: 10

        MyView {
            mymodel: ListModel {
                ListElement{ name: "roses"; color: "red";   }
                ListElement{ name: "violets"; color: "blue";   }
            }
             mydelegate: Text {
                 text: name
                 property color bgcolor: model.color
             }
        }

        MyView {
             mymodel: [{name: "roses", color: "red"}, {name: "violets", color: "blue"}]
             mydelegate: Text {
                 text: modelData.name
                 property color bgcolor: modelData.color
             }
        }

        MyView {
             mymodel: ["red", "blue"]
             mydelegate: Text {
                 text: "color="+modelData
                 property color bgcolor: modelData
             }
        }

    }
}
Jack White
  • 896
  • 5
  • 7
  • I don't understand your usage of the second Repeater. Maybe a Loader is what you're looking for? – JarMan Jan 24 '22 at 15:05
  • The usage of second `Repeater` is actually crucial here, as is a second `ListModel` and its function `append()` - it allows `mydelegate` to be exposed to the element of `mymodel` while not actually being instantiated by that `model` itself, i.e. being inside another element. It took me quite a while to figure out how to do it. In other words, it works exactly the same as `Repeater {model:mymodel; delegate:mydelegate}`, except the instantiated `delegate` is wrapped in another `Item` (here - `Rectangle`). And despite trying, I failed to find a way to reparent the element without causing errors. – Jack White Jan 25 '22 at 21:19
0

I think you're really looking for a Loader to instantiate the delegate, rather than a second Repeater. You can also simplify a bit by using aliases. Try something like this:

Column {
    property alias mymodel: repeater.model
    property Component mydelegate

    Repeater {
        id: repeater
        delegate: Rectangle {
            width: childrenRect.width; height: childrenRect.height

            color: ( null !== loader.item
                     && undefined !== loader.item.bgcolor ) 
                     ? loader.item.bgcolor : "transparent"

            Loader {
                id: loader
                sourceComponent: myDelegate
            }
        }
    }
}
JarMan
  • 7,589
  • 1
  • 10
  • 25
  • 1
    Thanks for the answer. `loader` cannot be reached by the `alias` inside of the root element because `repeater`'s `delegate` is really a `Component` which is loaded with `Loader` "hidden" within the `Repeater`. That makes it impossible to refer to `id` within the `Repeater`'s `delegate` - it's not really an object until `Loader` loads the `Component`, and even then the `id`s are visible only inside the `delegate`. That is the reason why I didn't use alias here. The other property is not aliased because in real code it is actually used in two different `Repeafer`s so it does not make much sense. – Jack White Jan 25 '22 at 20:58
  • Sorry if it's not clear enough, but the main hurdle for me here is to somehow expose the `model` to the `component` in the same way as `Repeater`/`Instantiator` does (or perhaps find another QML-only way to do that?). I.e. pass data from the element of the `model` to the instance of `delegate`. Otherwise what's the point of providing a `delegate` to a `View` if that `View` only makes several copies of the exact same thing? In your example I cannot see how passing `model` data can be accomplished. Am I missing something? – Jack White Jan 25 '22 at 21:03
  • You're absolutely right about the aliasing the Loader. Sorry for not testing it before I posted. I'll update that part before thinking through your other comment. – JarMan Jan 25 '22 at 23:40
  • Don't worry about it. Anyway, I reread the docs on `Loader`, specifically `Using a Loader within a View Delegate`. It has an example almost exactly the same as you wrote, specifically stating that this will **not** work. However, it also has solutions for that issue. First one is not for me, and the last one works but is inferior to my `Repeater`-`ListModel`-`append(model)`. But the second one states that moving to another file can somehow help? How and why could it possibly solve anything - isn't it still a different context? If you suggested `Loader` here maybe you know anything about it? – Jack White Jan 26 '22 at 05:04
  • Please add `property var model2: model` or something similar to the `loader`, otherwise this still makes no sense as a view. Your suggestion does have an advantage - it's faster especially with very large models. But only `model` itself is exposed, so not 100% compatibility with a simple `Repeater`, but this is acceptable. – Jack White Jan 28 '22 at 22:16