3

I am trying to extend the QtQuick's ComboBox with icons, and I struggle to access the model's data for the currently selected item. I need to have a textRole, a valueRole and my new iconSourceRole (which defines a qrc: url to a .png file).

Using ComboBox's delegate I can access all the needed data from the model for each row by using model[comboBox.textRole] and model[comboBox.iconSourceRole], but this delegate is not used to render the currently selected item.

contentItem handles this and I cannot find any way to access all the roles from the model from inside there. comboBox.displayText is used for the display text.

My icon ComboBox will be used with different types of models, and they all seem to have a completely different api to get the data. For example Qml's ListModel has a .get() method, which only takes one parameter, Qml's FolderListModel has a .get() method, which needs 2 arguments. And I cannot find any way to access the data from a C++ QAbstractListModel class. Neither .roleNames() nor .itemData() were marked as Q_INVOKABLE, and if I want to use .data(), it seems like I need to know the numeric value of the role instead of the name.

This example code almost works for all cases but not with C++ models and the currently selected item. I am looking for a way to access model from inside contentItem:.

Example application showing comboBoxes with different types of models

Main.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
import QtQuick.Layouts
import QtQuick.Window
import Qt.labs.folderlistmodel 2.4
import comboboxtests 1.0

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")

    GridLayout {
        anchors.fill: parent

        columns: 2

        // IconComboBox shall support qml ListModels
        IconComboBox {
            Layout.preferredHeight: 64
            id: listModelComboBox
            textRole: 'theText'
            valueRole: 'theValue'
            iconSourceRole: 'theIconUrl'
            model: ListModel {
                ListElement { theText: 'text0'; theValue: 'value0'; theIconUrl: 'qrc:/comboboxtests/icons/movinghead.png' }
                ListElement { theText: 'text1'; theValue: 'value1'; theIconUrl: 'qrc:/comboboxtests/icons/movinghead.png' }
                ListElement { theText: 'text2'; theValue: 'value2'; theIconUrl: 'qrc:/comboboxtests/icons/nebelmaschine.png' }
                ListElement { theText: 'text3'; theValue: 'value3'; theIconUrl: 'qrc:/comboboxtests/icons/nebelmaschine.png' }
                ListElement { theText: 'text4'; theValue: 'value4'; theIconUrl: 'qrc:/comboboxtests/icons/rgbstrahler.png' }
                ListElement { theText: 'text5'; theValue: 'value5'; theIconUrl: 'qrc:/comboboxtests/icons/rgbstrahler.png' }
            }
        }
        Label {
            text: qsTr('currentValue: ') + listModelComboBox.currentValue
        }

        // IconComboBox shall support qml FolderListModels (to let the user select which icon to use)
        IconComboBox {
            Layout.preferredHeight: 64
            id: folderListModelComboBox
            textRole: "fileBaseName"
            valueRole: "fileBaseName"
            iconSourceRole: "fileUrl"
            model: FolderListModel {
                folder: "qrc:/comboboxtests/icons/"
                showDirs: false

                function getUrlForIcon(name) {
                    let myFolder = folder;
                    if (myFolder.length < 1 || myFolder.charAt(myFolder.length - 1) !== '/') {
                        myFolder = myFolder + '/';
                    }

                    return myFolder + name + ".png"
                }
            }
        }
        Label {
            text: qsTr('currentValue: ') + folderListModelComboBox.currentValue
        }

        // IconComboBox shall support C++ QAbstractListModels (access to our internal database)
        IconComboBox {
            Layout.preferredHeight: 64
            id: cppModelComboBox
            textRole: 'theText'
            valueRole: 'theValue'
            iconSourceRole: 'theIconUrl'
            model: CppDefinedModel {

            }
        }
        Label {
            text: qsTr('currentValue: ') + cppModelComboBox.currentValue
        }

        Item {
            Layout.fillHeight: true
        }
    }
}

