C++で高度なQML拡張機能を書く

BirthdayParty ベースプロジェクト

extending-qml-advanced/advanced1-Base-project

このチュートリアルでは、誕生日パーティーを例にして QML の機能のいくつかを紹介します。以下で説明する様々な機能のコードは、この誕生日パーティーのプロジェクトを基にしており、QML拡張に関する最初のチュートリアルの資料の一部に依存しています。この単純な例は、以下に説明する様々なQML拡張機能を説明するために拡張されています。それぞれの新しい拡張機能の完全なコードは、チュートリアルの各セクションのタイトルの下に指定されている場所か、このページの一番最後にあるコードへのリンクをたどって見つけることができます。

ベースプロジェクトではPerson クラスとBirthdayParty クラスが定義されており、それぞれ出席者とパーティーそのものをモデル化しています。

class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL)
    Q_PROPERTY(int shoeSize READ shoeSize WRITE setShoeSize NOTIFY shoeSizeChanged FINAL)
    QML_ELEMENT
    ...
    QString m_name;
    int m_shoeSize = 0;
};

class BirthdayParty : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
    Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
    QML_ELEMENT
    ...
    Person *m_host = nullptr;
    QList<Person *> m_guests;
};

パーティに関するすべての情報は、対応するQMLファイルに格納することができます。

BirthdayParty {
    host: Person {
        name: "Bob Jones"
        shoeSize: 12
    }
    guests: [
        Person { name: "Leo Hodges" },
        Person { name: "Jack Smith" },
        Person { name: "Anne Brown" }
    ]
}

main.cpp ファイルは、誰の誕生日で、誰がパーティーに招待されているかを表示するシンプルなシェルアプリケーションを作成します。

    QQmlEngine engine;
    QQmlComponent component(&engine);
    component.loadFromModule("People", "Main");
    std::unique_ptr<BirthdayParty> party{ qobject_cast<BirthdayParty *>(component.create()) };

このアプリはパーティーの概要を次のように出力します。

"Bob Jones" is having a birthday!
They are inviting:
    "Leo Hodges"
    "Jack Smith"
    "Anne Brown"

以下では、継承と強制を利用して、Person だけでなく、BoyGirl の出席者のサポートを追加する方法、デフォルトのプロパティを利用してパーティの出席者を暗黙のうちにゲストとして割り当てる方法、プロパティを一人ずつではなくグループとして割り当てる方法、添付オブジェクトを利用して招待されたゲストの応答を追跡する方法、プロパティ値のソースを利用してハッピーバースデーソングの歌詞を時間経過とともに表示する方法、サードパーティのオブジェクトを QML に公開する方法について説明します。

継承と強制

extending-qml-advanced/advanced2-Inheritance-and-coercion

現在、各アテンダントは一人の人間としてモデル化されています。これは少し一般的すぎるので、出席者についてもっと詳しく知ることができるといいでしょう。出席者を男の子と女の子に特化させることで、誰が来るのかをよりよく知ることができる。

そのために、Person を継承したBoyGirl クラスを導入する。

class Boy : public Person
{
    Q_OBJECT
    QML_ELEMENT
public:
    using Person::Person;
};

class Girl : public Person
{
    Q_OBJECT
    QML_ELEMENT
public:
    using Person::Person;
};

Person クラスはそのままで、BoyGirl C++ クラスはその些細な拡張です。型とそのQML名はQML_ELEMENT でQMLエンジンに登録されます。

BirthdayPartyhost プロパティとguests プロパティはPerson のインスタンスを取ります。

class BirthdayParty : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
    Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
    QML_ELEMENT
    ...
};

Person クラスの実装自体は変更されていません。しかし、Person クラスはBoyGirl の共通ベースとして再利用されたため、Person は QML から直接インスタンス化することはできません。代わりにBoyGirl をインスタンス化する必要があります。

class Person : public QObject
{
    ...
    QML_ELEMENT
    QML_UNCREATABLE("Person is an abstract base class.")
    ...
};

