QML モジュールに関する Qt バージョン 6.5 での新機能

本稿は「What’s new for QML Modules in 6.5」の抄訳です。

QML モジュール 以前から存在していましたが、Qt 6 より前のバージョンではあまり使われていませんでした。しかし新たに導入された qt_add_qml_module の登場により、現在では多く活用されています。これには正当な理由があります。qmllint や Qt Qucik Compilers といった QMLコードを高速化、効率化するためのツールはモジュールとして配置された QML ファイルに対してのみ正しく機能することができるのです。

利用が増える一方で、Qt の API を見たとき、Qt 自体が提供する API の一部に、モジュールをモジュールとして適切に扱うことができないものがありました。例えば QQmlComponent は追加の QML ファイルを読み込むためには明示的なファイルパスを指定する必要があります。6.5 以降では、この従来の方法に加えモジュールとして QMLを読み込むための新たな方法が提供されるようになります。この新たな方法は、このブログが紹介することの一つです。また、これまでモジュールを扱うためにはインポート パスを考慮し、調整する必要がありましたが、この状況の多くを解消する新たなソリューションについてもこのブログの中で紹介します。

新しくなったデフォルトのインポートパス

まずは追加された新しいデフォルトのインポートパスについて説明しましょう。6.5 からは作成したモジュールを qrc:/qt/qml 配下に配置することができます。そしてここに配置されたモジュールは自動的に検出されます。従来モジュールをリソースシステムに配置する場合は、保存したパスを addImportPath を呼び出して追加するか、モジュールを qrc:/qt-project.org/imports の配下に配置する必要がありました。これらの手法の中で後者は避けるべき方法です。qrc:/qt-project.org/imports はシステム全体で使用されることが期待される Qt 独自のモジュールとライブラリのみを対象としたパスだからです。また前者はライブラリでは簡単に行うことができず、ライブラリのすべてのユーザーに最初にこの呼び出し行うことを強制することもあまり良い解決策ではありませんでした。この状況を改善するために、デフォルトのインポートパスとして qrc:/qt/qml が新たに追加されました。アプリケーションとライブラリは今後モジュールをこのパスに配置することをお勧めします。


NOTE:

アプリケーションにおいて、メイン要素と qmldir がすべて一つのモジュールに集約されており、同じフォルダーの中に配置されている場合、リソースシステム内での明示的なパス指定でメインの要素をロードし、暗黙的なインポートパスの依存関係を利用してモジュールの qmldir を見つけさせることで、インポートパスの問題を回避することもできます。しかしこのソリューションにも独自の欠点もあります。またライブラリにおいてはこの方法はうまく機能しないということもあります。


CMake での統合

残念ながら、qt_add_qml_module ではすでに / をリソースを配置する際のプリフィックスとして使用しており、このデフォルトの振る舞いについては勝手に変更することはできません。幸いなことに、Qt 6.5では Qt CMakeポリシーもサポートするようになり、これにより Qt が提供する CMake APIのデフォルトの振る舞いを変更することが可能になっています。qt_add_qml_module のリソースファイルのプリフィックスについて QTP0001 を有効にすることで /qt/qml に変更することができます。このポリシーを有効にする最も簡単な方法は、CMakeLists.txt の中で次の呼び出しを行うことです。

qt_standard_project_setup(REQUIRES 6.5)

qt_standard_project_setupREQUIRES 6.5 を渡すと、Qt 6.5 までに導入されているすべての新しいポリシーがグローバルで有効になります。6.5での新たなものは現在のところリソースプリフィックスに関するものだけです。ポリシーの制御の詳細については、Qt CMake ポリシーに関するドキュメントqt_policy コマンドのドキュメントを参照してください。

qmake の場合

qmake を使用しているプロジェクトの場合は、RESOURCES変数 にプリフィックスを設定することで、新しいデフォルトのインポートパスに QML を配置させることができます。

QML_IMPORT_NAME = MyModule
QML_IMPORT_MAJOR_VERSION = 1

SOURCES += \
    main.cpp

