Help us understand the problem. What is going on with this article?

QtのMVC系アーキテクチャ モデル/ビューアーキテクチャ 初めの1歩

More than 1 year has passed since last update.

1. 初めに

QtにもMVC系アーキテクチャとして、モデル/ビューアーキテクチャ(以下、Model/Viewアーキテクチャ)が実装されています。
本記事では、「データを更新したら表示が更新される」という単純な動作を、Model/Viewアーキテクチャを使用して実装するとどんなコードになるのか、という観点で、できるだけシンプルな形で説明します。


<-- 2019/09/28 追記 start -->
Githubにサンプルコードを登録しました。
<-- 2019/09/28 追記 end -->

1.1. 公式サイトの記述

Model/Viewアーキテクチャに関するQt公式サイトのページは、
Model/View Programming
Model/View Tutorial
になります。
(大体いつも公式サイトの記述だと説明が不足(or 不適切)→わからないところを自分で調べる→調べたことを発表しよう、という流れになるので、)Model/Viewアーキテクチャを理解するに辺り、上記サイトを読んで混乱したことについて、話をします。
上記サイトは、Modelの基本クラスであるQAbstractItemModelとViewの基本クラスであるQAbstractItemViewの両方を使用した説明が全くありません(ちなみにQt SDKのサンプルにも両方のクラスを同時使用したサンプルも1つもありません)。どちらかのサブクラスであるコンビニエンスクラスを使用した説明はたくさんあるのですが、基本編を飛ばして、いきなり応用編の話をしているような印象を受けます。概念を理解する為のノイズが多いのです。
私が最初にイメージしていた「あるint型のデータをQLabelに表示する、という単純な動作をModel/Viewアーキテクチャで実装するとどうなるのだろう?」という素朴な疑問を解決するのに、かなりの時間を費やしました。
(基本クラスっぽい名前なのに、基本じゃないQStandardItemModelクラスにも惑わされました。。。)
そこで、QAbstractItemModelとQAbstractItemViewの両方を使用した例が必要だろうと思ったのが、本稿作成のきっかけです。

2. MVC系アーキテクチャってそもそも何?

MVC系アーキテクチャは、PDS(PresentationDomainSeparation)をする実現する為にあるアーキテクチャです。
概念を説明し始めるとそれだけでページが終わってしまうし、MVC系アーキテクチャって、人によって言っていること違うし、、、
ということで、簡単な説明に留めます。
「画面に関係する部分(Presentation)と関係しない部分(Domain)を分離(Separation)すると、プログラムをメンテナンスしやすくなったりして、いい事あるよ」
ということです。(雑)
例えば、通信プログラム(通信クラス)を書いている人は、普通、データの受信内容がどのように画面に表示されるかは気にしません。
気にしなくてはならないようなプログラムは、画面に関係する部分(Presentation)と関係しない部分(Domain)が分離できておらず、デスマーチに〇名様ご招待!ということになります。

では、通信プログラムで受信したデータを画面に表示しなければならなくなった時、コールバックとかObserverパターンとかムツカシイことを考えなくても両者を簡単に連携できる方法はないかな、という発想から生まれたものがMVC系アーキテクチャだと思います。
Qtでは、画面に関係する部分(Presentation)をView、関係しない部分(Domain)をModelと呼びます。

3. QtのModel/Viewアーキテクチャ

Qtには、以下の図のような形でアーキテクチャで実装されています。
Model/Viewアーキテクチャの図
Model/View Programmingの図より

要素として、「Model」、「Data」、「View」、「Delegate」があります。
「Data」は、Modelが管理しているデータです。Model自身がデータを保持してもしなくてもかまいませんが、保持しない場合は、ModelからDataに対するアクセス手段をプログラマーが設計しなければなりません。
注目したいのは、Model→Viewという部分は一方向だということです。
Modelが管理するデータをViewに反映する場合は、Delegateは必要ありません。
逆にView、つまり画面で操作した内容をModelに反映したい場合は、Delegateが必要になります。
今回は、単純な構造なので、Delegateは使用しないで説明します。

4. QtのModel/Viewアーキテクチャのクラス群

