Qt 6における暗黙のインポートとQMLモジュール

本稿は「Implicit Imports vs. QML Modules in Qt 6」の抄訳です。
 
私が最後にQMLモジュールに関する論文を発表して以来、Qtのいくつかのバージョンがリリースされました。そのほとんどは、実際には今でも非常に有効なアドバイスですが、人々がしばしば誤解するいくつかの点を強調する必要があると感じています。
 

QMLでピクルスをチョコレートに変える

ご存知かもしれませんが、QMLドキュメントにはそれぞれ暗黙的なインポートが設定されています。同じディレクトリにある他のQMLドキュメントは、インポートなしで使用することができます。これは、記述する必要のある定型文の量を大幅に削減する、非常に便利な機能です。通常、QMLファイルが属するモジュール自体をインポートする必要はありません。

この機能は、暗黙のインポートによってファイルが属するモジュールが取得されない場合、明らかに有用性が低下します。ここであなたは「そんなことが起こり得るのか?」と私に尋ねるでしょう。私は「そんなことは起こりません!」と答えたいところですが、過去に戻って CMake API を制限することはできないため、起こり得ることを認めざるを得ません。明らかに、QML モジュールについて書いているとき、私はこのケースを考えていませんでした。どの例でも、このケースは実行されていません。

しかし、私よりも想像力豊かな人々は、本来の目的とは異なる方法で QML モジュール用の CMake API を使用する方法を知っていました。教育目的のために、次に何が起こるのかを見てみましょう。親の許可なしにこれを行ってはいけません。

それでは、QML モジュールをそのファイルの暗黙的なインポートと異なるものにする方法を説明します。

myProject
    | - CMakeLists.txt
    | - main.cpp
    | - some_qml_type.h
    | - some_qml_type.cpp
    | - qml
        | - main.qml
        | - Pickles.qml

上記のプロジェクト構造では、CMakeLists.txtでqt_add_qml_moduleを呼び出しているものとします。また、このように定義されたQMLモジュールには、main.qmlとPickles.qmlが含まれています。main.qmlとPickles.qmlの暗黙的なインポートは「qml」ディレクトリです。しかし、それらのモジュールは「myProject」ディレクトリで定義されています。ここで、main.qmlまたはPickles.qmlで、some_qml_type.hで宣言されたものを使用したい場合は、myProjectを明示的にインポートする必要があります。おめでとうございます!

なぜでしょうか? 理由は簡単です。qmldirファイルはmyProjectの1つ上のディレクトリにあるため、main.qmlとPickles.qmlはqmldirファイルを自分たちのディレクトリで見つけることができません。同じディレクトリにqmldirファイルがない場合、暗黙的なインポートは、そこに存在するQMLファイルのファイル名のみを使用して構築されます。この場合、C++定義の型を挿入する余地はありません。

また、CMakeLists.txtに次の行を追加すると、さらに楽しくなります。

set_source_files_properties(qml/Pickles.qml PROPERTIES
    QT_QML_SOURCE_TYPENAME Chocolate
)

これで、main.qml では、隣にある Pickles.qml で定義されている「Pickles」というコンポーネントを、これまでと同様に使用することができます。ただし、myProject をインポートした場合、「Pickles」というコンポーネントは取得できません。代わりに、同じ内容の「Chocolate」というコンポーネントが取得されます。myProject をインポートしていない main.qml では、この「Chocolate」は取得できません。素晴らしいと思いませんか?

しかし、まだ終わりではありません。名前付きモジュール経由でインポートされたシングルトンを参照する型名は、同じモジュール内の暗黙のインポート経由でアクセスされた場合は通常の型を参照する、ということをご存知でしたか? これを行う正確な方法は、読者(保護者の監視下にある)の練習問題として残しておきます。 ヒント:思ったほど簡単ではありません。 正しい解答をコメントに投稿した最初の1人に、ピクルスをお送りします。

