C++型の属性をQMLに公開する

QML は C++ コードで定義された機能を簡単に拡張することができます。QML エンジンはQt メタオブジェクトシステムと密接に統合されているため、QObject 由来のクラスやQ_GADGET の型が適切に公開している機能であれば、 QML のコードからアクセスすることができます。これにより、C++のデータや関数にQMLから直接アクセスできるようになります。

QMLエンジンはメタオブジェクトシステムを通してQObject インスタンスをイントロスペクトする機能を持っています。つまり、どの QML コードでもQObject 由来クラスのインスタンスの以下のメンバにアクセスすることができます:

  • プロパティ
  • メソッド(パブリックスロットであるか、Q_INVOKABLE のフラグがついていれば)
  • シグナル

(さらに、Q_ENUM で宣言された列挙型も利用可能です。詳細はQML と C++ のデータ型変換を参照してください)。

一般的には、QObject から派生したクラスがQML の型システムに登録されているかどうかに関係なく、QML からアクセスすることができます。しかし、あるクラスをメソッドのパラメータやプロパティとして使ったり、 列挙型の1つをこのような形で使ったりする場合など、エンジンが追加の型情報に アクセスする必要があるような使い方をする場合には、そのクラスを登録する必要が あるかもしれません。登録された型のみがコンパイル時に解析されるため、QMLで使用するすべての型について登録することを推奨します。

Q_GADGET 。これらの型は既知の共通ベースから派生しておらず、自動的に利用できるようにすることができないため、登録が必要です。登録しないと、そのプロパティやメソッドにアクセスできません。

DEPENDENCIESオプションを使用してqt_add_qml_module呼び出しに依存関係を追加することで、異なるモジュールからのC++型を自分のモジュールで利用できるようにすることができます。例えば、QtQuick に依存することで、QMLで公開されているC++型がQColor をメソッドの引数や戻り値として利用できるようになります。QtQuickQColor値型 カラーとして公開します。このような依存関係は実行時に自動的に推測されるかも知れませんが、それ に頼るべきではありません。

また、この文書で扱われている重要な概念の多くは、チュートリアル「Writing QML Extensions with C++」で紹介されています。

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

データ型の取り扱いと所有権

C++からQMLへ転送されるデータは、プロパティ値、メソッドのパラメータや戻り値、 シグナルのパラメータ値など、QMLエンジンがサポートする型でなければなりません。

デフォルトでは、QML エンジンは多くの Qt C++ 型をサポートしており、QML から利用する際には自動的に適切な型に変換することができます。さらに、QMLの型システムに登録されているC++クラスも、適切に登録されていれば、その列挙型もデータ型として使用することができます。詳しくはQMLとC++間のデータ型変換を参照してください。

さらに、C++からQMLへデータを転送する際には、データ所有権のルールが考慮され ます。詳細は「データ所有権」を参照してください。

プロパティの公開

QObject から派生したクラスでは、Q_PROPERTY()マクロを使ってプロパティを指定することができます。プロパティはクラスのデータメンバであり、読み込み関数と書き込み関数を持ちます。

QObject-derived またはQ_GADGET クラスのすべてのプロパティは QML からアクセス可能です。

例えば、author プロパティを持つMessage クラスを以下に示します。Q_PROPERTY マクロ呼び出しで指定されたように、このプロパティはauthor() メソッドで読み込み可能であり、setAuthor() メソッドで書き込み可能です:

注: Q_PROPERTY の型にtypedefusingを使うと moc が混乱するので使わないでください。これにより、特定の型比較に失敗することがあります。

代わりに

using FooEnum = Foo::Enum;

class Bar : public QObject
{
    Q_OBJECT
    Q_PROPERTY(FooEnum enum READ enum WRITE setEnum NOTIFY enumChanged)
};

型を直接参照する:

class Bar : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Foo::Enum enum READ enum WRITE setEnum NOTIFY enumChanged)
};

Message を使えるようにするには、C++ でQML_ELEMENT を使い、CMake でqt_add_qml_module を使う必要があります。