公式サイトで調べられるModel/Viewアーキテクチャ関連クラスのクラス図です。
class.png
青い部分がModel/Viewアーキテクチャの基本となるクラスです。
前述しましたが、Model側がQAbstractItemModelクラスで、View側がQAbstractItemViewクラスです。

4.1. 蛇足:WidgetやViewの命名規則

Qtには、〇〇Widget、〇〇Viewの命名規則ってルールがあるのでしょうか?(〇〇Windowも・・・)
上記のクラス図を見るとQWidgetの下の方にQAbstractItemViewがあり、さらに下の方にQTableWidgetがある。
このクラス図を見る限りでは、ルールがあるように見えません。(Qtの中の人を小一時間ほど問い詰めたい)

5. Model/Viewアーキテクチャ実装における登場人物

Model/Viewアーキテクチャにおける登場人物として、Model/Viewアーキテクチャを使用する側と提供する側に(論理的には)分けて考えたほうがよいです。
ModelViewアーキテクチャ登場人物.png
この中で[1]と[2]については、同じクラスにまとめてもいいし、継承関係(is-a関係)やhas-a関係にしてもよいと思います。
[4]と[2]はシグナルとスロットのような関係(実際、中の処理でシグナル・スロットを使用している)であり、1:Nの関係です。
[3]は通信プログラムなどデータを持っているクラスです。

6. Model/Viewアーキテクチャで扱うデータの管理方法

ModelとView間で渡すデータはQVariantクラスのオブジェクトに変換する必要があります。
また、QVariantは論理的に2次元配列のような、Excelのセルのようなデータ構造で管理します。
ModelIndex
公式サイトより抜粋
このrow(行)やcolumn(列)をまとめて管理するクラスがQModelIndexクラスです。

6.1. QModelIndexクラス

QModelIndexクラスはModel/Viewアーキテクチャ用に用意されたクラスです。
row(行)やcolumn(列)は0以上が有効なIndexです。
QModelIndexクラスが有効なIndexと判断する(QModelIndex::isValid()関数がtrueを返す)のは、row(行)やcolumn(列)は0以上でかつ、Modelと関連付けられていることです。
QModelIndexクラスのデフォルトコンストラクタは、row(行)、column(列)共に-1が設定されています。

7. ModelをViewにバインドする

シグナルをスロットとconnect関数で接続するように、ModelをViewと接続することを一般的には「バインドする」と言いますが、Qtでは「バインドする」という言い方はしないようです。しかし、やっていることは同じです。
QAbstractItemView::setModel()を使用してバインドします。
バインドするまでの大まかなシーケンスを記述します。
base_use.png

8. 具体例を見る

githubに登録すると、よけい投稿が遅れるので、本記事にソースを張り付けていきます。

8.1. Modelの実装

QAbstractItemModelを継承したMyModelクラスを例にして、説明します。

mymodel.h
#ifndef MYMODEL_H
#define MYMODEL_H

#include <QAbstractItemModel>
#include <QObject>

class SampleData;

class MyModel : public QAbstractItemModel
{
    Q_OBJECT
public:
    explicit MyModel(QObject *parent = nullptr);

    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &child) const override;
    int rowCount(const QModelIndex &parent) const override;
    int columnCount(const QModelIndex &parent) const override;
    QVariant data(const QModelIndex &index, int role) const override;
    bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;

    void setTestData(int data);
    void setSampleData(const SampleData& data);
private:
    QMap<QModelIndex, QVariant> m_data;
public:
};
#endif // MYMODEL_H
mymodel.cpp
#include "mymodel.h"
#include "sampledata.h"
#include <QVariant>

MyModel::MyModel(QObject *parent) : QAbstractItemModel(parent)
{

}


QModelIndex MyModel::index(int row, int column, const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return createIndex(row, column);
}

QModelIndex MyModel::parent(const QModelIndex &child) const
{
    Q_UNUSED(child);
    return QModelIndex();
}

int MyModel::rowCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return 1;
}

int MyModel::columnCount(const QModelIndex &parent) const
{
    Q_UNUSED(parent);
    return 2;
}

QVariant MyModel::data(const QModelIndex &index, int role) const
{
    Q_UNUSED(role);
    if (index.isValid()) {
        return m_data[index];
    }
    return QVariant();
}


bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    Q_UNUSED(role);
    if (index.isValid()) {
        m_data[index] = value;
        emit dataChanged(index, index);
        return true;
    }
    return false;
}

void MyModel::setTestData(int data)
{
    setData(index(0, 0), QVariant(data));
}

void MyModel::setSampleData(const SampleData &data)
{
    setData(index(0, 1), QVariant::fromValue(data));
}
sampledata.h
#ifndef SAMPLEDATA_H
#define SAMPLEDATA_H

#include <QMetaType>
#include <QString>

class SampleData
{
public:
    //デフォルトコンストラクタ.
    SampleData();
    //コピーコンストラクタ.
    SampleData(const SampleData &obj);
    QString data();
    void setData(const QString &data);
private:
    QString m_data;
};
Q_DECLARE_METATYPE(SampleData)
#endif // SAMPLEDATA_H
sampledata.cpp
#include "sampledata.h"

SampleData::SampleData()
{

}

SampleData::SampleData(const SampleData &obj)
{
    m_data = obj.m_data;
}

QString SampleData::data()
{
    return m_data;
}

void SampleData::setData(const QString &data)
{
    m_data = data;
}

8.1.1. データ構造と扱い方

今回のサンプルでは、データ構造として、
(row,colomn)=(0,0):int型のデータ
(row,colomn)=(0,1):SampleData型のデータ
としています。
これらを今回は、

    QMap<QModelIndex, QVariant> m_data;

という形で、MyModelクラス内に実装しています。
これは、

QVariant m_data[1][2];

という形で実装してもいいですし、別クラス(データベースを管理するクラスなど)にデータ管理を移譲しても構いません。
論理的にIndexの整合性が合えば問題ないと思います。

8.1.2. QAbstractItemModelの純粋仮想関数

QAbstractItemModelの純粋仮想関数として、

    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;
    QModelIndex parent(const QModelIndex &child) const override;
    int rowCount(const QModelIndex &parent) const override;
    int columnCount(const QModelIndex &parent) const override;
    QVariant data(const QModelIndex &index, int role) const override;

が実装されます。
    QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override;

はcreateIndex()関数でIndexを作成して返します。parentはこのサンプルでは使用しません。(以後、同じ)
従って、parent()関数も未使用です。
rowCount()関数とcolumnCount()関数は、自分が設計したセル構造のrow、columnのMAX数を返します。
data()関数は、View(またはDelegate)からデータの取得要求があった時に呼ばれる関数です。

8.1.3. QAbstractItemModelからの追加実装関数

bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;

MyModelを使用する側からデータを設定する為の関数です。

bool MyModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
    Q_UNUSED(role);
    if (index.isValid()) {
        m_data[index] = value;
        emit dataChanged(index, index);
        return true;
    }
    return false;
}

この関数の実装が重要です。
m_dataにデータを格納後、シグナルdataChangedをemitします。
このシグナルにより、View側がデータが更新されたことに気付きます。
isValid()関数で有効なデータかどうかを判定していますが、必ずisValid()関数を使用しなければならないわけではありません。
もし、データ構造がきれいな〇×〇の形のIndexにならない場合は、isValid()関数を使用しないで判定するロジックを記述します。

8.1.4. QVariantにオリジナルデータクラスを設定するには

本サンプルでは、SampleDataクラスというオリジナルデータクラスを用意しました。
オリジナルデータクラスをQVariant化するには、
オリジナルデータクラスのヘッダファイルに

Q_DECLARE_METATYPE(SampleData)

のように宣言する必要があります。
これにより、QVariant::fromValue()関数やQVariant::data()関数が使用できるようになります。
この辺りの仕様は、ここを参照してください。

8.2. Viewの実装

今回のサンプルでは、QAbstractItemViewを継承したMyViewクラスと、
さらに、MyViewクラスを継承し、具体的な表示内容(ラベル)を持つTestViewクラスを実装しました。

myview.h
#ifndef MYVIEW_H
#define MYVIEW_H

#include <QAbstractItemView>

class MyView : public QAbstractItemView
{
    Q_OBJECT
public:
    explicit MyView(QWidget *parent = nullptr);

    QRect visualRect(const QModelIndex &index) const override;
    void scrollTo(const QModelIndex &index, ScrollHint hint) override;
    QModelIndex indexAt(const QPoint &point) const override;

