凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Qt Quick での C++ × QML バインディングについてまとめてみた

はじめに

でもチラッと書きましたが、もう少し詳細に 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++ バインディングへのアプローチ

いくつかの方法があります。

  • QML 側で定義した要素を C++ から読み込んで使う
  • C++ 側で定義したクラスを QML から扱う
  • QML 側で定義した関数を C++ から呼ぶ
  • C++ 側で QML で使える新しい型を定義する

これらを紹介します。

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 に加えて、signalsslots といったアクセス指定子が出てきたりします。他にも上で見たような 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" に変わります。

おわりに

Qt の仕組み良く出来ていて面白いですね。特に C++ と JS のバインディングは v8 を生で使うよりもずっと楽な感じがします。また、moc も中でどういった処理をしているのか、まだよく理解できていないので調べてみたいです。