class Message : public QObject
{
    Q_OBJECT
    QML_ELEMENT
    Q_PROPERTY(QString author READ author WRITE setAuthor NOTIFY authorChanged)
public:
    void setAuthor(const QString &a)
    {
        if (a != m_author) {
            m_author = a;
            emit authorChanged();
        }
    }

    QString author() const
    {
        return m_author;
    }

signals:
    void authorChanged();

private:
    QString m_author;
};

Message のインスタンスをMyItem.qml というファイルに必須プロパティとして渡すことで、利用可能にすることができます:

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

    QQuickView view;
    Message msg;
    view.setInitialProperties({{"msg", &msg}});
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}

その後、MyItem.qml からauthor プロパティを読み込むことができます:

// MyItem.qml
import QtQuick

Text {
    required property Message msg

    width: 100; height: 100
    text: msg.author    // invokes Message::author() to get this value

    Component.onCompleted: {
        msg.author = "Jonah"  // invokes Message::setAuthor()
    }
}

QMLとの相互運用性を最大にするために、書き込み可能なプロパティには、その値が変更されたときに発信されるNOTIFYシグナルを関連付けるべきです。これはQMLの本質的な機能であり、依存関係にあるプロパティの値が変更されると、自動的にそのプロパティが更新されることで、プロパティ間の関係を強制するものです。

上記の例では、author プロパティに関連する NOTIFY シグナルは、Q_PROPERTY() マクロ呼び出しで指定されたように、authorChanged です。つまり、Message::setAuthor() で作者が変更されたときなど、このシグナルが発せられるたびに、author プロパティに関係するすべてのバインディングを更新しなければならないことが QML エンジンに通知され、その結果、エンジンはMessage::author() を再度呼び出してtext プロパティを更新します。

author プロパティが書き込み可能であるにもかかわらず、関連する NOTIFY シグナルがない場合、text の値はMessage::author() によって返された初期値で初期化されますが、このプロパティに対する後の変更によって更新されることはありません。さらに、QML からこのプロパティにバインドしようとすると、エンジンから実行時警告が出される。

注意: NOTIFY シグナルは<property>Changedという名前にすることを推奨します。<property> はプロパティの名前です。QML エンジンが生成するプロパティ変更シグナルハンドラは、関連する C++ シグナルの名前にかかわらず、常にon<Property>Changed という形式をとります。

通知シグナルの使用に関する注意

ループや過剰な評価を防ぐため、開発者は、プロパティ値が実際に変更されたときにのみ、プロパティ変更シグナルが発信されるようにする必要があります。また、あるプロパティやプロパティ・グループの使用頻度が低い場合、複数のプロパティに同じ NOTIFY シグナルを使用することが許される。この場合、パフォーマンスが低下しないように注意する必要がある。

NOTIFY シグナルが存在すると、小さなオーバーヘッドが発生する。プロパティの値がオブジェクトの構築時に設定され、その後変更されない場合がある。最も一般的なケースは、グループ化されたプロパティを使用するタイプで、グループ化されたプロパティ・オブジェクトが一度割り当てられ、オブジェクトが削除されたときにのみ解放される場合です。このような場合、NOTIFYシグナルの代わりにCONSTANT属性をプロパティ宣言に追加することができる。

CONSTANT属性は、クラス・コンストラクタ内でのみ値が設定され、確定されるプロパティにのみ使用されるべきである。バインディングで使用したい他のすべてのプロパティは、代わりにNOTIFYシグナルを持つべきです。

オブジェクト型プロパティ

オブジェクト型のプロパティは、QMLの型システムに適切に登録されていれば、 QMLからアクセスすることができます。

例えば、Message 型はMessageBody* 型のbody プロパティを持つかもしれません:

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageBody* body READ body WRITE setBody NOTIFY bodyChanged)
public:
    MessageBody* body() const;
    void setBody(MessageBody* body);
};

class MessageBody : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString text READ text WRITE text NOTIFY textChanged)
// ...
}

Message 型がQMLの型システムに登録され、QMLのコードからオブジェクト 型として利用できるようになったとします:

Message {
    // ...
}

MessageBody 型も QML の型システムに登録されていれば、Messagebody プロパティにMessageBody を代入することができます:

