Search code examples
c++qtlistviewqmlqtquick2

Accessing Qt Quick ListView model from the nested elements in a custom delegate


I'm having difficulty finding the idiomatic way to access ListView's model from its delegate in QML.

Consider the following fairly simple custom delegate that supports a checkbox near the item and tracks the currently chosen item in the list. (To clarify: the checkbox selection and list selection are independent.)

Rectangle {
    id: wrapper

    required property int index
    required property string name
    required property bool selected

    width: ListView.view.width
    height: 32
    radius: 3
    color: "transparent"

    Label {
        anchors.left: parent.left
        anchors.right: selectedBox.left
        anchors.verticalCenter: parent.verticalCenter
        anchors.margins: 5
        text: wrapper.name

        MouseArea {
            anchors.fill: parent
            onClicked: wrapper.ListView.view.currentIndex = wrapper.index
        }
    }

    CheckBox {
        id: selectedBox

        anchors.right: parent.right
        anchors.verticalCenter: parent.verticalCenter
        checked: wrapper.selected

        onToggled: wrapper.ListView.view.model.setProperty(wrapper.index, "selected", checked)
    }
}

This is working code though making onToggle work was particularly troublesome. In the end I found this magical ListView.view.model rune to access the underlying model in the Qt docs and figured out I can prepend the delegate ID to it.

It seems to be too verbose to be correct for the task delegates were created for though.

The majority of examples online hardcode this value to a particular model which violates Delegate and Model concept separation and I don't want to go this route. I also saw just model being used (the last listing in the linked docs), but it doesn't work for me even if I require property ListModel model for it in the delegate.

So the question is whether there is a simpler way.

Here's a sample list and model to test the delegate:

ListView {
    // ...
    model: myModel
    delegate: myDelegate
    highlight: Rectangle {
        color: "lightsteelblue"
        radius: 5
    }
    focus: true
}

ListModel {
    id: myModel

    ListElement {
        name: "Item 1"
        selected: false
    }

    ListElement {
        name: "Item 2"
        selected: false
    }
}

EDIT. There are actually two good solutions, a short one and a more verbose but foolproof.

I could drop all required property declarations and use

    Label {
        // ...
        text: name
        // ...
    }

    CheckBox {
        // ...
        checked: selected
        onToggled: selected = checked
    }

I couldn't come to this solution because my initial design had the model property named checked and then it's kind of unclear how this approach can be applied then.

In case of name collisions it's possible to make a required model property and then it's bound to the corresponding row of the list model:

Rectangle {
    id: wrapper

    required property var model
    // ...

    Label {
        // ...
        text: model.name
        // ...
    }

    CheckBox {
        // ..
        checked: model.selected
        onToggled: model.selected = checked
    }
}

The second options seems to be preferable, so I'll go with it.


Solution

  • There are several ways to reach the model from the delegate. It depends mainly on the type of model that the ListView is using.

    Commonly, I prefer to explicitly mark the model property as required using required property var model, especially if you define other properties as required. Otherwise, you can omit it.

    For example, in your case you can define a delegate in this way:

    Component {
        id: myDelegate
    
        CheckDelegate { // Need QtQuick.Controls
            required property var model
    
            width: ListView.view.width
            height: 32
    
            text: model.name
    
            checked = model.selected
            onCheckedChanged: model.selected = checked
    
            Timer { // Emulating an external entity that update the model
                interval: 4000
                onTriggered: parent.model.selected = !parent.model.selected
                running: true
            }
        }
    }
    

    In this case, I initialize and bind the checked property with model.selected. When the signal onCheckedChanged is emitted, I update the model too.

    If an external entity (the timer in the previous example) changes the model value, also the checked state will be updated.

    The bad part of this approach is the recurrence of the bindings:

    1. When the user clicks on CheckDelegate, onCheckedChanged signal is emitted
    2. The signal calls the function that updates the model.selected property
    3. The model emits a signal too that the selected property changed
    4. The CheckDelegate.checked evaluates the model.selected property due to the binding, but in this case, they're equal and no other signals will be emitted

    Point 4 is unnecessary but occurs due to a CheckBox/CehckDelegate implementation.

    The best approach should be: users click on the delegate, a clicked signal is emitted and the checked state is not updated. The signal updates the model value that, thanks to the bind, updates the checked property too.

    To obtain that, you have to implement your custom control