QML 内からPerson をインスタンス化することを禁止したいのですが、プロパティ型とし て使用できるように QML エンジンに登録する必要があります。これを行うのがQML_UNCREATABLE マクロです。PersonBoyGirl の3つの型はすべてQMLシステムに登録されているので、割り当て時にQMLは自動的に(そして型安全に)BoyGirl のオブジェクトをPerson に変換します。

これらの変更により、次のように誕生日パーティーの参加者情報を指定することができます。

BirthdayParty {
    host: Boy {
        name: "Bob Jones"
        shoeSize: 12
    }
    guests: [
        Boy { name: "Leo Hodges" },
        Boy { name: "Jack Smith" },
        Girl { name: "Anne Brown" }
    ]
}

デフォルトのプロパティ

extending-qml-advanced/advanced3-Default-properties

現在、QMLファイルでは、各プロパティは明示的に割り当てられています。例えば、host プロパティには、Boy が割り当てられ、guests プロパティには、Boy またはGirl のリストが割り当てられています。これは簡単ですが、この特定のユースケースのために、もう少しシンプルにすることができます。guests プロパティを明示的に割り当てる代わりに、BoyGirl オブジェクトをパーティ内部に直接追加し、guests に暗黙的に割り当てることができます。指定した出席者のうち、ホスト以外の出席者はすべてゲストということになります。この変更は純粋に構文的なものですが、多くの場面でより自然な感じを加えることができます。

guests プロパティは、BirthdayParty のデフォルト・プロパティとして指定することができます。 つまり、BirthdayParty の内部で作成された各オブジェクトは、暗黙のうちにデフォルト・プロパティguests に追加されます。出来上がったQMLは次のようになります。

BirthdayParty {
    host: Boy {
        name: "Bob Jones"
        shoeSize: 12
    }

    Boy { name: "Leo Hodges" }
    Boy { name: "Jack Smith" }
    Girl { name: "Anne Brown" }
}

この動作を有効にするために必要な唯一の変更は、guests をデフォルト・プロパティとして指定するために、BirthdayPartyDefaultProperty class info アノテーションを追加することだけです。

class BirthdayParty : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
    Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
    Q_CLASSINFO("DefaultProperty", "guests")
    QML_ELEMENT
    ...
};

このメカニズムについては、すでにご存知かもしれません。QMLにおけるItem のすべての子孫のデフォルト・プロパティはdata プロパティです。Item のプロパティに明示的に追加されない要素は、すべてdata に追加されます。 これにより、構造が明確になり、コード内の不要なノイズが減ります。

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

extending-qml-advanced/advanced4-Grouped-properties

ゲストの靴については、さらに多くの情報が必要です。サイズの他に、靴の色、ブランド、価格も保存したい。この情報はShoeDescription クラスに格納されます。

class ShoeDescription : public QObject
{
    Q_OBJECT
    Q_PROPERTY(int size READ size WRITE setSize NOTIFY shoeChanged FINAL)
    Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY shoeChanged FINAL)
    Q_PROPERTY(QString brand READ brand WRITE setBrand NOTIFY shoeChanged FINAL)
    Q_PROPERTY(qreal price READ price WRITE setPrice NOTIFY shoeChanged FINAL)
    ...
};

name と靴の説明shoe です。

class Person : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL)
    Q_PROPERTY(ShoeDescription *shoe READ shoe WRITE setShoe NOTIFY shoeChanged FINAL)
    ...
};

靴の説明の各要素の値を指定することはうまくいきますが、少し繰り返しが多くなります。

    Girl {
        name: "Anne Brown"
        shoe.size: 7
        shoe.color: "red"
        shoe.brand: "Job Macobs"
        shoe.price: 99.99
    }

