Sturm
Sturm

Reputation: 4125

Qt nested model views

I am coming from C# and WPF, and there we have quite nice way of binding collections to views, populating data via current DataContext.

I have been learning Qt and I know that there is a way of providing a view with a model (QAbstractItemModel) and then by modifying the model you can update the view automatically, which is nice and what I want.

My current issue is that I want to create a editor view for the following class Definition

class Definition
{
public:
    vector<Step*> Steps;

    bool SomeBoolMember1;
    bool SomeBoolMember2;
    int SomeIntMember1;
    int SomeIntMember2;
}

Step is

class Step
{
public:
    vector<Requirement*> Requirements;

    bool SomeBoolMember1;
    bool SomeBoolMember2;
    int SomeIntMember1;
    int SomeIntMember2;
}

And Requirement is

class Requirement
{
public:
    RequirementType Type;
}

My aim is to build a view to be able to modify a collection of Definitions. A possible view could be something like this:

enter image description here

When a different Definition is selected the selected item view is updated (to load data of selected Definition), and then we have a scrollview with a list of steps. Each step with a list of requirements.

As you see it is not rocket science but I know it might be tedious. Any hints of how to structure models/view models to achieve the desired functionality?

Upvotes: 4

Views: 1736

Answers (1)

Jaa-c
Jaa-c

Reputation: 5137

Your question is quite broad. Qt's models are very general and there are many ways to structure them to achieve the same behavior. I've created a simplified example how you can structure them. I only used Definition and Step, adding Requirement to Step is analogical to adding Step to Definition...

struct Step
{
    bool SomeBoolMember1 {false};
    int SomeIntMember1 {0};
};

struct Definition
{
    std::vector<std::shared_ptr<Step>> Steps;

    bool SomeBoolMember1 {false};
    int SomeIntMember1 {0};
};

struct Data
{
    std::vector<std::shared_ptr<Definition>> definitions;
};

Models are quite straightforward, I've created one model for Definition and one for Step. Each model has columns that represent properties of these objects and rows of the models represent one instance of the object. Model for Definition has a column for Step, that returns model for Step for given Definition.

class StepModel : public QAbstractItemModel
{
    Q_OBJECT
    using BaseClass = QAbstractItemModel;

public:
    enum Columns
    {
        E_BOOL_1,
        E_INT_1,
        E_REQUIREMENT,
        _END
    };

    StepModel(Definition* definition, QObject* parent) :
        BaseClass(parent),
        definition(definition)
    {
    }

    virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
    {
        return createIndex(row, column, nullptr);
    }
    virtual QModelIndex parent(const QModelIndex &child) const override
    {
        return QModelIndex();
    }
    virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
        return definition->Steps.size();
    }
    virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override
    {
        return _END;
    }
    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
        if (role != Qt::DisplayRole) return QVariant();

        const auto& step = definition->Steps.at(index.row());
        switch (index.column())
        {
            case E_BOOL_1:
                return step->SomeBoolMember1;
            case E_INT_1:
                return step->SomeIntMember1;
            case E_REQUIREMENT:
                return QVariant();
        }
    }
    virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override
    {
        if (role != Qt::EditRole) return false;

        auto& step = definition->Steps.at(index.row());
        switch (index.column())
        {
            case E_BOOL_1:
                step->SomeBoolMember1 = value.toBool();
                return true;
            case E_INT_1:
                step->SomeIntMember1 = value.toInt();
                return true;
            case E_REQUIREMENT:
                assert(false);
                return false;
        }
    }
    virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override
    {
        assert(count == 1);
        beginInsertRows(parent, row, row);
        definition->Steps.push_back(std::make_shared<Step>());
        endInsertRows();
        return true;
    }
    virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override
    {
        assert(count == 1);
        beginRemoveRows(parent, row, row);
        definition->Steps.erase(definition->Steps.begin() + row);
        endRemoveRows();
        return true;
    }

private:
    Definition* definition;
};
Q_DECLARE_METATYPE(StepModel*)


class DefinitionModel : public QAbstractItemModel
{
    Q_OBJECT
    using BaseClass = QAbstractItemModel;

public:
    enum Columns
    {
        E_NAME,
        E_BOOL_1,
        E_INT_1,
        E_STEPS,
        _END
    };

    DefinitionModel(QObject* parent) :
        BaseClass(parent)
    {
    }

    virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override
    {
        return createIndex(row, column, nullptr);
    }
    virtual QModelIndex parent(const QModelIndex &child) const override
    {
        return QModelIndex();
    }
    virtual int rowCount(const QModelIndex &parent = QModelIndex()) const override
    {
        return definitionData.definitions.size();
    }
    virtual int columnCount(const QModelIndex &parent = QModelIndex()) const override
    {
        return _END;
    }
    virtual QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override
    {
        if (role != Qt::DisplayRole) return QVariant();

        const auto& definition = definitionData.definitions.at(index.row());
        switch (index.column())
        {
            case E_NAME:
                return QString("Definition %1").arg(index.row() + 1);
            case E_BOOL_1:
                return definition->SomeBoolMember1;
            case E_INT_1:
                return definition->SomeIntMember1;
            case E_STEPS:
                return QVariant::fromValue(new StepModel(definition.get(), nullptr));
        }
    }
    virtual bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override
    {
        if (role != Qt::EditRole) return false;

        auto& definition = definitionData.definitions.at(index.row());
        switch (index.column())
        {
            case E_BOOL_1:
                definition->SomeBoolMember1 = value.toBool();
                return true;
            case E_INT_1:
                definition->SomeIntMember1 = value.toInt();
                return true;
            default:
                assert(false);
                return false;
        }
    }
    virtual bool insertRows(int row, int count, const QModelIndex &parent = QModelIndex()) override
    {
        assert(count == 1);
        beginInsertRows(parent, row, row);
        definitionData.definitions.push_back(std::make_shared<Definition>());
        endInsertRows();
        return true;
    }
    virtual bool removeRows(int row, int count, const QModelIndex &parent = QModelIndex()) override
    {
        assert(count == 1);
        beginRemoveRows(parent, row, row);
        definitionData.definitions.pop_back();
        endRemoveRows();
        return true;
    }

private:
    Data definitionData;
};

In this example, DefinitionModel owns instance of Data, which is not something you would wanna do in real application, it's just for the sake of simplicity.

Dialog is a little bit more complicated as you have to dynamically create widgets for Steps as you create new instances. I've created a simple mapping for widgets, that are mapped to columns of it's appropriate model. Each time you select new Definition, you load the data back from the model by this mapping.

namespace Ui
{
class MainWindow;
}

class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0)  :
        QMainWindow(parent),
        ui(new Ui::MainWindow)
    {
        ui->setupUi(this);

        auto definitionModel = new DefinitionModel(this);
        ui->definitionView->setModel(definitionModel);

        connect(ui->definitionView->selectionModel(), &QItemSelectionModel::currentRowChanged, this, [=]
        {
            update();
        }, Qt::QueuedConnection);

        connect(ui->addDefinition, &QPushButton::pressed, this, [this]
        {
            auto* model = ui->definitionView->model();
            model->insertRow(model->rowCount());

            ui->definitionView->update();
            ui->definitionView->setCurrentIndex(model->index(model->rowCount() - 1, 0));
        });
        connect(ui->removeDefinition, &QPushButton::pressed, this, [this]
        {
            auto* model = ui->definitionView->model();
            if (model->rowCount() > 0)
            {
                const int row = model->rowCount() - 1;
                for (const auto& widget : steps[row])
                {
                    unmap(widget);
                }
                steps.erase(steps.find(row));
                model->removeRow(row);
            }
        });

        auto getCurrentDefinition = [this] { return ui->definitionView->currentIndex().row(); };
        map(ui->definitionInt, definitionModel, DefinitionModel::E_INT_1, getCurrentDefinition);
        map(ui->definitionBool, definitionModel, DefinitionModel::E_BOOL_1, getCurrentDefinition);

        connect(ui->addStep, &QPushButton::pressed, this, [=]
        {
            if (getCurrentDefinition() == -1) return;
            auto widget = new QWidget(this);
            auto layout = new QHBoxLayout(widget);
            auto checkBox = new QCheckBox(widget);
            layout->addWidget(checkBox);
            auto spinBox = new QSpinBox(widget);
            layout->addWidget(spinBox);
            auto removeButton = new QPushButton(widget);
            removeButton->setText("remove");
            layout->addWidget(removeButton);
            ui->rightLayout->addWidget(widget);

            const int currentDefinition = getCurrentDefinition();
            steps[currentDefinition].push_back(widget);

            auto model = definitionModel->data(definitionModel->index(currentDefinition, DefinitionModel::E_STEPS), Qt::DisplayRole).value<QAbstractItemModel*>();
            model->setParent(widget);
            const int rowCount = model->rowCount();
            model->insertRow(rowCount);

            auto getRow = [=]
            {
                auto it = std::find(steps[currentDefinition].begin(), steps[currentDefinition].end(), widget);
                return std::distance(steps[currentDefinition].begin(), it);
            };
            map(checkBox, model, StepModel::E_BOOL_1, getRow);
            map(spinBox, model, StepModel::E_INT_1, getRow);

            connect(ui->definitionView->selectionModel(), &QItemSelectionModel::currentRowChanged, widget, [=] (const QModelIndex& current)
            {
                widget->setVisible(current.row() == currentDefinition);
            });

            connect(removeButton, &QPushButton::pressed, widget, [=]
            {
                model->removeRow(rowCount);
                unmap(checkBox);
                unmap(spinBox);
                ui->rightLayout->removeWidget(widget);
                auto it = std::find(steps[getCurrentDefinition()].begin(), steps[getCurrentDefinition()].end(), widget);
                steps[currentDefinition].erase(it);
                delete widget;
            });

            update();
        });
    }

    ~MainWindow()
    {
        delete ui;
    }