    virtual void updateView(const QModelIndex &index, const QVariant& data) = 0;

protected slots:
    void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) override;

protected:
    QModelIndex moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers) override;
    int horizontalOffset() const override;
    int verticalOffset() const override;
    bool isIndexHidden(const QModelIndex &index) const override;
    void setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command) override;
    QRegion visualRegionForSelection(const QItemSelection &selection) const override;
};

#endif // MYVIEW_H
myview.cpp
#include "myview.h"
#include <QLabel>
#include <QDebug>

MyView::MyView(QWidget *parent) : QAbstractItemView(parent)
{
}


QRect MyView::visualRect(const QModelIndex &index) const
{
    Q_UNUSED(index);
    return QRect(0, 0, 300, 200);
}

void MyView::scrollTo(const QModelIndex &index, ScrollHint hint)
{
    Q_UNUSED(index);
    Q_UNUSED(hint);
}

QModelIndex MyView::indexAt(const QPoint &point) const
{
    Q_UNUSED(point);
    return QModelIndex();
}

QModelIndex MyView::moveCursor(CursorAction cursorAction, Qt::KeyboardModifiers modifiers)
{
    Q_UNUSED(cursorAction);
    Q_UNUSED(modifiers);
    return QModelIndex();
}

int MyView::horizontalOffset() const
{
    return 0;
}

int MyView::verticalOffset() const
{
    return 0;
}

bool MyView::isIndexHidden(const QModelIndex &index) const
{
    Q_UNUSED(index)
    return false;
}

void MyView::setSelection(const QRect &rect, QItemSelectionModel::SelectionFlags command)
{
    Q_UNUSED(rect);
    Q_UNUSED(command);
}

QRegion MyView::visualRegionForSelection(const QItemSelection &selection) const
{
    Q_UNUSED(selection);
    return QRegion();
}


void MyView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
    Q_UNUSED(bottomRight);
    Q_UNUSED(roles);
    //モデルからデータを取得し、表示内容を更新する.
    updateView(topLeft, model()->data(topLeft));
}
testview.h
#ifndef TESTVIEW_H
#define TESTVIEW_H

#include "myview.h"

#include <QWidget>
#include <QLabel>

class TestView : public MyView
{
    Q_OBJECT
public:
    explicit TestView(QWidget *parent = nullptr);
    void updateView(const QModelIndex &index, const QVariant &data) override;
private:
    QLabel *m_label;
};

#endif // TESTVIEW_H
testview.cpp
#include "sampledata.h"
#include "testview.h"

#include <QString>

TestView::TestView(QWidget *parent) : MyView(parent)
{
    m_label = new QLabel(this);
    m_label->resize(150, 30);
}


void TestView::updateView(const QModelIndex &index, const QVariant &data)
{
    switch (index.row()) {
    case 0:
        switch (index.column()) {
        case 0:
            m_label->setText(QString::number(data.toInt()));
            break;
        case 1:
            m_label->setText(data.value<SampleData>().data());
            break;
        default:
            break;
        }
        break;
    default:
        break;
    }

}

8.2.1. QAbstractItemViewの純粋仮想関数

    QRect visualRect(const QModelIndex &index) const override;
    void scrollTo(const QModelIndex &index, ScrollHint hint) override;
    QModelIndex indexAt(const QPoint &point) const override;

visualRect関数は、ウィジェットとしてのQRectを返します。
他の2つの関数は、今回は使用しません。

8.2.2. QAbstractItemViewからの追加実装関数

protected slots:
    void dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles) override;

このスロットがQAbstractItemModel::dataChangedシグナルに対応するスロットです。
Qt内部で接続されているので、自分で接続する必要はありません。
接続はQt::AutoConnection固定です。

void MyView::dataChanged(const QModelIndex &topLeft, const QModelIndex &bottomRight, const QVector<int> &roles)
{
    Q_UNUSED(bottomRight);
    Q_UNUSED(roles);
    //モデルからデータを取得し、表示内容を更新する.
    updateView(topLeft, model()->data(topLeft));
}