グループ化されたプロパティは、これらのプロパティをよりエレガントに割り当てる方法を提供します。各プロパティに値を一つずつ代入する代わりに、個々の値をグループとしてshoe プロパティに渡すことができ、コードがより読みやすくなります。この機能はQMLのデフォルトで利用可能なので、有効にするための変更は必要ありません。

    host: Boy {
        name: "Bob Jones"
        shoe { size: 12; color: "white"; brand: "Bikey"; price: 90.0 }
    }

付属プロパティ

extending-qml-advanced/advanced5-Attached-properties

ホストが招待状を送る時が来ました。どの招待客がいつ返信したかを記録するために、その情報を保存する場所が必要です。BirthdayParty オブジェクト自体に格納するのはあまり適切ではない。よりよい方法は、パーティ・オブジェクトにアタッチされたオブジェクトとして返事を保存することです。

まず、BirthdayPartyAttached クラスを宣言します。

class BirthdayPartyAttached : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QDate rsvp READ rsvp WRITE setRsvp NOTIFY rsvpChanged FINAL)
    QML_ANONYMOUS
    ...
};

そして、それをBirthdayParty クラスにアタッチし、アタッチされたオブジェクトを返すためにqmlAttachedProperties() を定義します。

class BirthdayParty : public QObject
{
    ...
    QML_ATTACHED(BirthdayPartyAttached)
    ...
    static BirthdayPartyAttached *qmlAttachedProperties(QObject *);

};

これで、アタッチされたオブジェクトをQMLの中で使って、招待客のrsvp情報を保持することができます。

BirthdayParty {
    Boy {
        name: "Robert Campbell"
        BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-01", "yyyy-MM-dd")
    }

    Boy {
        name: "Leo Hodges"
        shoe { size: 10; color: "black"; brand: "Reebok"; price: 59.95 }
        BirthdayParty.rsvp: Date.fromLocaleString(Qt.locale(), "2023-03-03", "yyyy-MM-dd")
    }

    host: Boy {
        name: "Jack Smith"
        shoe { size: 8; color: "blue"; brand: "Puma"; price: 19.95 }
    }
}

最後に、この情報は次のようにしてアクセスできます。

            QDate rsvpDate;
            QObject *attached = qmlAttachedPropertiesObject<BirthdayParty>(guest, false);

            if (attached)
                rsvpDate = attached->property("rsvp").toDate();

このプログラムは、次のようなパーティーの概要を出力します。

"Jack Smith" is having a birthday!
He is inviting:
    "Robert Campbell" RSVP date: "Wed Mar 1 2023"
    "Leo Hodges" RSVP date: "Mon Mar 6 2023"

プロパティ値 ソース

extending-qml-advanced/advanced6-Property-value-source

パーティーの間、ゲストはホストのために歌わなければならない。ゲストを助けるために、プログラムがその場用にカスタマイズされた歌詞を表示できれば便利です。そのために、時間の経過とともに歌の節を生成するプロパティ値ソースを使用します。

class HappyBirthdaySong : public QObject, public QQmlPropertyValueSource
{
    Q_OBJECT
    Q_INTERFACES(QQmlPropertyValueSource)
    ...
    void setTarget(const QQmlProperty &) override;

};

値ソースとしてHappyBirthdaySong というクラスが追加されている。このクラスはQQmlPropertyValueSource を継承し、Q_INTERFACES マクロでQQmlPropertyValueSource インタフェースを実装する必要があります。setTarget() 関数を使用して、このソースがどのプロパティに作用するかを定義します。この場合、値ソースは、BirthdayPartyannouncement プロパティに書き込み、歌詞を時系列で表示します。この関数には内部タイマーがあり、パーティのannouncement プロパティに歌詞の次の行が繰り返し設定されます。

QML では、HappyBirthdaySongBirthdayParty の内部でインスタンス化されます。シグネチャのon キーワードは、値ソースがターゲットとするプロパティ(この場合はannouncement )を指定するために使用されます。また、HappyBirthdaySong オブジェクトのname プロパティは、パーティのホストの名前にバインドされています。