Message {
    body: MessageBody {
        text: "Hello, world!"
    }
}

オブジェクトリスト型を持つプロパティ

QObject から派生した型のリストを含むプロパティもQMLに公開することができます。しかし、この場合、QList<T>ではなく、QQmlListProperty をプロパティの型として使う必要があります。なぜなら、QListQObject から派生した型ではないため、リストが変更されたときのシグナル通知など、Qt メタオブジェクトシステムを通じて必要な QML プロパティの特性を提供することができないからです。

例えば、以下のMessageBoard クラスにはQQmlListProperty 型のmessages プロパティがあり、Message インスタンスのリストを格納しています:

class MessageBoard : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QQmlListProperty<Message> messages READ messages)
public:
    QQmlListProperty<Message> messages();

private:
    static void append_message(QQmlListProperty<Message> *list, Message *msg);

    QList<Message *> m_messages;
};

MessageBoard::messages()関数は、QList<T>m_messages メンバからQQmlListProperty を作成し、QQmlListProperty コンストラクタが必要とする適切なリスト変更関数を渡して返すだけです:

QQmlListProperty<Message> MessageBoard::messages()
{
    return QQmlListProperty<Message>(this, 0, &MessageBoard::append_message);
}

void MessageBoard::append_message(QQmlListProperty<Message> *list, Message *msg)
{
    MessageBoard *msgBoard = qobject_cast<MessageBoard *>(list->object);
    if (msg)
        msgBoard->m_messages.append(msg);
}

なお、QQmlListProperty のテンプレートクラス型(ここではMessage )は QML の型システムに登録されている必要があります。

グループ化されたプロパティ

読み取り専用のオブジェクト型プロパティはグループ化されたプロパティとして QMLコードからアクセスすることができます。グループ化されたプロパティは、関連するプロパティをまとめて公開することが できます。

例えば、Message::author プロパティが単純な文字列ではなく、MessageAuthor タイプで、nameemail のサブプロパティを持つとします:

class MessageAuthor : public QObject
{
    Q_PROPERTY(QString name READ name WRITE setName)
    Q_PROPERTY(QString email READ email WRITE setEmail)
public:
    ...
};

class Message : public QObject
{
    Q_OBJECT
    Q_PROPERTY(MessageAuthor* author READ author)
public:
    Message(QObject *parent)
        : QObject(parent), m_author(new MessageAuthor(this))
    {
    }
    MessageAuthor *author() const {
        return m_author;
    }
private:
    MessageAuthor *m_author;
};

author プロパティは、QMLのグループ化されたプロパティ構文を使って、次のように記述することができます:

Message {
    author.name: "Alexandra"
    author.email: "alexandra@mail.com"
}

グループ化されたプロパティとして公開される型は、オブジェクト型のプロパティとは異なり、読み取り専用で、構築時に親オブジェクトによって有効な値に初期化されます。グループ化されたプロパティのサブプロパティは、QML から変更することができますが、グループ化されたプロパティオブジェクト自体が変更されることはありません。このように、グループ化されたプロパティオブジェクトの寿命は C++ の親実装によって厳密に制御されるのに対し、オブジェクト型のプロパティは QML のコードによって自由に生成・破棄することができます。

メソッドの公開(Qt スロットを含む)

QObject から派生した型のメソッドは、QML コードからアクセス可能です:

  • Q_INVOKABLE()マクロのついたパブリックメソッド。
  • Qt のパブリックスロットであるメソッド

例えば、以下のMessageBoard クラスはQ_INVOKABLE マクロでフラグ付けされたpostMessage() メソッドと、 public スロットであるrefresh() メソッドを持っています:

class MessageBoard : public QObject
{
    Q_OBJECT
    QML_ELEMENT

public:
    Q_INVOKABLE bool postMessage(const QString &msg) {
        qDebug() << "Called the C++ method with" << msg;
        return true;
    }

public slots:
    void refresh() {
        qDebug() << "Called the C++ slot";
    }
};

