先日、明星大学で行われたOpen Source Cunference 2016 Tokyo/SpringにQtユーザー会として出展してきました。
その際、QMLのデモに対してデータバインディングはどうなっているのかという質問があったのですが、いまいち質問の意味を理解できていなくて、きちんとお答えできなくてすいません。
どうやらGoogle先生経由で聞いた限り、その時聞きたかったデータバインディングというのは、元になるデータと画面表示とをどのように結びつけるのかというお話しのようですので、Qtでいう所のModel/View/Delegateのお話しを聞きたかったようです(間違っていたらごめんなさい)。
最近はかなり古い環境でのお仕事が多くて、用語や他の環境の情報収集が滞っているもので。Tcl/TkとかXlibとかならお話し通じるのですが・・・勉強不足でごめんなさい。QMLとデータバインディングだといまいちこれというものがヒットしなかったので、OSCで展示していたplanetsサンプルプログラムをベースに簡単に説明を書いておいてみようと思います。
Model/View Programingについて
GUIアプリケーションのデザインパターンとして、データ構造であるモデル, 描画をつかさどるビュー, 入力を受け取るコントローラ に分けて設計する MVCパターンが有名ですが、Qtでは、ビューとコントローラは不可分のものとして一つと考えるM-VCとなるようなModel/View構造を採用しています。
ところで、モデルとビューを分離させるとデータをどのように表現したいのか、あるいは入力をどのようにデータに反映させたいのかという両者の間を取り持つ必要がでてきます。Qtではデリゲートと呼ぶ機構にこの調整を任せています。
今回は、QMLについて、この部分がどうなっているのか見ていきます。
planets サンプルアプリケーション
OSCでデモしていたアプリは、canvas3dを使ったplanetsというデモになります。Model/Viewのサンプルではなく、WebGLが使えるCanvas3Dという機能のデモなのですが、興味を持ってくれた人も多かったので、このサンプルを読むときの助けにもなればということで。
たとえば、Qt5.5をインストールしている場合、インストールした先の以下のようなディレクトリの下にソースコードが一式あるかと思います。
Examples/Qt-5.5/canvas3d/canvas3d/threejs/planets
Qt Creatorをお使いの方は、ようこその画面のサンプルの中からPlanets Exampleを検索してみて下さい。
# 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を見てみましょう。
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でブロックを折り畳んだ状態だと考えて下さい。
簡単に画面上に見えているものだけ説明すると、Item(id:mainview)の中一杯にCanvas3D(id:canvas3d)がおいてあり、その上に
- ListView(id: planetButtonView)
- InfoSheet(id:info)
- StyledSlider(id:speedSlider) + Text
- StyledSlider(id:scaleSlider) + Text
- 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は、ListElementを格納するシンプルなコンテナです。ListElementはListModelで定義できるプロパティとしてデータを保持するリスト要素です。
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は、QML組込みのListModel、XmlListModelといったタイプや、C++でQAbstractItemModel,QAbstractListModelを継承したカスタムモデルを表示するためのViewです。
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 typeで、内部にPlanetButtonを保持しており、planetModelのプロパティをPlanetButtonとバインディングする役割を担っています。
Component {
id: planetButtonDelegate
PlanetButton {
source: planetImageSource
text: name
focusPlanet: planetNumber
planetSelector: mainview
}
}
planetButtonDelegateは、ListElementのプロパティをPlanetButtonのプロパティにバインディングしています。
PlanetButtonは、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になっています。
例えば、
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なんかを使ってしまえば、データベース内のデータを好きなように表示とか、色々できそうですよね。
QMLではなく Qt(C++) では
なお、蛇足ですが、C++で実装する場合も、考え方は同じです(・・・というより、Model/View/Delegateの概念はC++の実装が先にあったわけです)。QTableViewとかQListView、QTreeViewといった一般的なテーブルやリスト,ツリー表現用のビューが用意されています。C++の場合は、通常はデフォルトのデリゲートが用意されているので、特にデリゲートを意識しなくてもQTableView.setModel()でモデルをセットするだけで、一般的な見栄えのリスト表示にデータが並ぶようにできています。