Qt/QML は C++ クラスを QML に公開し、setContextProperty が最良のアイデアではない理由

この記事では、C++ クラスを QML に公開するさまざまな方法について説明します。 QML は HTML/CSS のようなマークアップ言語 (QT フレームワークの一部) であり、(QT) アプリケーションの C++ コードと対話できるインライン JavaScript を備えています。 C++ クラスを QML に公開する方法は複数あり、それぞれに独自の利点と癖があります。このガイドでは、3 つの統合方法、qmlRegisterSingletonType<> について説明します。 、 rootContext->setContextProperty() そして qmlRegisterType<> .最初の 2 つの起動時間の違いを示す簡単なベンチマークで終わります。

エグゼクティブ サマリーは、setContextProperty です。 は推奨されておらず、パフォーマンスに影響があります (そして qmlRegisterSingletonType<> を使用する必要があります) . mybenchmarks では qmlRegisterSingletonType 1 つは setContextProperty より高速です .クラスのインスタンスが複数必要な場合は、qmlRegisterType<> を使用します オブジェクトを QML で直接インスタンス化します。qmlRegisterType 私のベンチマークでは、コンテキスト プロパティよりも高速です。

シングルトン メソッドは、特定のインスタンス (モデルやビューモデルなど) が必要な場合に最適な方法であり、registerType メソッドは、QML で多くのものをインスタンス化する必要がある場合に最適な方法です。ルート コンテキスト プロパティの設定には複数の問題があり、パフォーマンスはその 1 つです。また、名前の衝突の可能性、静的分析がなく、QML のどこにいても誰でも利用できます。 Qt バグ レポート (QTBUG-73064) によると、将来的に QML から削除される予定です。

はじめに

すべてが他のすべてのものと密接に結びついている絡み合ったごちゃまぜではなく、アプリケーションに明確な境界を設定することは、私の意見では望ましいことです。シングルトンまたは分離が可能な型では、不可能なルート コンテキスト プロパティを使用します。小規模なプロジェクトの場合、setContextProperty メソッドは問題ありませんが、シングルトン メソッドはそれほど手間がかからないので、その場合でもシングルトンを使用することをお勧めします。

Qt/QML のドキュメントは包括的ですが、私が見つけた 1 つの欠点は、フレームワークには (推奨される) 方法がないことです。すべてのメソッド パラメータと可能なオプションを見つけることができますが、Button{} のテキストの色を変更する方法を知りたい場合は、 C++ と QML の統合についても同様です。 Qt のドキュメントには、さまざまな統合方法の概要が記載されていますが、どれが最適かはわかりません。どの方法を使用するかを説明するフローチャートがありますが、オンラインのほとんどすべてのガイドと例では rootContext->setContextProperty() のみを使用しています。 .シグナルとスロットに関する私自身の記事でさえ、小規模なプロジェクトでは単純であるため、これを使用しています。

QML はドメインの知識を持つべきではありません。これは単なる UI マークアップ言語であるため、実際の作業やロジックは QML/JavaScript 経由ではなく、C++ 側で行う必要があります。 JavaScript を使用すると、非常に速く乱雑になり、単体テストではテストできません。そのため、JavaScript を使用することは私にとって大きな問題です。 WPF と同様 そしてXAML Microsoft 側では、ユーザー インターフェイスに viewModel へのバインディングがいくつかあるはずです。 独自のコードやロジックはありません。 QML で非常に複雑なステート マシン全体と複雑な JavaScript メソッドを見てきましたが、今でも悪夢に悩まされています。これらの関数はすべて C++ で実行でき、単体テストを使用してテストできます。そちらの方が速いに違いありません。