IcomComboBox.qml

import QtQuick
import QtQuick.Controls
import QtQuick.Controls.Material
import Qt.labs.folderlistmodel 2.4
import comboboxtests 1.0

ComboBox {
    id: comboBox

    property string iconSourceRole

    delegate: ItemDelegate {
        height: 64
        anchors.left: parent.left
        anchors.right: parent.right
        contentItem: IconChooserDelegateLayout {
            anchors.top: parent.top
            anchors.bottom: parent.bottom
            text: model[comboBox.textRole]
            iconSource: model[comboBox.iconSourceRole]
        }
    }
    contentItem: IconChooserDelegateLayout {
        text: comboBox.displayText
        isInsideMaterialComboBox: true
        iconSource: {
//            console.log("QAbstractListModel", model instanceof QAbstractListModel);
//            console.log("QAbstractItemModel", model instanceof QAbstractItemModel);
//            console.log("FolderListModel", model instanceof FolderListModel);
//            console.log("DeviceTypesModel", model instanceof CppDefinedModel);
//            console.log("QtObject", model instanceof QtObject);

            if (comboBox.currentIndex < 0)
                return '';
            if (!comboBox.model)
                return '';
            if (!comboBox.iconSourceRole)
                return '';

            // FolderListModel has a different API
            if (model instanceof FolderListModel)
                return model.get(comboBox.currentIndex, iconSourceRole);
            // ListModel has another different API
            else if ('get' in model)
            {
                const data = model.get(comboBox.currentIndex);
                console.log(data);
                return data[iconSourceRole];
            }
            // and I dont know how to access C++ models from QML at all
            else if ('roleNames' in model || 'data' in model)
            {
                if (!('roleNames' in model && 'data' in model))
                    throw 'roleNames or data not defined!';

                const roleNames = model.roleNames();
                console.log('roleNames', roleNames);

                const index = model.index(comboBox.currentIndex, 0);
                const data = model.data(index, 99);
                console.log('data', data);

                throw 'getting data from model using roleNames and data is not yet implemented.';
            }
            else
                throw 'unknown model type: ' + typeof model;
        }
    }
}

cppdefinedmodel.h

#pragma once

#include <QAbstractListModel>
#include <qqml.h>

class CppDefinedModel : public QAbstractListModel
{
    Q_OBJECT
    QML_ELEMENT

    enum {
        TextRole = Qt::UserRole,
        ValueRole,
        IconUrlRole
    };

public:
    using QAbstractListModel::QAbstractListModel;

    int rowCount(const QModelIndex &parent) const override
    {
        return 6;
    }
    QVariant data(const QModelIndex &index, int role) const override
    {
        switch (role)
        {
        case TextRole:    return QString("name%0").arg(index.row());
        case ValueRole:   return QString("value%0").arg(index.row());
        case IconUrlRole: return QString("qrc:/comboboxtests/icons/%0.png")
                    .arg(std::array<const char *,3>{{"movinghead", "nebelmaschine", "rgbstrahler"}}[index.row() / 2 % 3]);
        }
        return {};
    }
    QHash<int, QByteArray> roleNames() const override
    {
        return {{TextRole, "theText"}, {ValueRole, "theValue"}, {IconUrlRole, "theIconUrl"}};
    }
};

I think qml needs a unified interface to access the data models. Is the logic to handle all the different model types implemented in ListView and ComboBox's C++ implementation?

I have commited this test project to GitHub

feedc0de
  • 3,646
  • 8
  • 30
  • 55

2 Answers2

2

I think qml needs a unified interface to access the data models.

It absolutely does - see https://bugreports.qt.io/browse/QTBUG-111176 for work that is currently being done on this.

Is the logic to handle all the different model types implemented in ListView and ComboBox's C++ implementation?

As mentioned in the link above, a complex binding is currently required to support the various model types that users expect to be able to use:

