1
votes

I have subclassed the QAbstractItemModel in order to create a very generic model, here are the files:

cvartablemodel.h

#ifndef CVARTABLEMODEL_H
#define CVARTABLEMODEL_H

#include <QObject>
#include <QAbstractTableModel>
#include <QList>
#include <QVariant>

/**
 * @brief   Provides a QAbstractTableModel override class that implements the
 *          API for MDE variables storing, reading and writing.
 */
class CVarTableModel : public QAbstractTableModel
{

public:

    /**
     * @brief   An enumeration class providing the columns and the amount of
     *          columns as well (use ZCOUNT as always last member).
     */
    enum class Columns
    {
        Name = 0,
        Unit,
        Value,

        ZCOUNT,
    };
    Q_ENUM(Columns)

    CVarTableModel(QObject* parent = nullptr);
    ~CVarTableModel() override;

    // Basic overrides
    int rowCount(const QModelIndex &parent = QModelIndex()) const override;
    int columnCount(const QModelIndex &parent = QModelIndex()) const override;
    QVariant data(const QModelIndex &index, int role) const override;

    // Since its a well behaved model...
    QVariant headerData(int section,
                        Qt::Orientation orientation,
                        int role) const override;

    // Its an editable model
    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index,
                 const QVariant &value,
                 int role = Qt::EditRole) override;

    // Only rows are modificable for now
    bool insertRows(int position,
                    int rows,
                    const QModelIndex &index = QModelIndex()) override;
    bool removeRows(int position,
                    int rows,
                    const QModelIndex &index = QModelIndex()) override;

private:

    /**
     * @brief   The local, intermediate storage object.
     */
    QList<QList<QVariant>> m_data;
};

#endif // CVARTABLEMODEL_H

cvartablemodel.cpp

#include <QDebug>

#include "cvartablemodel.h"

/**
 * @brief   The default constructor, nothing interesting in here.
 * @param   parent: the parent object.
 */
CVarTableModel::CVarTableModel(QObject* parent) : QAbstractTableModel(parent)
{

}

/**
 * @brief   Clear the storage and remove connections (if any) at exit
 */
CVarTableModel::~CVarTableModel()
{
    foreach (auto row, m_data)
        row.clear();

    m_data.clear();
}

/**
 * @brief   Returns the fixed (for now) amount of columns
 * @param   parent: unused
 * @return  The amount of available columns
 */
int CVarTableModel::columnCount(const QModelIndex& parent) const
{
    if (parent.isValid())
        return 0; // https://doc.qt.io/qt-5/qabstractitemmodel.html#columnCount

    return static_cast<int>(Columns::ZCOUNT);
}

/**
 * @brief   Row count is equal to the stored number of variables.
 * @param   parent: unused.
 * @return  The amount of stored variables entries.
 */
int CVarTableModel::rowCount(const QModelIndex& parent) const
{
    if (parent.isValid())
        return 0; // https://doc.qt.io/qt-5/qabstractitemmodel.html#rowCount

    return m_data.length();
}

/**
 * @brief   Reads the cell specified by the \ref index.
 * @param   index: Stores row/ col data.
 * @param   role: the display role.
 * @return  In case of valid \p index, a valid cell value. Otherwise empty
 *          QVariant object.
 */
QVariant CVarTableModel::data(const QModelIndex& index, int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();

    if (!index.isValid())
        return QVariant();

    // check the row
    if ((index.row() >= m_data.length()) || (index.row() < 0))
        return QVariant();

    // check the column
    if ((index.row() >= m_data[index.row()].length()) || (index.column() < 0))
        return QVariant();

    return m_data[index.row()][index.column()];
}

/**
 * @brief   Obtains the header (columns) names.
 * @param   section: column number.
 * @param   orientation: Accepts only horizontal.
 * @param   role: Accepts only display.
 * @return  The column header text in case all params are valid.
 *          Otherwise empty QVariant.
 */
QVariant CVarTableModel::headerData(int section,
                                    Qt::Orientation orientation,
                                    int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();

    if (orientation != Qt::Horizontal)
        return QVariant();

    if (section >= static_cast<int>(Columns::ZCOUNT))
        return QVariant();

    return QVariant::fromValue(static_cast<Columns>(section));
}

/**
 * @brief   Returns the \p index flags. Only values column is editable for now.
 * @param   index: model index item.
 * @return  flags enum val.
 */
Qt::ItemFlags CVarTableModel::flags(const QModelIndex& index) const
{
    Qt::ItemFlags flags = Qt::ItemIsEnabled;

    if (index.isValid())
    {
        if (static_cast<Columns>(index.column()) == Columns::Value)
            flags |= Qt::ItemIsEditable;
    }

    return flags;
}

/**
 * @brief   Cell data writing override.
 * @param   index: The model index with row/ col.
 * @param   value: Value to be set in the cell.
 * @param   role: Only EditRole accepted.
 * @return  Non zero on succesfull data editing.
 */
bool CVarTableModel::setData(const QModelIndex& index,
                             const QVariant& value,
                             int role)
{
    if (!index.isValid() || (role != Qt::EditRole))
        return false;

    // check the row
    if ((index.row() >= m_data.length()) || (index.row() < 0))
        return false;

    // check the column
    if ((index.row() >= m_data[index.row()].length()) || (index.column() < 0))
        return false;

    m_data[index.row()][index.column()] = value;
    emit dataChanged(index, index, {role});

    return true;
}