MessageBoard のインスタンスが、ファイルMyItem.qml の必須プロパティとして設定されている場合、MyItem.qml は、以下の例に示すように、2 つのメソッドを呼び出すことができます:

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

    MessageBoard msgBoard;
    QQuickView view;
    view.setInitialProperties({{"msgBoard", &msgBoard}});
    view.setSource(QUrl::fromLocalFile("MyItem.qml"));
    view.show();

    return app.exec();
}
QML
// MyItem.qml
import QtQuick 2.0

Item {
    required property MessageBoard msgBoard

    width: 100; height: 100

    MouseArea {
        anchors.fill: parent
        onClicked: {
            var result = msgBoard.postMessage("Hello from QML")
            console.log("Result of postMessage():", result)
            msgBoard.refresh();
        }
    }
}

C++ のメソッドにQObject* 型のパラメータがある場合、QML からオブジェクトid またはオブジェクトを参照する JavaScriptvar の値を使ってパラメータ値を渡すことができます。

QML はオーバーロードされた C++ 関数の呼び出しに対応しています。同じ名前で異なる引数を持つ C++ 関数が複数存在する場合、提供された引数の数 と型に応じて、正しい関数が呼び出されます。

C++ メソッドから返された値は、QML の JavaScript 式からアクセスされた場合、JavaScript の値に変換されます。

C++メソッドとthisオブジェクト

あるオブジェクトから C++ メソッドを取り出し、それを別のオブジェクトから呼び出したい場合があります。Example という QML モジュールの中で、次のような例を考えてみましょう:

C++
class Invokable : public QObject
{
    Q_OBJECT
    QML_ELEMENT
public:
    Invokable(QObject *parent = nullptr) : QObject(parent) {}

    Q_INVOKABLE void invoke() { qDebug() << "invoked on " << objectName(); }
};
QML
import QtQml
import Example

Invokable {
    objectName: "parent"
    property Invokable child: Invokable {}
    Component.onCompleted: child.invoke.call(this)
}

適切な main.cpp から QML コードをロードすると、"invoked on parent" と表示されるはずです。しかし、長年のバグのため、そうはなりません。歴史的に、C++ベースのメソッドの'this'オブジェクトはメソッドと不可分にバインドされています。既存のコードでこの動作を変更すると、多くの場所で 'this' オブジェクトが暗黙的に存在するため、微妙なエラーが発生します。Qt 6.5以降では、明示的に正しい動作を選択し、C++メソッドが'this'オブジェクトを受け入れるようにすることができます。そのためには、QMLドキュメントに以下のプラグマを追加してください:

pragma NativeMethodBehavior: AcceptThisObject

この行を追加することで、上記の例は期待通りに動作するようになります。

シグナルの公開

QObject に由来するパブリックシグナルは、QMLのコードからアクセス可能です。

QMLエンジンは、QObject-derived 型のシグナルをQMLから使用する場合、自動的にシグナルハンドラを生成します。シグナルハンドラは常にon<Signal>という名前になります。<Signal> はシグナルの名前で、最初の文字は大文字になります。シグナルから渡されるすべてのパラメータは、シグナルハンドラ内でパラメータ名を通して利用することができます。

例えば、MessageBoard クラスにnewMessagePosted() シグナルがあり、subject という1つのパラメータがあるとします:

class MessageBoard : public QObject
{
    Q_OBJECT
public:
   // ...
signals:
   void newMessagePosted(const QString &subject);
};

MessageBoard の型がQMLの型システムに登録されていれば、QMLで宣言されたMessageBoard オブジェクトはonNewMessagePosted という名前のシグナルハンドラを使ってnewMessagePosted() シグナルを受け取り、subject パラメータの値を調べることができます:

MessageBoard {
    onNewMessagePosted: (subject)=> console.log("New message received:", subject)
}

プロパティ値やメソッドパラメータと同様に、シグナルパラメータもQMLエンジンが サポートする型でなければなりません。(未登録の型を使用してもエラーにはなりませんが、ハンドラからパラメータ値にアクセスすることはできません)。

クラスは同じ名前のシグナルを複数持つことができますが、QMLシグナルとしてアクセ スできるのは最後のシグナルだけです。同じ名前で異なるパラメータを持つシグナルは区別できないことに注意してください。

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