text: control.textRole ? (Array.isArray(control.model) ? modelData[control.textRole] : model[control.textRole]) : modelData

However, in this case there's the added problem that you're not inside a delegate. There's no convenient way of accessing data from the model in this situation:

https://bugreports.qt.io/browse/QTBUG-99115

Until then, you can achieve what you're after with a less complex iconSource binding than your current one:

    iconSource: comboBox.currentIndex === -1
        ? ""
        : Array.isArray(comboBox.model)
            ? comboBox.model[comboBox.currentIndex][comboBox.iconSourceRole]
            : (comboBox.model as ListModel)?.get(comboBox.currentIndex)[comboBox.iconSourceRole]
                ?? qrcPathOrUndefined((comboBox.model as FolderListModel)?.get(comboBox.currentIndex, "filePath"))
                ?? comboBox.model.data(comboBox.model.index(comboBox.currentIndex, 0), CppDefinedModel.IconUrlRole)

    function qrcPathOrUndefined(path) {
        if (path === undefined)
            return undefined

        return path.startsWith(":/") ? "qrc" + path : undefined
    }

A few things to note:

  • We use as-casts, optional chaining, and nullish coalescing to make the binding shorter.
  • We define the qrcPathOrUndefined function to allow the use of nullish coalescing and also to fix the path that FolderListModel gives us. I'm not sure if it was ever designed to work with qrc paths, because it should return a path prefixed with "qrc:" for use with QML, rather than the C++-oriented ":" that it currently returns.
  • This won't work with every model type. Specifically, I'm not sure how you'd make this work with models like integers in a generic way.
  • It does support arrays, but your delegate implementation would need to be modified to use the first binding mentioned (the one from QTBUG-111176).

This also requires giving your model's role enum a name, making it public, and exposing it to QML:

public:
    enum Roles {
        TextRole = Qt::UserRole,
        ValueRole,
        IconUrlRole
    };
    Q_ENUM(Roles);

FolderListModel seems to also require this binding in ComboBox to ensure that the first item is displayed at startup:

currentIndex: model.status === FolderListModel.Ready ? 0 : -1
Mitch
  • 23,716
  • 9
  • 83
  • 122
0

Since you're already using ItemDelegate why don't you just set icon.source?

In fact, I can make it work using your suggestion: model[comboBox.iconSourceRole]

Also, if your images are located relative to the qml source, you do not need a fully qualified qrc: reference to your image. It's sufficient to use relative references for your images. e.g. "icons/rgbstrahler.png" instead of "qrc:/comboboxtests/icons/rgbstrahler.png".

Here's a demonstration of just the ComboBox with the icon.source populated.

import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
Page {
    ColumnLayout {
        IconComboBox {
            id: listModelComboBox
            textRole: 'theText'
            valueRole: 'theValue'
            iconSourceRole: 'theIconUrl'
            model: ListModel {
                ListElement { theText: 'text0'; theValue: 'value0'; theIconUrl: 'icons/calc.svg' }
                ListElement { theText: 'text1'; theValue: 'value1'; theIconUrl: 'icons/smile.svg' }
                ListElement { theText: 'text2'; theValue: 'value2'; theIconUrl: 'icons/calc.svg' }
                ListElement { theText: 'text3'; theValue: 'value3'; theIconUrl: 'icons/smile.svg' }
                ListElement { theText: 'text4'; theValue: 'value4'; theIconUrl: 'icons/calc.svg' }
                ListElement { theText: 'text5'; theValue: 'value5'; theIconUrl: 'icons/smile.svg' }
            }
        }
        ItemDelegate {
            text: qsTr("currentValue: %1").arg(listModelComboBox.currentValue)
        }
        ItemDelegate {
            text: qsTr("currentText: %1").arg(listModelComboBox.currentText)
        }
        ItemDelegate {
            text: qsTr("currentIconSource: %1").arg(listModelComboBox.currentIconSource)
            icon.source: listModelComboBox.currentIconSource
        }
    }
}

