QtQuick の ListModel を継承して使いたい

はじめに

ListModelListElement はよく使うエレメントですが、実は Qt Quick の中では一風変わったエレメントだということに気づいたことはありませんか?

一般的な QML のエレメントは「あらかじめ定義されているプロパティを設定する記述をする」もしくは「自分でプロパティの定義を記述する」ような使い方をしますが、ListElement は「プロパティの定義をせずに、プロパティの記述に似た文法で role/value のセットを記述できる(何言っているかわからない)」素敵なエレメントです。

ListModel も謎の性質があり、一般的な QML のエレメントでは、エレメント直下の子エレメントは、親エレメントの「リスト型の デフォルトのプロパティ に対する値の設定」となるのですが、ListModel には ListElement を受け付けるようなプロパティはありません

ListModel は特別なパーサーを利用している

ListModel 自体は、Qt で一般的にリスト形式のモデルを扱う際に使われる QAbstractListModelサブクラス となっています。

そのクラスを QML 側で利用できるようにする際に、以下のように 専用のパーサーを利用するように設定している ため、前述したような、一般的な記述方法で特殊な処理をするしくみになっています。

qmlRegisterCustomType<QQmlListModel>(uri, 2, 0, "ListModel", new QQmlListModelParser);

ListModel の派生エレメントを作成すると怒られる

というわけで、ListModel は特殊なパーサーを採用しているため、QML 側で派生クラスを作成すると(その場合、専用のパーサーが効かなくなるので)エラーが発生してしまいます。

// AbstractMyListModel.qml
import QtQml.Models 2.15

ListModel {
    ListElement { key: 'key1'; value: 'value1' }
}
// MyListModel.qml
import QtQml.Models 2.15

AbstractMyListModel {
    ListElement { key: 'key2'; value: 'value2' }
}
// main.qml
import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 640
    height: 480
    visible: true
    ListView {
        anchors.fill: parent
        model: MyListModel {}
        delegate: Text { text: '%1: %2'.arg(model.key).arg(model.value) }
    }
}
file:///.../main.qml:10 Type MyListModel unavailable
file:///.../MyListModel.qml:4 Cannot assign to non-existent default property

どうしてもこうしたい場合の対応案(1)

派生クラスの Component.onCompleted で JSON 形式のデータを append() することで、実現可能です。

が、かっこ悪いです。

どうしてもこうしたい場合の対応案(2)

すこし工夫をして、同じ ような 記述ができるようにしてみました。

// MyElement.qml
import QtQml 2.15

QtObject {
    property string key
    property var value
}
// AbstractMyListModel.qml
import QtQml 2.15
import QtQml.Models 2.15

ListModel {
    id: root
    property list<MyElement> elements: [
        MyElement { key: 'key1'; value: 'value1' }
    ]
    default property alias elements2: root.elements

    Component.onCompleted: {
        for (var i = 0; i < elements.length; i++)
            root.append(elements[i])
    }
}
// MyListModel.qml
AbstractMyListModel {
    MyElement { key: 'key2'; value: 'value2' }
}

ListElement 相当のエレメント MyElement を、QtObject の派生クラスとして定義します。

(ListElement の記述の柔軟性を犠牲にしています。また、各項目毎に QtObject を生成することによりメモリの消費量とパフォーマンスも犠牲になっています)

AbstractMyListModel 内で、MyElement を受け取れるよう、デフォルトのプロパティを作成し、そのエレメントおよび、派生エレメントで MyElement を普通の書き方で記述していきます。

AbstractMyListModel のコンストラクタで、定義済みの MyElement を ListModel のデータとして登録しています。

main.qml は前述のものと同じものを利用可能です。

できました!

おわりに

複雑なシステムを構築する際に UI の定義などを継承して扱いたい場合があり、せっかく QML で書いているのだから QML らしく書きたいなーということで、いくつかの制限や犠牲はありつつも実現できました。

ソースコードは https://github.com/task-jp/qtquick–listmodel-sub-element から入手できますので、是非お試しください。