/**
 * @brief   Inserts the \p rows amount of rows. They will start at \p position.
 * @param   position: position at which the insertion starts (the new 1st item
 *          will have this index in the end).
 * @param   rows: amount of rows to insert.
 * @param   index: unused.
 * @return  Non zero in case of succesfull row insertion.
 */
bool CVarTableModel::insertRows(int position,
                                int rows,
                                const QModelIndex& index)
{
    Q_UNUSED(index);

    if ((position >= rowCount(QModelIndex())) || (position < 0))
        return false;

    beginInsertRows(QModelIndex(), position, position + rows - 1);
    for (int row = 0; row < rows; row++)
    {
        QList<QVariant> emptyRow;
        for (int i = 0; i < columnCount(QModelIndex()); i++)
            emptyRow.append(QVariant());

        m_data.insert(position, emptyRow);
    }
    endInsertRows();

    return true;
}

/**
 * @brief   Removes \p rows amount of rows starting at \p position
 * @param   position: removing starts at this position.
 * @param   rows: the amount of rows that will be removed.
 * @param   index: unused.
 * @return  Non zero on succesfull rows removal.
 */
bool CVarTableModel::removeRows(int position,
                                int rows,
                                const QModelIndex& index)
{
    Q_UNUSED(index);

    if ((position >= rowCount(QModelIndex())) || (position < 0))
        return false;

    if (rows > rowCount(QModelIndex()))
        return false;

    beginRemoveRows(QModelIndex(), position, position + rows - 1);
    for (int row = 0; row < rows; row++)
        m_data.removeAt(position);

    endRemoveRows();
    return true;
}

I need to connect an instance of this object with the QML TableView component and I am really not sure how.

I have created the instance and a getter for it:

    /**
     * @brief   The API + intermediate storage model for the bad nodes
     */
    CVarTableModel m_varTabModel;

/**
 * @brief   A pointer getter for the whole variable table model.
 * @return  pointer to the model.
 */
QObject* CVessel::varTabModel()
{
    return static_cast<QObject*>(&m_varTabModel);
}

So initially this would need to be a 3 column table with no rows (some rows could be added in the constructor for testing purposes).

How can a TableView component on the QML side utilize this now? I would appreciate some QML examples of a TableView allowing to enter and edit some values.

1

1 Answers

1
votes

A QML TableView uses the roles instead of column number. If you check the column in the model passed to the method data(), you will see that it's always at 0.

So, you have to convert the role given in the TableView and the column number in your model.

You could use a proxy model to handle the role/column conversion. You'll not have to change your current model:

The QIdentityProxyModel class is a good to base for that:

class QMLProxy: public QIdentityProxyModel
{
    Q_OBJECT
public:

    QMLProxy(QObject* parent=nullptr): QIdentityProxyModel(parent)
    {}

    enum Role
    {
        NameRole = Qt::UserRole + 1,
        UnitRole
    };
    QHash<int, QByteArray> roleNames() const override {
        QHash<int, QByteArray> roles;
        roles[NameRole] = "COL1";
        roles[UnitRole] = "COL2";
        return roles;
    }

    Q_INVOKABLE QVariant data(const QModelIndex &index, int role) const override
      {
        QModelIndex newIndex = mapIndex(index, role);
        if (role == NameRole || role == UnitRole)
            role = Qt::DisplayRole;
        return QIdentityProxyModel::data(newIndex, role);
      }

    Q_INVOKABLE void edit(int row,
                             const QVariant &value,
                             QString const& role)
    {
        if (role == QString(roleNames().value(NameRole)))
            setData(createIndex(row, 0), value, Qt::EditRole);
        else if (role == QString(roleNames().value(UnitRole)))
            setData(createIndex(row, 1), value, Qt::EditRole);
    }

private:
    QModelIndex mapIndex(QModelIndex const& source, int role) const {
        switch(role)
        {
        case NameRole:
            return createIndex(source.row(), 0);
        case UnitRole:
            return createIndex(source.row(), 1);
        }
        return source;
    }
};

I overrided the data() to convert the role to the column number. And I created a method edit because when it will be called from the QML, the signature will be different than the method setData.

To pass the model to the QML from the main:

CVarTableModel* model = new CVarTableModel();

QMLProxy* proxy = new QMLProxy();
proxy->setSourceModel(model);

QQuickView *view = new QQuickView;
view->rootContext()->setContextProperty("myModel", proxy);
view->setSource(QUrl("qrc:/main.qml"));
view->show();

Then, in the QML, you need a delegate to make your table editable (I used a TextInput. But, you can use another component):

TableView {
    TableViewColumn {
        role: "COL1"
        title: "Col 1"
        width: 100
    }
    TableViewColumn {
        role: "COL2"
        title: "Col 2"
        width: 200
    }
    model: myModel
    itemDelegate: Component {
        TextInput {
          id:textinput
          text: styleData.value
          onAccepted: {
                  myModel.edit(styleData.row, text, styleData.role)
          }
          MouseArea {
            anchors.fill: parent
            onClicked: textinput.forceActiveFocus()
          }
      }
    }
}