4

How to make a TableView or TreeView with a cell delegate chosen according to the value of another cell?

The idea is to make a property editor similar to this:

Screenshot

I tried various of the approaches listed here: https://doc.qt.io/qt-5/qml-qt-labs-qmlmodels-tablemodel.html

However DelegateChooser can only choose based on column or based on roleValue. None of those would work for the above usecase.

The model could be something like this:

model: TableModel {
    TableModelColumn { display: "name" }
    TableModelColumn { display: "value" }
    rows: [
        {
            name: "Name",
            type: "string",
            value: "Alfred"
        },
        {
            name: "Amount",
            type: "float",
            value: 3.75
        },
        {
            name: "Enabled",
            type: "bool",
            value: true
        },
        {
            name: "Count",
            type: "int",
            value: 2
        },
        {
            name: "Color",
            type: "color",
            value: "#3300ff"
        }
    ]
}

to show a 2-column table view, where the delegate in the second column is chosen according to the value of type.

Even selecting on the name role (which is a suboptimal solution, because there will be many properties of each type, and each DelegateChoice should match multiple names) does not work:

delegate: DelegateChooser {
    role: "name"
    DelegateChoice {
        roleValue: "Enabled"
        delegate: CheckBox {
            checked: model.display
            onToggled: model.display = checked
        }
    }
    DelegateChoice {
        roleValue: "Count"
        delegate: SpinBox {
            value: model.display
            onValueModified: model.display = value
        }
    }
    DelegateChoice {
        delegate: TextField {
            text: model.display
            selectByMouse: true
            implicitWidth: 140
            onAccepted: model.display = text
        }
    }
}
fferri
  • 18,285
  • 5
  • 46
  • 95
  • Did you ever make progress on this? I'm stuck on the same thing, and the workarounds feel very kludge-y (going back to the old Loader pattern). – Aaron Oct 16 '20 at 18:53
  • Nope, I didn't find any good solution, so I postponed this project for later. – fferri Oct 19 '20 at 08:08

1 Answers1

2

As it is said in TableModel documentation:

As model manipulation in Qt is done via row and column indices, and because object keys are unordered, each column must be specified via TableModelColumn. This allows mapping Qt's built-in roles to any property in each row object...

So, I've got working solution using built-in roles:

import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import Qt.labs.qmlmodels 1.0

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Properties table")

    TableView {
        anchors.fill: parent

        model: TableModel {
            TableModelColumn {
                display: "name"
                decoration: function() { return "";}
            }
            TableModelColumn {
                display: "value"
                decoration: "type"
            }
            rows: [
                {
                    name: "Name",
                    type: "string",
                    value: "Alfred"
                },
                {
                    name: "Enabled",
                    type: "bool",
                    value: true
                },
                {
                    name: "Count",
                    type: "int",
                    value: 2
                }
            ]
        }

        delegate: DelegateChooser {
            role: "decoration"
            DelegateChoice {
                roleValue: "string"
                delegate: TextField {
                    text: model.display
                    selectByMouse: true
                }
            }
            DelegateChoice {
                roleValue: "int"
                delegate: SpinBox {
                    value: model.display
                }
            }
            DelegateChoice {
                roleValue: "bool"
                delegate: CheckBox {
                    checked: model.display
                }
            }
            DelegateChoice {
                delegate: Rectangle {
                    color: "beige"
                    implicitWidth: textLabel.width + 10
                    implicitHeight: textLabel.height
                    Text {
                        id: textLabel
                        anchors.centerIn: parent
                        text: model.display
                    }
                }
            }
        }
    }
}

However, I think a better solution would be define a custom PropertiesTableModel inherited from QAbstractTableModel:

properties_table_model.hpp:

#pragma once

#include <QAbstractTableModel>

class PropertiesTableModel : public QAbstractTableModel
{    
    Q_OBJECT

public:
    enum PropertyType {
        String,
        Integer,
        Boolean
    };
    Q_ENUM(PropertyType)

    struct Property {
        QString name;
        QVariant value;
        PropertyType type;
    };

    enum CustomRoles {
        NameRole = Qt::UserRole + 1,
        ValueRole,
        TypeRole
    };

    PropertiesTableModel(QObject *parent = nullptr) {
        m_properties.append({"String prop", "StringProperty", PropertyType::String});
        m_properties.append({"Int prop", 55, PropertyType::Integer});
        m_properties.append({"Bool prop", true, PropertyType::Boolean});
    }

    int rowCount(const QModelIndex & = QModelIndex()) const override
    {
        return m_properties.size();
    }

    int columnCount(const QModelIndex & = QModelIndex()) const override
    {
        return 2;
    }

    QVariant data(const QModelIndex &index, int role) const override
    {
        auto& property = m_properties.at(index.row());
        switch (role) {
            case CustomRoles::NameRole:
                return property.name;
            case CustomRoles::TypeRole:
                if (index.column() > 0)
                    return property.type;
                else
                    return -1;
            case CustomRoles::ValueRole:
                return property.value;
            default:
                break;
        }

        return QVariant();
    }

    QHash<int, QByteArray> roleNames() const override
    {
        QHash<int, QByteArray> roles;
        roles[NameRole] = "name";
        roles[ValueRole] = "value";
        roles[TypeRole] = "type";
        return roles;
    }
private:
    QVector<Property> m_properties;
};

, and use it like this:

import QtQuick 2.14
import QtQuick.Window 2.14
import QtQuick.Controls 2.14
import Qt.labs.qmlmodels 1.0

import MyLib 1.0

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Properties table")

    TableView {
        anchors.fill: parent

        model: PropertiesModel {}

        delegate: DelegateChooser {
            role: "type"
            DelegateChoice {
                roleValue: PropertiesModel.String
                delegate: TextField {
                    text: model.value
                    selectByMouse: true
                }
            }
            DelegateChoice {
                roleValue: PropertiesModel.Integer
                delegate: SpinBox {
                    value: model.value
                }
            }
            DelegateChoice {
                roleValue: PropertiesModel.Boolean
                delegate: CheckBox {
                    checked: model.value
                }
            }
            DelegateChoice {
                delegate: Rectangle {
                    color: "beige"
                    implicitWidth: textLabel.width + 10
                    implicitHeight: textLabel.height
                    Text {
                        id: textLabel
                        anchors.centerIn: parent
                        text: model.name
                    }
                }
            }
        }
    }
}

PS. remember to register it with:

qmlRegisterType<PropertiesTableModel>("MyLib", 1, 0, "PropertiesModel");
Artem Zh.
  • 438
  • 5
  • 11
  • Thanks. How to connect the values to existing properties? We can set the 'value' of the field, but when the value is changed via the delegate widget, where do we get the *Changed signal? It seems we can't set it in the `delegate` item, as that acts as a Component and not as the actual instance of the item. – fferri May 11 '22 at 11:44
  • can implement a generic change handler in every delegate Item, then by looking at properties `model.row` / `model.column` can be dispatched to a function attached to the specific model field / row. – fferri May 11 '22 at 12:31
  • Isn't the fact the model's `setData` function is being called every time delegate changes the data. And also see the `dataChanged` signal. I think it should be enough. – LRDPRDX Jun 03 '23 at 03:50