HEADERS += \
    filesystemmodel.h

qml_resources.files = \
    qmldir \
    Main.qml \
    MyType.qml

qml_resources.prefix = /qt/qml/MyModule

RESOURCES += qml_resources

これにより、実行時に追加のセットアップを行わなくても、エンジンは MyModule モジュールを検出することができるようになります。

モジュール対応 API の追加

QMLモジュールに関連する2番目の実質的な変更は、QMLと C++ の間での QML を扱うための API に関する変更です。新たに QMLモジュールに関連したAPI が追加されました。

コンポーネントの読み込み: これまでの方法

新しい APIを見ていく前に、まず単純なQMLアプリケーションのプロジェクトを例にこれまでどのようにQMLを扱ってきていたかを見てみましょう。例として次のディレクトリ構造を見てみます。

helloqml
├── CMakeLists.txt
├── main.cpp
└── main.qml

これを扱う CMakeLists.txt は例えば次のようになります。

cmake_minimum_required(VERSION 3.21)

project(helloqml VERSION 0.1 LANGUAGES CXX)

find_package(Qt6 6.5 COMPONENTS Quick REQUIRED)

qt_standard_project_setup(REQUIRES 6.5)

qt_add_executable(helloqmlapp
    main.cpp
)

qt_add_qml_module(helloqmlapp
    URI helloqml
    QML_FILES main.qml 
)

target_link_libraries(helloqmlapp
    PRIVATE Qt6::Quick)

ここではすでに紹介した新しいデフォルトのインポートパスがすでに適用されていることに注意してください。QML を読み込む main.cpp は例えば次のようになります。

#include <QGuiApplication>
#include <QQmlApplicationEngine>

using namespace Qt::Literals::StringLiterals;

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

    QQmlApplicationEngine engine;
    const QUrl url(u"qrc:/qt/qml/helloqml/main.qml"_s);
    QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed,
                     &app, []() { QCoreApplication::exit(-1); },
                     Qt::QueuedConnection);
    engine.load(url);

    return app.exec();
}

そして最後に、main.qml は例えば次のような内容です

import QtQuick

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Hello World")
}

興味深いのは const QUrl url(u"qrc:/qt/qml/helloqml/main.qml"_s); の行です。ここにこのコードに潜む複数の落とし穴を確認することができます。まず1つ目は main.qmlについて、これは helloqml というモジュールの構成物の一つとして CMakeLists.txt の中で定義されているため helloqml というフォルダーの中に格納されているということになります。2つめは  QML を正しく読み込むためにリソースシステム内でのQMLの保存先である qrc:/qt/qml/ をパスに含めなければならないという点です。これにより QML ファイルは実行時にファイルシステムから読み込まれ、インタープリタで実行されるのではなく、事前のコンパイル処理を経たリソースシステム内の QMLファイルを実行させることができるわけですが、一方でこのコードは直接 qrc のパスを記述しているので、例えば開発作業の場面など、変更したコードが確認できるまでの時間短縮のため、QMLのコンパイルをさせず、QMLをファイルシステムから直接読み込ませ、インタープリタで実行させたいといったような要件に応えることができません。Qt 6.5 ではこの問題を解決するために新しい方法を提供します。

 

コンポーネントの読み込み: 新しいアプローチ