この記事を書いた理由は、QML での C++ 統合に関するさまざまなオプションに飛び込んでいたからです。職場では、最近、パフォーマンス上の理由から大量の QML コードをリファクタリングしましたが、1 つのグローバル コンテキスト プロパティを削除したことが非常に役立ちました。また、コードとアセットの多くに名前空間を設定し、Qt ドキュメントの欠落や誤りなど、複数の問題に遭遇しました。コードは静的アプリケーションとしてコンパイルされ、staticlib としてコンパイルされます ライブラリの場合、すべてのアセットを qrc に含めます ファイル。私の qmldir とほぼ一致する静的なコンパイルとファイルシステムのパス 名前 (大文字の不一致) と間違ったドキュメントが組み合わさると、多くの頭痛の種になりましたが、最終的にはすべて修正し、ユーザーが直面する応答時間が著しく増加したことを示しました。

このプロジェクトのサンプル ソース コードは、こちらの github にあります。

信号機 QML の例

信号機と、その信号機を制御するためのいくつかのボタンを備えた単純な QML の例を作成しました。 TrafficLightQml オブジェクトは、それぞれ異なる色の 3 つの円を含む長方形です。異なるランプをオンまたはオフにするために、3 つのプロパティが公開されています。これは opacity です bool で制御 、物事をシンプルに保つために。最良の例ではありませんが、これにはステートマシンが理想的ですが、この記事では単純にするために、これで十分だと判断しました。

TrafficLightQmlControlButtons 2 つのボタンを格納し、1 つのプロパティと 1 つのシグナルを公開します。プロパティには暗黙的に生成された onXXXChanged があるため、実際には 2 つのシグナルです。 信号。 1 つのボタンでライトをオンまたはオフにし、1 つのボタンでオランダの信号機が使用するパターンでさまざまなランプを切り替えます。

Red (stop) -> Green (go) -> Orange (caution, almost Red)

TrafficLight QML 自体の内部で関連する関数を呼び出す代わりに、プロパティとシグナルを公開するのはなぜですか?これは、QMLcontrol を C++ の対応するものと公開方法に密接に結び付けます。 QML コントロールを十分にジェネリックにすることで、いつでも好きなときに実装を交換できます。ユーザーインターフェイスは、それがどのように、いつ、どのように行うかではなく、どのように見えるか、何をするかを知る必要があるだけです。これにより、動作の単体テストがはるかに簡単になります。QML コントロールにはインテリジェンスがないため、それをテストする必要がないからです。フレームワークがシグナルとメソッドを渡す際に機能することを信頼する必要があります。コア ロジック (どのランプ パターン、いつオンまたはオフにするかなど) はユニット テストする必要があります。これは、たとえば Qt Test や GoogleTest で簡単に実行できます。 QML コントロール / JavaScript 関数のテストははるかに困難です。

main.qml ファイルにはこれら 2 つのコントロールの 4 つのインスタンスがありますが、それぞれのプロパティとシグナルは異なる C++ オブジェクトにバインドされています。そうすれば、main.cpp でどのように作成され、渡されるかを含め、それぞれの使用方法を明確に確認できます。 .

ファイル名とクラス名は非常に冗長で、何がいつどこで使用されているかがわかります。すべて (qml、c++、id) の名前が trafficlight の場合 、その可視性と洞察が失われます。これで、QML と C++ の両方で、どの行がどのコンポーネントに関連しているかが非常に明確になりました。

setContextProperty

最も一般的な例から始めましょう。ほとんどすべてのチュートリアルで使用されています。ベスト プラクティスに関する Qt の公式ドキュメントでも、セクション Pushing References to QML 、彼らは setContextProperty を使用します .

setContextProperty を使用する場合 、プロパティは、QML エンジンによってロードされたすべてのコンポーネントで使用できます。コンテキスト プロパティは、QML が読み込まれるとすぐに利用可能でなければならず、QML でインスタンス化できないオブジェクトに役立ちます。

私の信号機の例では、main.cpp で次のようになります。

TrafficLightClass trafficLightContext;
qmlRegisterUncreatableType<TrafficLightClass>("org.raymii.RoadObjectUncreatableType", 1, 0, "TrafficLightUncreatableType", "Only for enum access");
engine.rootContext()->setContextProperty("trafficLightContextProperty", &trafficLightContext);

(すべての) QML では、次のように使用できます:

