qtqmlqt-quickqtquickcontrols

how to make QtQuick TableView / TreeView with heterogeneous delegate chosen based on field value


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
        }
    }
}

Solution

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