このスロット内のmodel()->data()でModel側のデータを取得します。
サンプルでは、TestView側に具体的な処理を移譲したかったので、updateView()関数という独自関数を用意しましたが、
ここの実装は人によりけりです。

9. データ更新時の動作シーケンス

データ更新時の流れをシーケンスにしてみます。
sequence.png

10. Model/Viewアーキテクチャを使用する側の実装

main.cpp
#include "widget.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Widget w;
    w.show();

    return a.exec();
}
widget.h
#ifndef WIDGET_H
#define WIDGET_H

#include <QWidget>

namespace Ui {
class Widget;
}

class MyModel;

class Widget : public QWidget
{
    Q_OBJECT

public:
    explicit Widget(QWidget *parent = 0);
    ~Widget();

private slots:
    void on_setTestData1Button_clicked();
    void on_setSampleDataButton_clicked();

private:
    Ui::Widget *ui;
    MyModel *model;
};

#endif // WIDGET_H
widget.cpp
#include "widget.h"
#include "ui_widget.h"
#include "mymodel.h"
#include "sampledata.h"

#include <QDebug>

Widget::Widget(QWidget *parent) :
    QWidget(parent),
    ui(new Ui::Widget)
{
    ui->setupUi(this);

    model = new MyModel(this);
    ui->widget->setModel(model);
}

Widget::~Widget()
{
    delete ui;
}

void Widget::on_setTestData1Button_clicked()
{
    int data = 100;
    model->setTestData(data);
}

void Widget::on_setSampleDataButton_clicked()
{
    SampleData sampleData;
    sampleData.setData(QString("test"));
    model->setSampleData(sampleData);
}
widget.ui
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Widget</class>
 <widget class="QWidget" name="Widget">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>256</width>
    <height>132</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Widget</string>
  </property>
  <widget class="TestView" name="widget" native="true">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>60</y>
     <width>211</width>
     <height>51</height>
    </rect>
   </property>
  </widget>
  <widget class="QPushButton" name="setTestData1Button">
   <property name="geometry">
    <rect>
     <x>20</x>
     <y>20</y>
     <width>101</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>setTestData1</string>
   </property>
  </widget>
  <widget class="QPushButton" name="setSampleDataButton">
   <property name="geometry">
    <rect>
     <x>130</x>
     <y>20</y>
     <width>101</width>
     <height>23</height>
    </rect>
   </property>
   <property name="text">
    <string>setSampleData</string>
   </property>
  </widget>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <customwidgets>
  <customwidget>
   <class>TestView</class>
   <extends>QWidget</extends>
   <header>testview.h</header>
   <container>1</container>
  </customwidget>
 </customwidgets>
 <resources/>
 <connections/>
</ui>

11. サンプルアプリの動作

application.png
「setTestData1」ボタンを押すと、ラベルに100が表示され、
「setSampleData」ボタンを押すと、ラベルに「test」が表示されます。

12. サンプルコードにおける問題点

QAbstractItemModel::setData()関数はpublicな関数です。
従って、現コードだとMyModelを使用する側がindex(0,0)やindex(0,1)といったデータ構造を意識する作りとなっています。
一応サンプルでは、直接setData()関数を呼ばないように、setTestData()関数やsetSampleData()関数を用意しましたが、
MyModelを使用する側にIndex情報を意識させたくないのであれば、setData()関数は非公開にするべきです。
もし、使用する側にIndexを意識させたくないのであれば、QAbstractItemModelクラスのサブクラスやQAbstractItemViewクラスのサブクラスはd-pointer化するのが望ましいかもしれません。

13. 最後に

長々と書いてしまいましたが、多少はイメージが湧いたでしょうか。
少しでもModel/Viewアーキテクチャの理解に繋がれば幸いです。
明日は @IoriAYANE さんの「Qt Quickアプリでスプラッシュウインドウを表示」です。
お楽しみに。

argama147
サークル:エゥーゴ。Qt/C++、Android/Java,Kotlin、IoTを主軸として活動するスキル横伸ばし型フリーランスPG/SE。技術書典5でQt5/C++入門、技術書典6でIoT、技術書典7でQtでAndroidアプリを作る本、技術書典9で「ライブラリを作ろう」という技術同人誌を頒布し、その後商業出版した。
https://techbookfest.org/organization/43220004
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away