Component.onCompleted: { trafficLightContextProperty.nextLamp(); // call a method } 
redActive: trafficLightContextProperty.lamp === TrafficLightUncreatableType.Red // use a property

インポートステートメントは必要ありません。この記事の後半に列挙型に関する段落があり、UncreatebleType について説明しています。 上記を参照してください。 QML 側でクラスの列挙型を使用する予定がない場合は、その部分をスキップできます。

このアプローチを使用して QML で C++ クラスを取得することには、今のところ本質的な問題はありません。小規模なプロジェクトやパフォーマンスが問題にならないプロジェクトの場合、コンテキスト プロパティは問題ありません。大まかに言えば、保守性などの機能について話していますが、小規模なプロジェクトの場合、大規模なコードベースや複数のチームが作業しているプロジェクトほど重要ではありません。

コンテキスト プロパティが悪いのはなぜですか?

シングルトンまたは registerType アプローチと比較して、いくつかの欠点があります。コンテキスト プロパティの将来の削除を追跡する Qt バグがあり、StackOverflow の投稿と QML コーディング ガイドは優れた要約を提供します。QML ドキュメントにもこれらの点が記載されていますが、あまり目立たない方法なので、要約はいいです.

Qt バグ (QTBUG-73064) の引用:

コンテキスト プロパティの問題は、それらが "魔法のように" 状態を QML プログラムに注入することです。 QML ドキュメントは、この状態が必要であると宣言していませんが、通常はこの状態がないと機能しません。コンテキスト プロパティが存在すると、それらを使用できますが、どのツールも、それらが追加された場所と削除された (または削除されるべき) 場所を適切に追跡できません。コンテキスト プロパティは QML ツールからは見えず、それらを使用するドキュメントを静的に検証することはできません。

QML コーディング ガイドの引用:

コンテキスト プロパティは常に QVariant を受け取ります または QObject 、これは、プロパティにアクセスするたびに再評価されることを意味します。これは、各アクセスの間にプロパティが setContextProperty() として変更される可能性があるためです いつでも使用できます。

コンテキスト プロパティはアクセスにコストがかかり、理由付けが困難です。 QML コードを書いているときは、コンテキスト変数 (直近のスコープには存在しないが、その上の変数) とグローバルな状態の使用を減らすよう努めるべきです。必要なプロパティが設定されている場合、各 QML ドキュメントは QMLscene で実行できる必要があります。

setContextProperty の問題に関する StackOverflow からのこの回答を引用 :

setContextProperty QML ツリーのルートノードにあるプロパティの値としてオブジェクトを設定するため、基本的には次のようになります。

property var myContextProperty: MySetContextObject {}
ApplicationWindow { ... }

これにはさまざまな意味があります:

  • 相互に「ローカル」ではないファイルへの相互ファイル参照を可能にする必要があります (main.cpp どこで使用しようとしても)
  • 名前は簡単に隠されます。コンテキスト プロパティの名前が別の場所で使用されている場合、解決に失敗します。
  • 名前解決の場合、可能な限り深いオブジェクト ツリーをクロールし、常に自分の名前のプロパティを探して、最終的にルートのコンテキスト プロパティを見つけます。これは少し効率が悪いかもしれませんが、おそらく大きな違いはありません。

qmlRegisterSingletonType 一方、必要な場所にデータをインポートできます。したがって、より高速な名前解決の恩恵を受ける可能性があります。名前のシャドウイングは基本的に不可能であり、透過的な相互ファイル参照はありません。

コンテキスト プロパティをほとんど使用してはならない理由をたくさん見てきました。次に、クラスの単一のインスタンスを QML に公開する方法に進みましょう。

qmlRegisterSingletonType<>

シングルトン タイプを使用すると、クライアントがオブジェクト インスタンスを手動でインスタンス化する必要なく、プロパティ、シグナル、およびメソッドを名前空間で公開できます。 QObject シングルトン型は、機能またはグローバル プロパティ値を提供するための効率的で便利な方法です。登録すると、QObject singleton タイプは、他の QObject と同様にインポートして使用する必要があります QML に公開されたインスタンス。

したがって、インポートする必要があることを除いて、基本的にコンテキスト プロパティと同じです。 QMLで。それが、私にとって、singletonsover コンテキスト プロパティを使用する最も重要な理由です。前の段落で、コンテキスト プロパティの相違点と欠点については既に述べたので、ここでは繰り返しません。

例の信号機コードでは、これは main.cpp の関連コードです。 :

TrafficLightClass trafficLightSingleton;
qmlRegisterSingletonType<TrafficLightClass>("org.raymii.RoadObjects", 1, 0, "TrafficLightSingleton",
                                     [&](QQmlEngine *, QJSEngine *) -> QObject * {
    return &trafficLightSingleton;
    // the QML engine takes ownership of the singleton so you can also do:
    // return new trafficLightClass;
});

QML 側では、使用する前にモジュールをインポートする必要があります:

import org.raymii.RoadObjects 1.0

使用例:

Component.onCompleted: { TrafficLightSingleton.nextLamp() // call a method }
redActive: TrafficLightSingleton.lamp === TrafficLightSingleton.Red; // use a property

UncreatableTypes で enum の奇妙さがない この場合。

qmlRegisterType

前の段落はすべて、単一の既存の C++ オブジェクトを QML に公開しました。ほとんどの場合、それで問題ありません。私たちは職場で models を公開しています。 と viewmodels この方法でQMLに。しかし、QML で C++ オブジェクトの複数のインスタンスを作成して使用する必要がある場合はどうすればよいでしょうか?その場合、qmlRegisterType<> を介してクラス全体を QML に公開できます。 、main.cpp の例では :

qmlRegisterType<TrafficLight>("org.raymii.RoadObjectType", 1, 0, "TrafficLightType");

QML 側では、再度インポートする必要があります:

import org.raymii.RoadObjectType 1.0

使用法は他の例と同様ですが、yourobject のインスタンスを作成する点が追加されています:

TrafficLightType {
    id: trafficLightTypeInstance1
}

TrafficLightType {
    id: trafficLightTypeInstance2
}

上記の例では、手動で作成して main.cpp でそのインスタンスを公開することなく、QML でその C++ タイプの 2 つのインスタンスを作成しました。 .使い方はシングルトンとほぼ同じです:

redActive: trafficLightTypeInstance1.lamp === TrafficLightType.Red; // use a property
Component.onCompleted: { trafficLightTypeInstance1.nextLamp() // call a method }

2 番目の例:

redActive: trafficLightTypeInstance2.lamp === TrafficLightType.Red; // use a property
Component.onCompleted: { trafficLightTypeInstance2.nextLamp() // call a method }

唯一の違いは ID、trafficLightTypeInstance1 です。 vs trafficLightTypeInstance2 .

qmlRegisterType でクラス全体を公開する これらすべてを C++ で手動で作成し、それらをシングルトンとして公開して、最終的に QML にインポートするよりもはるかに便利です。

setContextProperty と列挙型の奇妙さ

信号機クラスの例では、enum class があります。 LampState の場合 .ランプは Off にすることができます または3色のいずれか。型をシングルトンとして登録すると、ブール値評価による次の QML プロパティの割り当てが機能します:

redActive: TrafficLightSingleton.lamp === TrafficLightSingleton.Red

lamp 公開された Q_PROPERTY です 変更時に信号が添付されています。 Red enum class の一部です .

ただし、setContextProperty 経由で登録されたインスタンスで同じプロパティ ステートメントを使用する場合 、以下は機能しません:

redActive: trafficLightContextProperty.lamp === trafficLightContextProperty.Red

qrc:/main.qml:92: TypeError: Cannot read property 'lamp' of null のような漠然としたエラーが発生します プロパティが true に設定されることはありません。ゲッター関数を呼び出してQMLシグナルを使用するなど、さまざまな解決策を試しました(.getLamp() ) と Component.onCompleted() でのデバッグ . AQ_INVOKABLE クラスの debug メソッドは正常に動作しますが、enum 値は undefined を返します . .nextLamp() などのスロットへのその他の呼び出し 正常に動作しますが、列挙値のみにアクセスできません。

これはフローチャートとドキュメントに記載されていますが、それを知る前にイライラすることでしょう。

Qt Creator は値を認識しており、値を自動入力しようとしますが、エラー メッセージはまったく役に立ちません。私がそれらを使用できる場合、または役立つエラーメッセージを表示できる場合は、それらを自動入力しようとしないでください.Qt Creatorを開発する人への私の提案です.

これに対する解決策は、ドキュメントに記載されているように、クラス全体を UncreatableType として登録することです :

Sometimes a QObject-derived class may need to be registered with the QML
type system but not as an instantiable type. For example, this is the
case if a C++ class:

    is an interface type that should not be instantiable
    is a base class type that does not need to be exposed to QML
    **declares some enum that should be accessible from QML, but otherwise should not be instantiable**
    is a type that should be provided to QML through a singleton instance, and should not be instantiable from QML

作成できない型を登録すると、列挙値を使用できますが、TrafficLightType {} をインスタンス化することはできません QML オブジェクト。これにより、クラスを作成できない理由を提供することもでき、将来の参照に非常に便利です:

qmlRegisterUncreatableType<TrafficLight("org.raymii.RoadObjectType", 1, 0, "TrafficLightType", "Only for enum access");

QML ファイルで、タイプをインポートする必要があります:

import org.raymii.RoadObjectType 1.0

その後、比較で列挙値を使用できます:

redActive: trafficLightContextProperty.lamp === TrafficLightType.Red

型を登録するために余分な作業をすべて行っている場合は、シングルトン実装を使用しないでください。 enums を使用していない場合 setContextProperty() で逃げることができます 、 それでも。いつでもどこでも利用できるようにするのではなく、必要なときにだけ何かをインポートする方が、私にとってははるかに良いと感じています.

QML_ELEMENT ではない理由 / QML_UNCREATABLE / QML_INTERFACE / QML_SINGLETON ?

Qt 5.15 では、C++ を QML と統合するためにいくつかの新しいメソッドが利用可能になりました。これらは、ヘッダー ファイルのマクロと .pro の追加定義で機能します。 ファイル。

QML_ELEMENT / QML_UNCREATABLE / QML_INTERFACE / QML_SINGLETON / QML_ANONYMOUS

最新の 5.15 doc スナップショットとブログ投稿では、これらの方法が説明されています。これらの方法は、発生する可能性がある問題、つまり、C++ コードを QML 登録と同期させておく必要があるという問題を解決するはずです。ブログ投稿の引用:

次に、さらに (有効な) 技術的な詳細について説明します。

この比較にこれらを含めない理由は、それらが新しく、Qt 5.15 以降でのみ利用可能であり、.pro に依存しているためです。 ファイル、したがって qmake . Qt 6.0 でさえ、cmake のサポートは利用できません。

あなたのコードベースがこの最新の Qt 5.15 バージョンで実行できるほど新しい場合、または 6 以降を実行している場合、これらの新しい方法は上記の方法よりも優れています。なぜブログ投稿の技術的な部分を参照してください。可能であれば、Qt のバージョンとビルド システム (qmake ) で可能です。QML_SINGLETON を使用することをお勧めします そして友達。

qmlRegisterType<> と同じことを実現するための小さな例を書きました 以下参考までに。 .proCONFIG+= を余分に追加するファイル パラメータ(qmptypes ) と他の 2 つの新しいパラメーター:

CONFIG += qmltypes
QML_IMPORT_NAME = org.raymii.RoadObjects
QML_IMPORT_MAJOR_VERSION = 1    

あなたの .cpp で クラス、この場合は TrafficLightClass.h 、次を追加します:

#include <QtQml>
[...]
// below Q_OBJECT
QML_ELEMENT

qmlRegisterSingleton と同じ効果が必要な場合 、 QML_SINGLETON を追加 QML_ELEMENT の下 ライン。デフォルトで構築されたシングルトンを作成します。

QML ファイルで、登録済みの型をインポートします:

import org.raymii.RoadObjects 1.0

その後、それらをクラス名 (上記のように別の名前ではありません) で QML で使用できます:

TrafficLightClass {
    [...]
}

ベンチマークの起動時間

私たちが行っていることが実際に違いを生むかどうかを確認するために、簡単なベンチマークを作成しました。何かが高速であることを確認する唯一の方法は、プロファイルを作成することです。 Qt Profiler は独自のレベルにあるため、より簡単なテストを使用します。

シングルトン バリアントの方が遅いことが判明したとしても、前述と同じ理由で、グローバル プロパティよりもシングルトン バリアントを優先します。 (ご参考までに、このセクションはベンチマークを行う前に書いています。)

main.cpp の最初の行 現在のエポックをミリ秒単位で出力し、ルート ウィンドウの QML 側に Component.onCompleted を追加しました 現在のエポックもミリ秒単位で出力し、Qt.Quit を呼び出すハンドラ アプリケーションを終了します。これら 2 つのエポック タイムスタンプを差し引くと、起動時のランタイムが得られます。それを数回行い、qmlRegisterSingleton のみのバージョンの平均を取ります。 rootContext->setProperty() のみのバージョン .

ビルドは Qt Quick コンパイラが有効になっており、リリース ビルドです。他の QML コンポーネントは読み込まれず、終了ボタンもヘルプ テキストもありません。TrafficLightQML のウィンドウだけです。 そしてボタン。信号機 QML には、C++ ライトをオンにする onCompleted があります。

このベンチマークは単なる指標であることに注意してください。アプリケーションのパフォーマンスに問題がある場合は、Qt プロファイラーを使用して何が起こっているのかを把握することをお勧めします。 Qt のパフォーマンスに関する記事も参考になります。

main.cpp でのエポック タイムスタンプの出力 :

#include <iostream>
#include <QDateTime>
[...]
std::cout << QDateTime::currentMSecsSinceEpoch() << std::endl;

main.qml で出力する :

Window {
    [...]
    Component.onCompleted: {
        console.log(Date.now())
    }
}

grep の使用 およびタイムスタンプのみを取得する正規表現で、それを tac で逆にします (逆 cat )、次に awk を使用 2 つの数値を減算します。それを 5 回繰り返し、awk を使用します 再びミリ秒単位で平均時間を取得します:

for i in $(seq 1 5); do 
    /home/remy/tmp/build-exposeExample-Desktop-Release/exposeExample 2>&1 | \
    grep -oE "[0-9]{13}" | \
    tac | \
    awk 'NR==1 { s = $1; next } { s -= $1 } END { print s }'; 
done | \
awk '{ total += $1; count++ } END { print total/count }'
  • qmlRegisterSingleton<> の平均 例:420 ミリ秒

  • qmlRegisterType<> の平均 例:492.6 ミリ秒

  • rootContext->setContextProperty の平均 例:582.8 ミリ秒

上記のベンチマークを 5 回ループし、それらの平均を平均すると、シングルトンで 439.88 ミリ秒、registerType で 471.68 ミリ秒、rootContext プロパティで 572.28 ミリ秒になります。

この単純な例では、シングルトン変数で 130 ミリ秒から 160 ミリ秒の違いがすでに示されています。型を登録して QML でインスタンス化することさえ、コンテキスト プロパティよりも高速です。 (実際にそのような違いを期待していませんでした)

このベンチマークは、Raspberry Pi 4、Qt 5.15 で実行されました。実行中は、IceWM (ウィンドウ マネージャー) と xterm (ターミナル エミュレーター) 以外のアプリケーションは実行されていません。

このプロセスを私たちの作業アプリケーションで繰り返しました。この作業アプリケーションには、約 10 億億個のプロパティ バインディングを持つ非常に大きく複雑なオブジェクトがあり (実際の数は、リファクタリング時に自分で数えました)、2 秒以上の違いがありました。

ただし、絶対的な信頼できる情報源として上記の測定値を取得する前に、自分のマシンで独自のコードを使用していくつかのベンチマークを行ってください。

Qt Profiler を使用して起動時間を数回測定し、リスト全体を手動で掘り下げるよりも簡単に平均化する簡単な方法を知っている場合は、私にメールを送ってください。