C++からQMLオブジェクトを操作する

QML のオブジェクト型は、エンジン内部で実装されたものであれ、サードパーティが 定義したものであれ、すべてQObject から派生したものです。つまり、QML エンジンは QtMeta Object System を使って、任意の QML オブジェクト型を動的にインスタンス化し、生成されたオブジェクトを検査することができます。

これは、C++のコードからQMLオブジェクトを生成する際に便利で、視覚的にレンダリングできるQMLオブジェクトを表示したり、視覚的でないQMLオブジェクトのデータをC++アプリケーションに統合したりすることができます。一度作成された QML オブジェクトは、C++ から検査することで、プロパティの読み書きを行ったり、 メソッドを呼び出したり、シグナル通知を受け取ったりすることができます。

C++と様々なQML統合方法についての詳細は、C++とQML統合の概要ページをご参照ください。

C++ からの QML オブジェクトの読み込み

QMLドキュメントは、QQmlComponent またはQQuickView を使ってロードすることができます。QQmlComponent はQMLドキュメントをC++オブジェクトとしてロードし、C++コードから変更することができます。QQuickView も同様ですが、QQuickViewQWindow 由来のクラスであるため、ロードされたオブジェクトはビジュアル表示されます。QQuickView は一般的に、表示可能なQMLオブジェクトをアプリケーションのユーザーインターフェースに統合するために使用されます。

例えば、次のようなMyItem.qml ファイルがあるとします:

import QtQuick

Item {
    width: 100; height: 100
}

このQML文書は、QQmlComponent またはQQuickView を使って、次のようなC++コードで読み込むことができます。QQmlComponent を使うには、QQmlComponent::create() を呼び出してコンポーネントの新しいインスタンスを作成する必要があります。一方、QQuickView を使うと、自動的にコンポーネントのインスタンスが作成され、QQuickView::rootObject() からアクセスできるようになります:

// Using QQmlComponent
QQmlEngine engine;
QQmlComponent component(&engine,
        QUrl::fromLocalFile("MyItem.qml"));
QObject *object = component.create();
...
delete object;
// Using QQuickView
QQuickView view;
view.setSource(QUrl::fromLocalFile("MyItem.qml"));
view.show();
QObject *object = view.rootObject();

このobject は、作成されたMyItem.qml コンポーネントのインスタンスです。QObject::setProperty() またはQQmlProperty::write() を使用して、アイテムのプロパティを変更できます:

object->setProperty("width", 500);
QQmlProperty(object, "width").write(500);

QObject::setProperty()QQmlProperty::write() の違いは、後者がプロパティ値の設定に加えてバインディングの削除も行うことです。例えば、上記のwidth の代入がheight へのバインディングであったとします:

width: height

object->setProperty("width", 500) 呼び出しの後にItemheight が変更された場合、バインディングは有効なままなので、width は再度更新されます。しかし、QQmlProperty(object, "width").write(500) の呼び出しの後にheight が変更された場合、バインディングはもう存在しないので、width は変更されません。

別の方法として、オブジェクトを実際の型にキャストし、コンパイル時に安全なメソッドを呼び出すこともできます。この場合、MyItem.qml のベース・オブジェクトはItem であり、QQuickItem クラスによって定義されます:

QQuickItem *item = qobject_cast<QQuickItem*>(object);
item->setWidth(500);

また、QMetaObject::invokeMethod()やQObject::connect()を使って、コンポーネント内で定義されたシグナルや呼び出しメソッドに接続することもできます。詳しくは後述のQMLメソッドの呼び出しと QMLシグナルへの接続を参照してください。

よく定義されたC++インターフェースを介したQMLオブジェクトへのアクセス

C++ から QML にアクセスするための最良の方法は、C++ でインターフェースを定義し、 QML からそのインターフェースにアクセスすることです。他の方法では、QMLのコードをリファクタリングすると、QMLとC++の相互作用が簡単に壊れてしまいます。また、QMLとC++の相互作用は、QMLを経由することで、ユーザやqmllintのようなツールの両方から、より簡単に推論することができます。C++からQMLにアクセスすると、外部のC++コードがあるQMLコンポーネントを変更し ていないことを手作業で確認しなければ理解できないQMLコードが生成されます。