// IconComboBox.qml
import QtQuick
import QtQuick.Controls
ComboBox {
    id: comboBox
    property string iconSourceRole
    property string currentIconSource: get(model, comboBox.currentIndex, comboBox.iconSourceRole)
    delegate: ItemDelegate {
        text: model[comboBox.textRole]
        icon.source: model[comboBox.iconSourceRole]
    }
    contentItem: ItemDelegate {
        text: currentText
        icon.source: currentIconSource
    }
    function get(obj, index, prop) {
        if (obj instanceof ListModel)
            return obj.get(index)[prop];
        if (obj instanceof FolderListModel)
            return obj.get(index, prop);
        return null;
    }
}

// icons/calc.svg
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32" class="svg-icon"><path d="M1 0v12h29.8V0H1zm27 9H4V3h24v6zM1 14v8h8v-8H1zm6 6H3v-4h4v4zm5-6v8h8v-8h-8zm6 6h-4v-4h4v4zm5-6v18h8V14h-8zm6 16h-4V16h4v14zM1 24v8h8v-8H1zm6 6H3v-4h4v4zm5-6v8h8v-8h-8zm6 6h-4v-4h4v4z"/></svg>

// icons/smile.svg

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><path d="M16 29.8A13.8 13.8 0 1 1 29.8 16 13.815 13.815 0 0 1 16 29.8zm0-26.6A12.8 12.8 0 1 0 28.8 16 12.815 12.815 0 0 0 16 3.2zm-4.5 10.6a1.2 1.2 0 0 0 .608-.168 1.52 1.52 0 0 0 .464-.43 1.927 1.927 0 0 0 .278-.572 2.234 2.234 0 0 0 0-1.26 1.927 1.927 0 0 0-.278-.571 1.52 1.52 0 0 0-.464-.431 1.185 1.185 0 0 0-1.216 0 1.52 1.52 0 0 0-.464.43 1.927 1.927 0 0 0-.277.572 2.234 2.234 0 0 0 0 1.26 1.927 1.927 0 0 0 .277.571 1.52 1.52 0 0 0 .464.431 1.2 1.2 0 0 0 .608.168zm9.608-.168a1.52 1.52 0 0 0 .464-.43 1.927 1.927 0 0 0 .278-.572 2.234 2.234 0 0 0 0-1.26 1.927 1.927 0 0 0-.278-.571 1.52 1.52 0 0 0-.464-.431 1.185 1.185 0 0 0-1.216 0 1.52 1.52 0 0 0-.464.43 1.927 1.927 0 0 0-.277.572 2.234 2.234 0 0 0 0 1.26 1.927 1.927 0 0 0 .277.571 1.52 1.52 0 0 0 .464.431 1.185 1.185 0 0 0 1.216 0zm3.223 5.743l-.926-.379a7.863 7.863 0 0 1-7.39 4.976.166.166 0 0 0-.032 0 7.863 7.863 0 0 1-7.388-4.976l-.926.379a8.846 8.846 0 0 0 8.313 5.597.21.21 0 0 0 .035 0 8.848 8.848 0 0 0 8.314-5.597z"/><path fill="none" d="M0 0h32v32H0z"/></svg>

You can Try it Online!

Stephen Quan
  • 21,481
  • 4
  • 88
  • 75
  • I tried your currentIconSource, and it works for ListModel, but does not for FolderListModel and my custom C++ one: `Error: Insufficient arguments` and `TypeError: Type error` are logged for the line of the property – feedc0de Feb 27 '23 at 11:35
  • @feedc0de you can solve this by setting `currentIconSource` to a normalizing `get(obj, index, prop)` function - I've started it to show you how it looks like. Also, you can add a Q_INVOKABLE get method to your `CppDefinedModel` so that you can call it through such a normalizing function. – Stephen Quan Feb 28 '23 at 08:02