BirthdayParty {
    id: party
    HappyBirthdaySong on announcement {
        name: party.host.name
    }
    ...
}

このプログラムは、partyStarted シグナルを使用してパーティーの開始時刻を表示し、次のハッピーバースデーの詩を何度も繰り返し表示します。

Happy birthday to you,
Happy birthday to you,
Happy birthday dear Bob Jones,
Happy birthday to you!

外部オブジェクトの統合

extending-qml-advanced/advanced7-Foreign-objects-integration

出席者は、ただ歌詞をコンソールに出力するのではなく、色をサポートしたもっと派手な表示を使いたいと考えています。彼らはそれをプロジェクトに統合したいと考えていますが、現在のところ、QMLから画面を設定することはできません。これを解決するには、必要な型をQMLエンジンに公開し、そのプロパティをQMLで直接変更できるようにする必要があります。

表示の制御はThirdPartyDisplay 。このクラスは表示するテキストの内容や前景色、背景色を定義するプロパティを持っています。

class Q_DECL_EXPORT ThirdPartyDisplay : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QString content READ content WRITE setContent NOTIFY contentChanged FINAL)
    Q_PROPERTY(QColor foregroundColor READ foregroundColor WRITE setForegroundColor NOTIFY colorsChanged FINAL)
    Q_PROPERTY(QColor backgroundColor READ backgroundColor WRITE setBackgroundColor NOTIFY colorsChanged FINAL)
    ...
};

この型をQMLに公開するには、QML_ELEMENT を使ってエンジンに登録します。しかし、このクラスは変更するためにアクセス可能ではないので、QML_ELEMENT を単純に追加することはできません。この型をエンジンに登録するには、その型を外部から登録する必要があります。これがQML_FOREIGN 。ある型の中で他のQMLマクロと組み合わせて使われた場合、他のマクロはその型ではなく、QML_FOREIGN で指定された外部型に適用されます。

class ForeignDisplay : public QObject
{
    Q_OBJECT
    QML_NAMED_ELEMENT(ThirdPartyDisplay)
    QML_FOREIGN(ThirdPartyDisplay)
};

このようにして、BirthdayPartyはdisplayを持つ新しいプロパティを持つことになります。

class BirthdayParty : public QObject
{
    Q_OBJECT
    Q_PROPERTY(Person *host READ host WRITE setHost NOTIFY hostChanged FINAL)
    Q_PROPERTY(QQmlListProperty<Person> guests READ guests NOTIFY guestsChanged FINAL)
    Q_PROPERTY(QString announcement READ announcement WRITE setAnnouncement NOTIFY announcementChanged FINAL)
    Q_PROPERTY(ThirdPartyDisplay *display READ display WRITE setDisplay NOTIFY displayChanged FINAL)
    ...
};

そして、QMLでは、空想的な3番目のディスプレイ上のテキストの色を明示的に設定することができます。

BirthdayParty {
    display: ThirdPartyDisplay {
        foregroundColor: "black"
        backgroundColor: "white"
    }
    ...
}

BirthdayPartyのannouncement プロパティを設定することで、メッセージを印刷する代わりに、fancy displayに送信することができます。

void BirthdayParty::setAnnouncement(const QString &announcement)
{
    if (m_announcement != announcement) {
        m_announcement = announcement;
        emit announcementChanged();
    }
    m_display->setContent(announcement);
}

出力は、前のセクションと同じように何度もこのようになります。

[Fancy ThirdPartyDisplay] Happy birthday to you,
[Fancy ThirdPartyDisplay] Happy birthday to you,
[Fancy ThirdPartyDisplay] Happy birthday dear Bob Jones,
[Fancy ThirdPartyDisplay] Happy birthday to you!

QML オブジェクトタイプのデフォルトと親のプロパティの指定」、「グループ化されたプロパティ」、「付属プロパティの提供」、「プロパティ値のソース」、「外部タイプの登録」も参照してください

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