QMLにやりとりをさせるには、まずC++のインターフェースを定義する必要があります:

class CppInterface : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    // ...
};

QML主導のアプローチを用いると、このインターフェイスは2つの方法で操作することができます:

シングルトン

インターフェースにQML_SINGLETON マクロを追加することで、インターフェースをシングルトンとして登録し、すべてのコンポーネントに公開する方法があります。その後、単純なimport文でインターフェースが利用できるようになります:

import my.company.module

Item {
    Component.onCompleted: {
        CppInterface.foo();
    }
}

単純にオブジェクトを渡すだけでは、プロパティを介して他のコンポーネントに明示的に渡すか、非限定アクセスを使用する、遅くて推奨されない方法を利用する必要があるためです。

初期プロパティ

もうひとつの方法は、QML_UNCREATABLE を使ってインターフェースを作成不可能なものとしてマークし、QQmlComponent::createWithInitialProperties()を使ってルートQMLコンポーネントに供給し、QML側で必須プロパティを指定する方法です。

ルートコンポーネントは次のようになります:

import QtQuick

Item {
    required property CppInterface interface
    Component.onCompleted: {
        interface.foo();
    }
}

このプロパティを required にすることで、インターフェイスプロパティが設定されないままコンポーネントが 作成されることを防ぐことができます。

createWithInitialProperties() 以外はC++ から QML オブジェクトを読み込むで説明したのと同じ方法でコンポーネントを初期化することができます:

component.createWithInitialProperties(QVariantMap{{u"interface"_s, QVariant::fromValue<CppInterface *>(new CppInterface)}});

この方法は、インターフェースをルートコンポーネントだけが使用できるようにする必要がある場合に推奨されます。また、C++側でより簡単にインターフェースのシグナルやスロットに接続することができます。

どちらの方法も必要でない場合は、代わりにC++モデルの使い方を検討するとよいでしょう。

ロードされたQMLオブジェクトにオブジェクト名でアクセスする

QML コンポーネントは基本的にオブジェクトツリーであり、その子オブジェクトは 兄弟オブジェクトやその子オブジェクトを持ちます。QML コンポーネントの子オブジェクトは、QObject::objectName プロパティとQObject::findChild() を使って探すことができます。例えば、MyItem.qml のルート・アイテムに、Rectangle の子アイテムがあったとします:

import QtQuick

Item {
    width: 100; height: 100

    Rectangle {
        anchors.fill: parent
        objectName: "rect"
    }
}

子オブジェクトの位置は次のようになります:

QObject *rect = object->findChild<QObject*>("rect");
if (rect)
    rect->setProperty("color", "red");

オブジェクトは同じobjectName を持つ複数の子を持つことができることに注意してください。たとえば、ListView はそのデリゲートのインスタンスを複数作成するので、そのデリゲートが特定の objectName で宣言されている場合、ListView は同じobjectName を持つ複数の子を持つことになります。この場合、QObject::findChildren() を使用して、一致するobjectName を持つすべての子を見つけることができます。

警告 C++からQMLオブジェクトにアクセスし、それらを操作することは可能ですが、 テストやプロトタイピングを目的とする場合を除き、推奨される方法ではありません。QMLとC++の統合の強みの一つは、C++のロジックやデータセットのバックエンドとは別に、QMLでUIを実装できることです。このようなアプローチでは、QMLのUIをC++のUIに影響を与えずに変更することも難しくなります。

C++からQMLオブジェクト型のメンバへのアクセス

プロパティ

QML オブジェクトの中で宣言されたプロパティは、自動的に C++ からアクセスできるようになります。次のような QML アイテムがあるとします:

// MyItem.qml
import QtQuick

Item {
    property int someNumber: 100
}

someNumber プロパティの値はQQmlProperty 、あるいはQObject::setProperty() やQObject::property() を使って設定したり読み取ったりすることができます:

QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.qml");
QObject *object = component.create();

qDebug() << "Property value:" << QQmlProperty::read(object, "someNumber").toInt();
QQmlProperty::write(object, "someNumber", 5000);

qDebug() << "Property value:" << object->property("someNumber").toInt();
object->setProperty("someNumber", 100);