QMLモジュールが内包する QML要素はそのモジュールの名前とタイプ名によって特定できるようになっています。このルールを先ほどのコードに適用してみましょう。まず main.qml の名前を Main.qml に変更します ( デフォルトではファイル名の先頭の文字が大文字のファイルのみが公開されたQMLタイプとして解釈されるため、main.qml をモジュールの中のQMLタイプとして識別されるように m を大文字にします)。そして次に main.cpp の中で engine.load(url); と処理しているところを engine.loadFromModule("helloqml", "Main"); と書き換えます。これで完了です。これによりファイルが正確にどこにあるかを気にする必要がなくなり、リソースファイルシステムからの読み込みはバックグラウンドで処理されます。loadFromModule が可能にすることは load(QUrl) の呼び出しを置き換えということだけに留まりません。以前は不可能だったいくつかのことを可能にします。まず ロードするQML要素の起源がファイルである必要がなくなり、内部的に作成されるあらゆる QML要素にも対応できるようになりました。例えば以下が QML の要素の例です

  • ファイルで定義された要素 (これまで見てきたものです)

  • インラインコンポーネント: 例えば次のように定義された Outer 要素があった場合

    // Outer.qml, in module MyModule
    Item {
        component Inner : Rectangle { color: "red" }
    }
    そのインラインコンポーネント Inner を次のようにしてロードすることができます。

    engine.loadFromModule("MyModule", "Outer.Inner")

  • C++で定義された型: 例えば次のように定義された C++要素があった場合

    // part of MyModule
    struct MyFancyItem : public QQuickItem
    {
      QML_ELEMENT
      // ...
    }

    この MyFancyItem 要素を engine.loadFromModule("MyModule", "MyFancyItem")として生成させることができます。

以前は、最後の2つのケースは、目的の要素をインスタンス化したラッパーの QML ファイルを作成する必要がありました。この機能追加は QQmlApplicationEngine に限ったことではありません。QQmlComponent にも QQmlComponent::loadFromModule という形で同等のAPIが新たに追加されました。また、Qt.createComponent にも新しいオーバーロードが追加され、これは QMLでも利用することができます:

import QtQuick
ListView {
    model: 10
    delegate: Qt.createComponent("MyModule", "MyFancyItem")
}
シングルトン

これまで紹介してきた API は、新しいオブジェクトを作るためのものでしたので、シングルトンとして定義された QML には不向きです。シングルトンについては既存の API との一貫性を保つ形で QQmlEngine::singletonInstance に対する新しいオーバーロードが追加されました。これは、例えば QAbstractItemModel を公開したり、ネットワークから取得したアプリケーションの設定データを扱えるようにするなど、アプリケーションのグローバルデータをアプリケーション内で扱えるようにするときなどに便利です。例えば C++1 で定義された次のようなシングルトンがあった場合

class Globals : public QObject
{
   Q_OBJECT
   QML_ELEMENT
   QML_SINGLETON
   
   Q_PROPERTY(QAbstractItemModel *model READ model WRITE setModel NOTIFY modelChanged)
   //...
}

次のようにしてそのシングルトンのインスタンスを C++で作成、初期化することができます。

QQmlApplicationEngine engine;
// access the singleton
auto globals = engine.singletonInstance<Globals>("MyModule", "Globals");
// set up global state
globals.setModel(fancyModel);
// start the application after the initial setup
engine.loadFromModule("MyModule", "Main")

なお、以前の Qt バージョンでも、qmlTypeId と QQmlEngine::singletonInstance の id ベースのオーバーロードを組み合わせて使用することで、同様のことを行うことは可能でした。この新しい関数には、2つの利点があります。それは、コードの量がわずかながら少なくなり、singletonInstance2 の呼び出しが1回だけ必要な場合において、わずかながら速くなることです。

将来の展望

今回のモジュール関連の機能の追加により、QMLモジュールを扱う作業がより簡単になることを期待しています。また、QML要素を読み込む機能の追加は、QML Type Compiler ( C++ から QML をより効率的に扱うことを可能にするコンパイル技術 ) とのシームレスな統合に一歩近づいたとも言えます。現在、Type Compiler を使用するには、アプリケーションのエントリーポイントを変更する必要があります。将来的には、様々な loadFromModule 関数が、qmltc でコンパイルされた型を透過的にサポートできるようになる予定です。今後のアップデートにご期待ください!


  1. この方法は QML で定義されたシングルトンでも有効です

  2. 繰り返しシングルトンへのアクセスが必要で、シングルトンそのものではキャッシュすることができず、シングルトンID だけが常に保持できる場合、シングルトンID からシングルトンオブジェクトを取得する従来の方法のほうがシングルトンの型を指定する今回の方法より、都度オブジェクトを探索が行われる必要がない分高速です


Blog Topics:

Comments