Qt嵌套模型视图

4
我来自C#和WPF,那里我们有一种很好的方式将集合绑定到视图中,通过当前DataContext填充数据。
我一直在学习Qt,我知道可以通过提供模型(QAbstractItemModel)来为视图提供数据,然后通过修改模型可以自动更新视图,这很不错,也是我想要的。
我的当前问题是我想为以下类 Definition 创建编辑器视图。
class Definition
{
public:
    vector<Step*> Steps;

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

Step

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

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

需求

class Requirement
{
public:
    RequirementType Type;
}

我希望创建一个视图,可以修改一组“Definition”(定义)。可能的视图可能是这样的:

enter image description here

当选择不同的定义时,所选项目视图将更新(以加载所选定义的数据),然后我们有一个带有步骤列表的滚动视图。每个步骤都有一系列要求。
正如您所看到的,这并不是什么高深的技术,但我知道它可能会很繁琐。有没有关于如何构建模型/视图模型以实现所需功能的提示?

只是一句话:在每个步骤中应该显示“删除步骤”按钮,“删除需求”按钮则应显示在每个需求旁边。 - scopchanov
1
是的,它可能会这样做,也可能会删除最后一步(或所选步骤/要求)。 - Sturm
我并不在意它是否能完成工作,但我认为这种情况是最有用的。你有什么想法? - Sturm
让我准备一个例子,向您展示我所想的。 - scopchanov
Qt model-view 是一个非常通用的框架(但在我看来并不是非常容易理解)。其中一个重要的点是,你不能直接将自己的数据类映射到视图中,在 Qt-ModelView 中永远不会这样做:“我想显示一系列步骤实例”。所有的东西都通过“项目角色”进行处理,因此你只能显示/编辑基本值的列表/表格/树形结构(当然,该值是基于你的步骤实例构建的,但最终只是一个字符串或整数等等)。 - ymoreau
显示剩余2条评论
1个回答

6

你的问题比较宽泛。Qt的模型非常通用,可以通过多种结构实现相同的行为。我创建了一个简化的示例来展示如何构建这些模型。我只使用了DefinitionStep,将Requirement添加到Step类似于将Step添加到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;
};

模型非常简单,我已经创建了一个Definition模型和一个Step模型。每个模型都有代表这些对象属性的列,而模型的行表示对象的一个实例。 Definition模型有一个Step列,该列返回给定DefinitionStep模型。

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

在这个例子中,DefinitionModel 拥有 Data 实例,但在实际应用中,你不会这样做,这只是为了简化而已。
对话框稍微复杂一些,因为你需要动态地为 Steps 创建小部件,当你创建新的实例时。我已经为小部件创建了一个简单的映射,它们被映射到其适当模型的列。每次选择新的定义时,你都可以通过这个映射从模型中加载数据。
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文件:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接