private:

    void map(QCheckBox* checkBox, QAbstractItemModel* model, int column, std::function<int()> getRow)
    {
        connect(checkBox, &QCheckBox::toggled, this, [=] (bool value)
        {
            model->setData(model->index(getRow(), column), value, Qt::EditRole);
        });

        auto update = [=]
        {
            checkBox->setChecked(model->data(model->index(getRow(), column), Qt::DisplayRole).toBool());
        };

        mapping.emplace(checkBox, update);
    }

    void map(QSpinBox* spinBox, QAbstractItemModel* model, int column, std::function<int()> getRow)
    {
        connect(spinBox, static_cast<void (QSpinBox::*)(int)>(&QSpinBox::valueChanged), this, [=] (int value)
        {
            model->setData(model->index(getRow(), column), value, Qt::EditRole);
        });

        auto update = [=]
        {
            spinBox->setValue(model->data(model->index(getRow(), column), Qt::DisplayRole).toInt());
        };

        mapping.emplace(spinBox, update);
    }

    void unmap(QWidget* widget)
    {
        mapping.erase(mapping.find(widget));
    }

    void update() const
    {
        for (const auto& pair : mapping)
        {
            pair.second();
        }
    }

    Ui::MainWindow *ui;
    std::map<QWidget*, std::function<void()>> mapping;
    std::map<int, std::vector<QWidget*>> steps;
};

Ui file:

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>800</width>
    <height>600</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QHBoxLayout" name="verticalLayout" stretch="1,2">
    <item>
     <layout class="QVBoxLayout" name="verticalLayout_3">
      <property name="spacing">
       <number>0</number>
      </property>
      <property name="leftMargin">
       <number>10</number>
      </property>
      <item>
       <widget class="QListView" name="definitionView">
        <property name="showDropIndicator" stdset="0">
         <bool>false</bool>
        </property>
        <property name="selectionBehavior">
         <enum>QAbstractItemView::SelectRows</enum>
        </property>
       </widget>
      </item>
      <item>
       <layout class="QHBoxLayout" name="horizontalLayout">
        <property name="topMargin">
         <number>10</number>
        </property>
        <item>
         <widget class="QPushButton" name="addDefinition">
          <property name="text">
           <string>add</string>
          </property>
         </widget>
        </item>
        <item>
         <widget class="QPushButton" name="removeDefinition">
          <property name="text">
           <string>remove</string>
          </property>
         </widget>
        </item>
       </layout>
      </item>
     </layout>
    </item>
    <item>
     <layout class="QVBoxLayout" name="verticalLayout_2">
      <property name="rightMargin">
       <number>10</number>
      </property>
      <item>
       <layout class="QVBoxLayout" name="rightLayout">
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_2">
          <item>
           <widget class="QCheckBox" name="definitionBool">
            <property name="text">
             <string>CheckBox</string>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QSpinBox" name="definitionInt"/>
          </item>
         </layout>
        </item>
        <item>
         <layout class="QHBoxLayout" name="horizontalLayout_3">
          <property name="topMargin">
           <number>10</number>
          </property>
          <item>
           <widget class="QLabel" name="label">
            <property name="text">
             <string>Steps</string>
            </property>
           </widget>
          </item>
          <item>
           <widget class="QPushButton" name="addStep">
            <property name="text">
             <string>add</string>
            </property>
           </widget>
          </item>
         </layout>
        </item>
       </layout>
      </item>
      <item>
       <spacer name="verticalSpacer">
        <property name="orientation">
         <enum>Qt::Vertical</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>20</width>
          <height>40</height>
         </size>
        </property>
       </spacer>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

Upvotes: 6

Related Questions