はじめに
でもチラッと書きましたが、もう少し詳細に Qt Quick での C++ バインディングについて調べてみたのでまとめてみました。
Qt 始めたてなので幾つか間違い含んでいるかもしれません、見つけたら教えて下さい。
参考
基本的には「Using QML Bindings in C++ Applications」の内容を要約して自分の解釈を付け加えた形になっています。
(メモ)Qt Quick 1 と Qt Quick 2 の差異について
Qt 5 が昨年末に登場し、その目玉の一つに Qt Quick 2 が挙げられます。Qt Quick 2 では JS エンジンを従来の JavaScriptCore に代わり V8 を採用するなど大きな変化が起こりました。このようにいくつかの変更が加わったため、ネットに落ちている幾つかの情報も古いものになっています。例えば QDeclarative* 系のクラスが Qml* 系のクラスへと変化したりしたようです。ただ、C++ バインディングに限った話をすれば、機能を利用するユーザ側で大きな変更は必要無いようです。
QML 側で定義した要素を C++ から読み込んで使う
Qt Quick 2 アプリケーションプロジェクトを Qt Creator で作成すると以下の様な QML が自動生成されます。
import QtQuick 2.0 Rectangle { width: 360 height: 360 Text { text: qsTr("Hello World") anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } }
これを C++ からアクセスして Hello World の文字列を書き換えてみたいと思います。以下のように、デフォルトの main.cpp からコメントのある 3 行を付け加えます。
#include <QtGui/QGuiApplication> #include <QQuickItem> #include "qtquick2applicationviewer.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QtQuick2ApplicationViewer viewer; viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml")); viewer.showExpanded(); // ルート要素 (Rectangle) QObject* root = viewer.rootObject(); // Text 要素へアクセス QObject* title = root->children().at(0); // プロパティの書き換え title->setProperty("text", "Hello, C++ world!"); return app.exec(); }
他にも要素へのアクセス方法は色々あり、QML 側で objectName を定義しておけば、QObject::findChild でアクセスできます。詳細については Qt Creator で QObject にカーソルを合わせた状態で F1 キーを押下するとドキュメントが表示されるので、そこを参照してみて下さい。
C++ 側で定義したクラスを QML から扱う
先ほど Hello C++ World! とした場所に C++ から与えた現在時刻を表示させてみます。
まずは動かしてみる
Qt Creator の「ファイル/プロジェクトの新規作成」から 「C++ > C++ ヘッダー」を選び、「applicationdata.h」を作成して下さい。そしてヘッダファイル内を以下のようにします。
#ifndef APPLICATIONDATA_H #define APPLICATIONDATA_H #include <QObject> #include <QDateTime> class ApplicationData : public QObject { Q_OBJECT public: Q_INVOKABLE QDateTime getCurrentDateTime() const { return QDateTime::currentDateTime(); } }; #endif // APPLICATIONDATA_H
で「main.cpp」側を以下のようにします。
#include <QtGui/QGuiApplication> #include "qtquick2applicationviewer.h" #include "applicationdata.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QtQuick2ApplicationViewer viewer; viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml")); viewer.showExpanded(); // QML 側へクラスをセット ApplicationData data; viewer.rootContext()->setContextProperty("applicationData", &data); return app.exec(); }
そして QML を以下のように書き換えます。
import QtQuick 2.0 Rectangle { width: 360 height: 360 Text { text: applicationData.getCurrentDateTime() anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { Qt.quit(); } } }
これで実行すると現在時刻が表示されるようになります。JS から C++ で定義した getCurrentDateTime にアクセスできているのがとても不思議ですね...。
ちなみに警告が出る場合は、.pro に
QT += declarative
を追記するとなくなります。
何をやっているのか
Qt にはメタオブジェクトコンパイラ(moc)と呼ばれるツールが付属しています。Qt のプログラムを書いていると、public、private、protected に加えて、signals や slots といったアクセス指定子が出てきたりします。他にも上で見たような Q_INVOKABLE のようなキーワードがいくつが出てきます。これらは C++ のコード的には無害(public や空白等)になるように定義されており、qmake した時点で、こういったキーワードをアノーテーションとして解釈して追加の C++ コードを生成する Makefile を吐き出し、make を実行すると追加の C++ ファイルを生成します。例えば applicationdata.h では次のような追加のファイルが書きだされます。
/**************************************************************************** ** Meta object code from reading C++ file 'applicationdata.h' ** ** Created by: The Qt Meta Object Compiler version 67 (Qt 5.0.1) ** ** WARNING! All changes made in this file will be lost! *****************************************************************************/ #include "../CppBindTest/applicationdata.h" #include <QtCore/qbytearray.h> #include <QtCore/qmetatype.h> #if !defined(Q_MOC_OUTPUT_REVISION) #error "The header file 'applicationdata.h' doesn't include <QObject>." #elif Q_MOC_OUTPUT_REVISION != 67 #error "This file was generated using the moc from 5.0.1. It" #error "cannot be used with the include files from this version of Qt." #error "(The moc has changed too much.)" #endif QT_BEGIN_MOC_NAMESPACE struct qt_meta_stringdata_ApplicationData_t { QByteArrayData data[3]; char stringdata[37]; }; #define QT_MOC_LITERAL(idx, ofs, len) \ Q_STATIC_BYTE_ARRAY_DATA_HEADER_INITIALIZER_WITH_OFFSET(len, \ offsetof(qt_meta_stringdata_ApplicationData_t, stringdata) + ofs \ - idx * sizeof(QByteArrayData) \ ) static const qt_meta_stringdata_ApplicationData_t qt_meta_stringdata_ApplicationData = { { QT_MOC_LITERAL(0, 0, 15), QT_MOC_LITERAL(1, 16, 18), QT_MOC_LITERAL(2, 35, 0) }, "ApplicationData\0getCurrentDateTime\0\0" }; #undef QT_MOC_LITERAL static const uint qt_meta_data_ApplicationData[] = { // content: 7, // revision 0, // classname 0, 0, // classinfo 1, 14, // methods 0, 0, // properties 0, 0, // enums/sets 0, 0, // constructors 0, // flags 0, // signalCount // methods: name, argc, parameters, tag, flags 1, 0, 19, 2, 0x02, // methods: parameters QMetaType::QDateTime, 0 // eod }; void ApplicationData::qt_static_metacall(QObject *_o, QMetaObject::Call _c, int _id, void **_a) { if (_c == QMetaObject::InvokeMetaMethod) { ApplicationData *_t = static_cast<ApplicationData *>(_o); switch (_id) { case 0: { QDateTime _r = _t->getCurrentDateTime(); if (_a[0]) *reinterpret_cast< QDateTime*>(_a[0]) = _r; } break; default: ; } } } const QMetaObject ApplicationData::staticMetaObject = { { &QObject::staticMetaObject, qt_meta_stringdata_ApplicationData.data, qt_meta_data_ApplicationData, qt_static_metacall, 0, 0} }; const QMetaObject *ApplicationData::metaObject() const { return QObject::d_ptr->metaObject ? QObject::d_ptr->dynamicMetaObject() : &staticMetaObject; } void *ApplicationData::qt_metacast(const char *_clname) { if (!_clname) return 0; if (!strcmp(_clname, qt_meta_stringdata_ApplicationData.stringdata)) return static_cast<void*>(const_cast< ApplicationData*>(this)); return QObject::qt_metacast(_clname); } int ApplicationData::qt_metacall(QMetaObject::Call _c, int _id, void **_a) { _id = QObject::qt_metacall(_c, _id, _a); if (_id < 0) return _id; if (_c == QMetaObject::InvokeMetaMethod) { if (_id < 1) qt_static_metacall(this, _c, _id, _a); _id -= 1; } else if (_c == QMetaObject::RegisterMethodArgumentMetaType) { if (_id < 1) *reinterpret_cast<int*>(_a[0]) = -1; _id -= 1; } return _id; } QT_END_MOC_NAMESPACE
この moc によって Qt は C++ の文法では簡単に記述するのが難しいシグナル/スロットなどの幾つかの機能を実現しています。
まずクラスを見てみると Q_OBJECT という記述があります。このマクロは moc で利用する幾つかのメタ情報へと展開されます。
次に JS からもアクセス出来ている getCurrentDateTime() メソッドを見てみると、Q_INVOKABLE なるキーワードが付加されています。これは
#define Q_INVOKABLE
と定義されているので実質何もしていないように見えますが、このアノーテーションにより、上で述べたように moc によって追加のコードが生成され JS からもアクセスできるようになります。moc によって書きだされたファイルを見てみると ApplicationData::qt_static_metacall 内でこの getCurrentDateTime を呼び出していることが分かります。ちなみにこの qt_static_metacall は Q_OBJECT で展開されるメンバの一つです。
#define Q_OBJECT \ public: \ Q_OBJECT_CHECK \ static const QMetaObject staticMetaObject; \ virtual const QMetaObject *metaObject() const; \ virtual void *qt_metacast(const char *); \ QT_TR_FUNCTIONS \ virtual int qt_metacall(QMetaObject::Call, int, void **); \ private: \ Q_DECL_HIDDEN_STATIC_METACALL static void qt_static_metacall(QObject *, QMetaObject::Call, int, void **); \ struct QPrivateSignal {};
次にプロパティおよびそのアクセサと変更を検知するイベントハンドラを追加してみます。
#include <QObject> class Hoge : public QObject { Q_OBJECT Q_PROPERTY(QString hoge READ hoge WRITE setHoge NOTIFY hogeChanged) public: Hoge() : hoge_("hogehoge") {} QString hoge() const { return hoge_; } void setHoge(const QString& str) { hoge_ = str; emit hogeChanged(); } signals: void hogeChanged(); private: QString hoge_; };
これで文字列をメンバ変数に持ち、そのアクセサおよび変更があったら呼ばれるイベントハンドラを持つクラスが出来ました。アクセサやイベントハンドラは次のようにマクロを使って定義します。
Q_PROPERTY(型 JSでのプロパティ名 READ C++でのgetter WRITE C++でのsetter NOTIFY C++でのイベントハンドラ)
でこれに基づくプライベート変数や getter/setter を定義します。イベントハンドラに関しては JS 側の関数が呼ばれるように moc で定義が作成されるため、宣言のみに留めておきます。
次にこのクラスを利用した QML を見てみます。
import QtQuick 2.0 Rectangle { width: 360 height: 360 Text { id: message text: Hoge.hoge anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { Hoge.hoge = "fugafuga"; } } function onHogeChanged() { message.text = Hoge.hoge; } }
クリックすると Hoge.hoge を "hogehoge" から "fugafuga" に書き換え、この結果、onHogeChanged が呼ばれ、テキストが書き換わるという簡単な内容になっています。
以上のような形で C++ のクラスを QML 側へセットすることができます。
QML 側で定義した関数を C++ から呼ぶ
JS 側の関数を呼ぶには前回も書きましたが、以下のようにします。
QMetaObject::invokeMethod(QObject, "QML 側の関数名", Q_ARG(型名, 引数に与える変数), ...);
実際にコードで見てみましょう。まず以下の様な QML を書きます。
import QtQuick 2.0 Rectangle { width: 360 height: 360 function hoge() { console.log("hogehoge!"); } function fuga(arr, obj) { arr.forEach(function(elem) { console.log("Array item:", elem); }); for (var key in obj) { console.log("Object item:", key, obj[key]); } } }
この hoge() と fuga() を C++ 側から呼んでみます。
#include <QtGui/QGuiApplication> #include <QQuickItem> #include "qtquick2applicationviewer.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); QtQuick2ApplicationViewer viewer; viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml")); viewer.showExpanded(); QQuickItem* root_obj = viewer.rootObject(); // 引数なしの hoge を呼ぶ QMetaObject::invokeMethod(root_obj, "hoge"); // Array と Object を引数にとる fuga を呼ぶ QVariantList list; list << 10 << QColor("green") << "bottles"; QVariantMap map; map.insert("language", "QML"); map.insert("released", QDate(2010, 9, 21)); QMetaObject::invokeMethod(root_obj, "fuga", Q_ARG(QVariant, QVariant::fromValue(list)), Q_ARG(QVariant, QVariant::fromValue(map))); return app.exec(); }
結果
hogehoge! Array item: 10 Array item: #008000 Array item: bottles Object item: language QML Object item: released Tue Sep 21 2010 00:00:00 GMT+0900 (JST)
C++ 側で QML で使える新しい型を定義する
先ほど定義したクラス Hoge を Hoge という型で QML で使えるようにしてみます。
#include <QtGui/QGuiApplication> #include "qtquick2applicationviewer.h" #include "applicationdata.h" int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); // ライブラリ名、メジャーバージョン、マイナーバージョン、型名 qmlRegisterType<Hoge>("HogeLibrary", 1, 0, "Hoge"); QtQuick2ApplicationViewer viewer; viewer.setMainQmlFile(QStringLiteral("qml/CppBindTest/main.qml")); viewer.showExpanded(); return app.exec(); }
qmlRegisterType を1行書くだけです。これで次のように利用することができます。
import QtQuick 2.0 import HogeLibrary 1.0 Rectangle { width: 360 height: 360 Hoge { id: hoge hoge: "hogehoge" onHogeChanged: { console.log("hoge was changed!") } } Text { id: message text: hoge.hoge anchors.centerIn: parent } MouseArea { anchors.fill: parent onClicked: { hoge.hoge = "fugafuga"; } } }
クリックすると MouseArea の onClicked が呼ばれ、hoge.hoge のテキスト "hogehoge" が "fugafuga" へと変わり、onHogeChanged が呼ばれると共に、Text の text がプロパティバインディングによって "hogehoge" から "fugafuga" に変わります。