QMLのデータバインディングについて

先日、明星大学で行われたOpen Source Cunference 2016 Tokyo/Springに[Qtユーザー会として出展してきました](http://blog.hermit4.info/2016/02/osc-2016-tokyospring.html)。

その際、QMLのデモに対してデータバインディングはどうなっているのかという質問があったのですが、いまいち質問の意味を理解できていなくて、きちんとお答えできなくてすいません。

どうやらGoogle先生経由で聞いた限り、その時聞きたかったデータバインディングというのは、元になるデータと画面表示とをどのように結びつけるのかというお話しのようですので、Qtでいう所の[Model/View/Delegate](http://doc.qt.io/qt-5/qtquick-modelviewsdata-modelview.html)のお話しを聞きたかったようです（間違っていたらごめんなさい)。

最近はかなり古い環境でのお仕事が多くて、用語や他の環境の情報収集が滞っているもので。Tcl/TkとかXlibとかならお話し通じるのですが・・・勉強不足でごめんなさい。QMLとデータバインディングだといまいちこれというものがヒットしなかったので、OSCで展示していたplanetsサンプルプログラムをベースに簡単に説明を書いておいてみようと思います。

# Model/View Programingについて

GUIアプリケーションのデザインパターンとして、データ構造であるモデル, 描画をつかさどるビュー, 入力を受け取るコントローラ に分けて設計する MVCパターンが有名ですが、Qtでは、ビューとコントローラは不可分のものとして一つと考えるM-VCとなるようなModel/View構造を採用しています。

ところで、モデルとビューを分離させるとデータをどのように表現したいのか、あるいは入力をどのようにデータに反映させたいのかという両者の間を取り持つ必要がでてきます。Qtではデリゲートと呼ぶ機構にこの調整を任せています。

![modelview-overview.png](https://qiita-image-store.s3.amazonaws.com/0/58828/8bff5b1a-01f3-cf83-0eb4-fdfa806d935f.png)
（この画像はQtドキュメントからの抜粋です）

今回は、QMLについて、この部分がどうなっているのか見ていきます。

# planets サンプルアプリケーション
OSCでデモしていたアプリは、canvas3dを使ったplanetsというデモになります。Model/Viewのサンプルではなく、WebGLが使えるCanvas3Dという機能のデモなのですが、興味を持ってくれた人も多かったので、このサンプルを読むときの助けにもなればということで。

![planets.png](https://qiita-image-store.s3.amazonaws.com/0/58828/10f8a126-04dc-7f77-2626-3e6412f3d0bc.png)

たとえば、Qt5.5をインストールしている場合、インストールした先の以下のようなディレクトリの下にソースコードが一式あるかと思います。
Examples/Qt-5.5/canvas3d/canvas3d/threejs/planets

Qt Creatorをお使いの方は、ようこその画面のサンプルの中からPlanets Exampleを検索してみて下さい。

```cpp:main.cpp
#include <QtGui/QGuiApplication>
#include <QtCore/QDir>
#include <QtQuick/QQuickView>
#include <QtQml/QQmlEngine>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView viewer;

    // The following are needed to make examples run without having to install the module
    // in desktop environments.
#ifdef Q_OS_WIN
    QString extraImportPath(QStringLiteral("%1/../../../../%2"));
#else
    QString extraImportPath(QStringLiteral("%1/../../../%2"));
#endif
    viewer.engine()->addImportPath(extraImportPath.arg(QGuiApplication::applicationDirPath(),
                                                       QString::fromLatin1("qml")));

    viewer.setSource(QUrl("qrc:/planets.qml"));

    viewer.setTitle(QStringLiteral("Qt Canvas 3D Examples - Planets"));
    viewer.setResizeMode(QQuickView::SizeRootObjectToView);
    viewer.show();

    return app.exec();
}
```

mainの中では、QQuickViewに対してplanets.qmlをsetSourceしていますので、planets.qmlを見てみましょう。

```qml:planets.qml
import QtQuick 2.0
import QtCanvas3D 1.0

import "planets.js" as GLCode

Item {
    id: mainview
    width: 1280
    height: 768
    visible: true
    property int focusedPlanet: 100
    property int oldPlanet: 0
    property real xLookAtOffset: 0
    property real yLookAtOffset: 0
    property real zLookAtOffset: 0
    property real xCameraOffset: 0
    property real yCameraOffset: 0
    property real zCameraOffset: 0
    property real cameraNear: 0

    NumberAnimation { id: lookAtOffsetAnimation ... }
    NumberAnimation { id: cameraOffsetAnimation ... }
    Behavior on cameraNear { ... }
    onFocusedPlanetChanged: { ... }
    Canvas3D { id: canvas3d ... }
    ListModel { id: planetModel ... }
    Component { id: planetButtonDelegate ... }
    ListView { id: planetButtonView ... }
    InfoSheet { id: info ... }
    function updatePlanetInfo() { ... }
    StyledSlider { id: speedSlider ... }
    Text { ... }
    StyledSlider { id: scaleSlider ... }
    Text { ... }
    StyledSlider { id: distanceSlider ... }
    Text { ... }
    FpsDisplay { id: fpsDisplay ... }
}
```

コードを全部載せると長いので、ブロック無いの記述は省略しています。ちょうどQt Creatorでブロックを折り畳んだ状態だと考えて下さい。

![planet_number.png](https://qiita-image-store.s3.amazonaws.com/0/58828/5bfff040-edae-3413-ece4-1ca6d026ce7a.png)


簡単に画面上に見えているものだけ説明すると、Item(id:mainview)の中一杯にCanvas3D(id:canvas3d)がおいてあり、その上に
1. ListView(id: planetButtonView)
2. InfoSheet(id:info)
3. StyledSlider(id:speedSlider) + Text
4. StyledSlider(id:scaleSlider) + Text
5. StyledSlider(id:distanceSlider) + Text

が置いてあります。ちなみにFpsDisplayはhide:trueになっているので画面上に見えません。

今回は、Model/View/Delegateの話ですので、この中で1にあたるListViewと、ListModel(id: planetModel), Component(id: planetButtonDelegate)について見ていきます。

# QMLのModel/View/Delegate
## Model - ListModel

[ListModel](http://doc.qt.io/qt-5/qml-qtqml-models-listmodel.html)は、[ListElement](http://doc.qt.io/qt-5/qml-qtqml-models-listelement.html)を格納するシンプルなコンテナです。ListElementはListModelで定義できるプロパティとしてデータを保持するリスト要素です。

```qml:
    ListModel {
        id: planetModel

        ListElement {
            name: "Sun"
            radius: "109 x Earth"
            temperature: "5 778 K"
            orbitalPeriod: ""
            distance: ""
            planetImageSource: "qrc:/images/sun.png"
            planetNumber: 0
        }
        ListElement {
            name: "Mercury"
            radius: "0.3829 x Earth"
            temperature: "80-700 K"
            orbitalPeriod: "87.969 d"
            distance: "0.387 098 AU"
            planetImageSource: "qrc:/images/mercury.png"
            planetNumber: 1
        }
        ListElement {
            name: "Venus"
            radius: "0.9499 x Earth"
            temperature: "737 K"
            orbitalPeriod: "224.701 d"
            distance: "0.723 327 AU"
            planetImageSource: "qrc:/images/venus.png"
            planetNumber: 2
        }
        ListElement {
            name: "Earth"
            radius: "6 378.1 km"
            temperature: "184-330 K"
            orbitalPeriod: "365.256 d"
            distance: "149598261 km (1 AU)"
            planetImageSource: "qrc:/images/earth.png"
            planetNumber: 3
        }
        ListElement {
            name: "Mars"
            radius: "0.533 x Earth"
            temperature: "130-308 K"
            orbitalPeriod: "686.971 d"
            distance: "1.523679 AU"
            planetImageSource: "qrc:/images/mars.png"
            planetNumber: 4
        }
        ListElement {
            name: "Jupiter"
            radius: "11.209 x Earth"
            temperature: "112-165 K"
            orbitalPeriod: "4332.59 d"
            distance: "5.204267 AU"
            planetImageSource: "qrc:/images/jupiter.png"
            planetNumber: 5
        }
        ListElement {
            name: "Saturn"
            radius: "9.4492 x Earth"
            temperature: "84-134 K"
            orbitalPeriod: "10759.22 d"
            distance: "9.5820172 AU"
            planetImageSource: "qrc:/images/saturn.png"
            planetNumber: 6
        }
        ListElement {
            name: "Uranus"
            radius: "4.007 x Earth"
            temperature: "49-76 K"
            orbitalPeriod: "30687.15 d"
            distance: "19.189253 AU"
            planetImageSource: "qrc:/images/uranus.png"
            planetNumber: 7
        }
        ListElement {
            name: "Neptune"
            radius: "3.883 x Earth"
            temperature: "55-72 K"
            orbitalPeriod: "60190.03 d"
            distance: "30.070900 AU"
            planetImageSource: "qrc:/images/neptune.png"
            planetNumber: 8
        }
        ListElement {
            name: "Solar System"
            planetImageSource: ""
            planetNumber: 100 // Defaults to solar system
        }
    }
```

## View - ListView
[ListView](http://doc.qt.io/qt-5/qml-qtquick-listview.html)は、QML組込みのListModel、XmlListModelといったタイプや、C++でQAbstractItemModel,QAbstractListModelを継承したカスタムモデルを表示するためのViewです。

```qml:
    ListView {
        id: planetButtonView
        anchors.top: parent.top
        anchors.right: parent.right
        anchors.bottom: parent.bottom
        anchors.rightMargin: 15
        anchors.bottomMargin: 10
        spacing: 10
        width: 100
        interactive: false
        model: planetModel
        delegate: planetButtonDelegate
    }
```

modelプロパティで表示対象のモデルとして先ほど記載したplanetModelを、そのモデルをどう表示したいのかは、delegateプロパティにplanetButtonDelegateを指定しています。

## Delegate- Component

さて、いよいよ本命のデリゲートですが、[Component](http://doc.qt.io/qt-5/qml-qtqml-component.html) typeで、内部にPlanetButtonを保持しており、planetModelのプロパティをPlanetButtonとバインディングする役割を担っています。

```qml:
    Component {
        id: planetButtonDelegate
        PlanetButton {
            source: planetImageSource
            text: name
            focusPlanet: planetNumber
            planetSelector: mainview
        }
    }
```

planetButtonDelegateは、ListElementのプロパティをPlanetButtonのプロパティにバインディングしています。
PlanetButtonは、PlanetButton.qmlで実装されており、マウスエリアを持つ画像とテキストとを内包する四角形として定義されています。

```qml:PlanetButton.qml
import QtQuick 2.0

Rectangle {

    id: planetButton
    property alias text: planetText.text
    property alias source: planetImage.source
    property alias focusPlanet: planetImage.focusPlanet
    property Item planetSelector: parent.parent
    property int buttonSize: 70

    width: buttonSize; height: buttonSize
    color: "transparent"

    Image {
        id: planetImage
        anchors.fill: parent
        property int focusPlanet

        MouseArea {
            anchors.fill: parent
            hoverEnabled: true
            onClicked: { planetSelector.focusedPlanet = focusPlanet; }
            onEntered: PropertyAnimation { target: planetText; property: "opacity"; to: 1 }
            onExited: PropertyAnimation { target: planetText;
                property: "opacity";
                to: {
                    if (planetText.text != "Solar System")
                       0
                    else
                       1
                }
            }
        }
    }

    Text {
        id: planetText
        anchors.centerIn: parent
        font.family: "Helvetica"
        font.pixelSize: 16
        font.weight: Font.Light
        color: "white"
        opacity: {
            if (text == "Solar System") {
                opacity = 1;
            } else {
                opacity = 0;
            }
        }
    }
}
```

PlanetButton.qmlのsource, text, focusPlanetはaliasに、planetSelectorはItemになっています。

例えば、

```qml:
        ListElement {
            name: "Earth"
            radius: "6 378.1 km"
            temperature: "184-330 K"
            orbitalPeriod: "365.256 d"
            distance: "149598261 km (1 AU)"
            planetImageSource: "qrc:/images/earth.png"
            planetNumber: 3
        }
```

をListViewに表示する場合、Delegateによって

```:
PlanetButton.planetImage.source      = "qrc:/images/earth.png"
PlanetButton.planetText.text         = "Earth"
PlanetButton.planetImage.focusPlanet = 3
PlanetButton.planetSelector          = mainview
```

が設定されるわけですね。以上のように、QMLにおいても、ViewとModelのバインディングはデリゲートを使って簡単にできますよというお話しでした。

このあたりの説明は、XMLHttpRequestとXmlListModel等を使うとさらにお手軽にWeb APIをたたいてその結果を表示するアプリなんてものもサクサクとかけてしまいますという例が多いので、その辺りでGoogle先生に聞くとこれより面白そうなものがザクザクでてくるかもしれません。あるいは、C++と連携して[QSqlQueryModel](http://doc.qt.io/qt-5/qsqlquerymodel.html)なんかを使ってしまえば、データベース内のデータを好きなように表示とか、色々できそうですよね。


# QMLではなく Qt(C++) では
なお、蛇足ですが、C++で実装する場合も、[考え方は同じ](http://doc.qt.io/qt-5/model-view-programming.html)です(・・・というより、Model/View/Delegateの概念はC++の実装が先にあったわけです）。QTableViewとかQListView、QTreeViewといった一般的なテーブルやリスト,ツリー表現用のビューが用意されています。C++の場合は、通常はデフォルトのデリゲートが用意されているので、特にデリゲートを意識しなくてもQTableView.setModel()でモデルをセットするだけで、一般的な見栄えのリスト表示にデータが並ぶようにできています。