もちろん、これらの効果はすべて機能です。お楽しみください。バグレポートで私を悩ませないでください。お楽しみはここまでです。真面目な部分に戻りましょう。

どうしてこのようなことになったのでしょうか?

悲しい真実ですが、公式のサンプルの多くは、まさに上記の構造を示しており、アプリケーションのルートと QML ファイルの間に「qml」という追加のディレクトリがあります。Qt5 では、適切な名前の QML モジュールを作成するのが難しかったため、コードを構造化し、QML ファイルを C++ コードから分離するには、これが良い方法でした。 CMake への移植の際、この構造の問題はしばらくの間、検出されませんでした。 現在では、この構造が皆さんのプロジェクトに徐々に浸透し、レガシーとして定着しています。

しかし、人々は意図的にこのようなサブディレクトリを作成することもあります。QMLモジュールに内部構造を持たせることは、大きなモジュールをより小さな部分に分割し、可読性を向上させることにも役立ちます。これらの部分は、定義上、別々のモジュールであるべきです。しかし、サブディレクトリを追加する方が、モジュールを分割するよりも初期コストが少なくて済みます。結局、CMakeLists.txtを余分に書く必要はなく、URIやモジュールが動的か静的か、リンク方法などを考える必要もありません。もし、上述のような状況に遭遇しないのであれば、大きなモジュールを分割するメリットはないかもしれません。

どのように修正すればよいでしょうか?

プロジェクト構造を変更して、QMLファイルの暗黙的なインポートが、それらが属するモジュールと同じになるようにするには、複数の方法があります。

  1. QMLファイルを1つ上の階層に移動し、「qml」ディレクトリを削除します。これは、最も簡単な方法です。しかし、QMLとC++コードを内部的に分離する、おそらくは理由があって作成したものを解消することにもなります。
  2. QMLモジュールをさらに定義します。トップレベルのディレクトリで「myProject」というQMLモジュールを定義している場合、「qml」サブディレクトリで「myProject.qml」というQMLモジュールを定義できます。「qml」ディレクトリに「CMakeLists.txt」をもう1つ追加し、「myProject」ディレクトリに「add_subdirectory(qml)」を追加するだけです。一方のモジュールで使用している型をもう一方でも使用したい場合は、明示的にインポートする必要があります。おそらく、明示的なインポートは、上述のあいまいさよりも望ましいでしょう(実際にそれを楽しんでいる場合を除きます)。複数のQMLモジュールを同じバイナリに含めることができます。
  3. 各ファイルで、常に自分のモジュールを明示的にインポートします。この場合、モジュールがインポートパスで見つかる必要がありますのでご注意ください。この場合(または他の場合でも)は、NO_RESOURCE_TARGET_PATHを使用しないでください。また、モジュールが既知のインポートパス(例:「:/qt/qml」またはアプリケーションディレクトリ)から利用可能であるか、または明示的に定義されたインポートパスから利用可能であることを確認する必要があります。
  4. 筆者のUlfが解決策を見つけるまで待ちましょう。これらの「qml」サブディレクトリは広く普及しているため、おそらくqt_add_qml_module()への追加引数、またはqt_standard_project_setupを使用して有効にできるポリシーを介して、中央でこれを修正する時が来たようです。いくつかのアイデアが浮上していますが、まだ明確な解決策はありません。この件の進捗状況については、QTBUG-111763を参照してください。

おわりに

この記事を書いている際に、次のことに気づきました。

  • モジュールにプラグインがなく、
  • そのコンポーネントの1つをエントリーポイントとして、URLベースのQQmlComponentコンストラクタを使用して、
  • この方法で読み込むQMLファイルがモジュールのベースディレクトリにある場合、

モジュールのC++定義の型は暗黙的なインポートから依然としてロードされません。これは明らかにバグです。https://codereview.qt-project.org/c/qt/qtdeclarative/+/541951で修正されています。


Blog Topics:

Comments