QML プロパティの値を変更する際には、必ずQObject::setProperty(),QQmlProperty (),QMetaProperty::write() を使用し、QML エンジンがプロパティの変更を認識できるようにする必要があります。例えば、m_buttonText のメンバ変数の値を内部的に反映するbuttonText プロパティを持つカスタム型PushButton があるとします。このようにメンバ変数を直接変更することは良いアイデアではありません:

//bad code
QQmlComponent component(engine, "MyButton.qml");
PushButton *button = qobject_cast<PushButton*>(component.create());
button->m_buttonText = "Click me";

値が直接変更されるため、Qtのメタオブジェクトシステムをバイパスすることになり、QMLエンジンはプロパティの変更を認識することができません。つまり、buttonText へのプロパティのバインディングは更新されず、onButtonTextChanged ハンドラも呼び出されません。

QMLメソッドの呼び出し

すべてのQMLメソッドはメタオブジェクトシステムに公開されており、C++からQMetaObject::invokeMethod() を使って呼び出すことができます。以下のコードにあるように、コロンの後にパラメータと戻り値の型を指定することが できます。これは例えば、C++のあるシグネチャを持つシグナルをQMLで定義されたメソッドに接続したい場合などに便利です。型を省略した場合、C++のシグネチャはQVariant を使用します。

以下はQMetaObject::invokeMethod() を使って QML メソッドを呼び出す C++ アプリケーションです:

QML
// MyItem.qml
import QtQuick

Item {
    function myQmlFunction(msg: string) : string {
        console.log("Got message:", msg)
        return "some return value"
    }
}
C++
// main.cpp
QQmlEngine engine;
QQmlComponent component(&engine, "MyItem.qml");
QObject *object = component.create();

QString returnedValue;
QString msg = "Hello from C++";
QMetaObject::invokeMethod(object, "myQmlFunction",
        Q_RETURN_ARG(QString, returnedValue),
        Q_ARG(QString, msg));

qDebug() << "QML function returned:" << returnedValue;
delete object;

コロンの後に指定されたパラメータと戻り値の型に注目してください。型名には値型や オブジェクト型が使えます。

もし型が省略されていたり、QMLの中でvar と指定されている場合は、QMetaObject::invokeMethod を呼び出す際に、Q_RETURN_ARG()とQ_ARG()で型としてQVariant を渡す必要があります。

QMLシグナルへの接続

すべての QML シグナルは自動的に C++ で利用可能となり、通常の Qt C++ シグナルと同様にQObject::connect() を使って接続することができます。その代わり、C++のシグナルはシグナルハンドラを用いてQMLオブジェクトに受信させることができます。

以下は、qmlSignal という名前のシグナルを持つ QML コンポーネントです。 は文字列型のパラメータで発信されます。このシグナルはQObject::connect() を使って C++ オブジェクトのスロットに接続され、qmlSignal が発信されるたびにcppSlot() メソッドが呼び出されるようになっています:

// MyItem.qml
import QtQuick

Item {
    id: item
    width: 100; height: 100

    signal qmlSignal(msg: string)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal("Hello from QML")
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(const QString &msg) {
        qDebug() << "Called the C++ slot with message:" << msg;
    }
};

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

    QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QString)),
                     &myClass, SLOT(cppSlot(QString)));

    view.show();
    return app.exec();
}

シグナルのパラメータに含まれる QML オブジェクトの型は、C++ のクラスへのポインタに変換されます:

// MyItem.qml
import QtQuick 2.0

Item {
    id: item
    width: 100; height: 100

    signal qmlSignal(anObject: Item)

    MouseArea {
        anchors.fill: parent
        onClicked: item.qmlSignal(item)
    }
}
class MyClass : public QObject
{
    Q_OBJECT
public slots:
    void cppSlot(QQuickItem *item) {
       qDebug() << "Called the C++ slot with item:" << item;

       qDebug() << "Item dimensions:" << item->width()
                << item->height();
    }
};

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

    QQuickView view(QUrl::fromLocalFile("MyItem.qml"));
    QObject *item = view.rootObject();

    MyClass myClass;
    QObject::connect(item, SIGNAL(qmlSignal(QVariant)),
                     &myClass, SLOT(cppSlot(QVariant)));

    view.show();
    return app.exec();
}

本ドキュメントに含まれる文書の著作権は、それぞれの所有者に帰属します。 本書で提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。