QMLの基礎 - モデルとビュー

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

ソフトウェアはデータを生成して、データを表示する必要がある。
Qt Quickには、データを表示するためのモデル、ビュー、デリゲートという概念が存在する。

モデル、ビュー、デリゲートは、データの可視化をモジュール化し、開発者がデータの様々な側面をコントロールできる。
また、データにほとんど変更を加えることなく、リストビューとグリッドビューを入れ替えることができる。
同様に、データのインスタンスをデリゲートにカプセル化することで、データの表示方法や処理方法を決定することができる。

  • モデル
    データとその構造を含む。
    モデルを作成するためのQMLタイプがいくつかある。

  • ビュー
    データを表示するコンテナである。
    ビューは、リストやグリッドでデータを表示する。

  • デリゲート
    データがビューにどのように表示されるかを指定する。
    デリゲートはモデルの各データを受け取り、それをカプセル化する。データはデリゲートを通してアクセスできる。
    デリゲートは、データを編集可能なモデルに書き戻すこともできる。(例. TextFieldonAcceptedハンドラ内)
    データを可視化するには、ビューのmodelプロパティをモデル、delegateプロパティをコンポーネントやその他の互換性のあるタイプにバインドする。


QML Model and View 1.png



ビューによるデータの表示

ビューは、アイテムのコレクションのためのコンテナである。
豊富な機能を持ち、スタイルや動作の要求に合わせてカスタマイズすることができる。

Qt Quickの基本セットには、以下に示す標準的なビューが用意されている。

  • ListView
    水平または垂直のリストにアイテムを配置する。

  • GridView
    利用可能なスペース内のグリッドにアイテムを配置する。

  • PathView
    アイテムをパス上に配置する。


上記のビューには、それぞれ専用のプロパティと動作がある。

ビューの装飾

ビューでは、headerfootersection等の装飾用プロパティを使用して、見た目をカスタマイズすることができる。

これらのプロパティに対して、オブジェクト(通常は別のビジュアルオブジェクト)をバインドすることにより、ビューを装飾することができる。
footerには、ボーダーを表示するRectangleアイテムや、ビューの上にロゴを表示するヘッダ等がある。

以下の例では、あるクラブがメンバーリストを自社のブランドカラーで装飾している。
メンバーリストはモデルの中にあり、デリゲートはモデルのコンテンツを表示している。

クラブは、ビジュアルアイテムをheaderプロパティとfooterプロパティにバインドすることにより、メンバーリストを装飾することができる。
ビジュアルアイテムは、同一ファイル、他のファイル、Componentアイテムで定義することができる。

 ListModel {
    id: nameModel
    ListElement { name: "Alice" }
    ListElement { name: "Bob" }
    ListElement { name: "Jane" }
    ListElement { name: "Harry" }
    ListElement { name: "Wendy" }
 }
 
 Component {
    id: nameDelegate
    Text {
       text: name;
       font.pixelSize: 24
    }
 }
 
 ListView {
    anchors.fill: parent
    clip: true
 
    model: nameModel
    delegate: nameDelegate
 
    header: bannercomponent
 
    footer: Rectangle {
       width: parent.width
       height: 30;
       gradient: clubcolors
    }
 
    highlight: Rectangle {
        width: parent.width
        color: "lightgray"
    }
 }
 
 Component {  // instantiated when header is processed
    id: bannercomponent
    Rectangle {
       id: banner
       width: parent.width
       height: 50
       gradient: clubcolors
 
       border {
          color: "#9EDDF2"
          width: 2
       }
 
       Text {
          anchors.centerIn: parent
          text: "Club Members"
          font.pixelSize: 32
       }
    }
 }
 
 Gradient {
    id: clubcolors
 
    GradientStop {
       position: 0.0
       color: "#8EE2FE"
    }
 
    GradientStop {
       position: 0.66
       color: "#7ED2EE"
    }
 }


QML Model and View 2.png


マウスとタッチの操作

ビューは、コンテンツのドラッグやフリックを処理するが、個々のデリゲートとのタッチ操作は処理しない。

デリゲートがタッチ入力に反応してcurrentIndexプロパティを設定するためには、適切なタッチ処理を持つMouseAreaアイテムをデリゲートに定義する必要がある。

※注意
highlightRangeModeプロパティがStrictlyEnforceRangeに設定されている場合、ビューは常にcurrentIndexプロパティが指定されたハイライト範囲内にあることを保証するため、
currentIndexプロパティはビューをドラッグおよびフリックしても影響を受けない。

ListViewのセクション

ListViewのコンテンツはセクションにグループ化されるため、関連するリストアイテムは、そのセクションに応じてラベル付けされる。
また、セクションは、デリゲートを使用して装飾することもできる。

以下の例では、ListViewに、人名とチームの所属を示すリストを作成している。

ListViewには、隣接するアイテムや関連するアイテムをセクションにまとめることができるpropertyプロパティが存在する。
propertyプロパティは、どのリストタイプのプロパティをセクションとして使用するかを決定する。

cliteriaプロパティは、セクション名の表示方法を決定する。
delegateプロパティは、ListView等のdelegateプロパティと同様である。

 ListModel {
    id: nameModel
    ListElement { name: "Alice"; team: "Crypto" }
    ListElement { name: "Bob"; team: "Crypto" }
    ListElement { name: "Jane"; team: "QA" }
    ListElement { name: "Victor"; team: "QA" }
    ListElement { name: "Wendy"; team: "Graphics" }
 }
 
 Component {
    id: nameDelegate
    Text {
       text: name;
       font.pixelSize: 24
       anchors.left: parent.left
       anchors.leftMargin: 2
    }
 }
 
 ListView {
    anchors.fill: parent
    model: nameModel
    delegate: nameDelegate
    focus: true
 
    highlight: Rectangle {
       color: "lightblue"
       width: parent.width
    }
 
    section {
       property: "team"
       criteria: ViewSection.FullString
       delegate: Rectangle {
          color: "#b0dfb0"
          width: parent.width
          height: childrenRect.height + 4
          Text {
             anchors.horizontalCenter: parent.horizontalCenter
             font.pixelSize: 16
             font.bold: true
             text: section
          }
       }
    }
 }


QML Model and View 3.png



ビューのデリゲート

ビューは、リストの中のアイテムを視覚的に表現するために、デリゲートが必要となる。

ビューは、デリゲートにより定義されたテンプレートにしたがって、各アイテムのリストを生成する。
モデル内のアイテムは、アイテムのプロパティだけでなく、インデックスのプロパティにもアクセスできる。

QML Model and View 4.png


 Component {  // delegate: <アイテム名> { を使用することもできる
    id: petdelegate
 
    Text {
       id: label
       font.pixelSize: 24
       text: index === 0 ? type + " (default)" : type
 
       required property int index
       required property string type
    }
 }



デリゲートからのビューやモデルへのアクセス

デリゲートがバインドされているビューは、viewプロパティを介してデリゲートからアクセスできる。(例. ListView.view, GridView.view等)
したがって、対応するモデルとそのプロパティは、view.modelを通じて使用できる。(例. ListView.view.model, GridView.view.model等)

さらに、モデルに定義されているシグナルやメソッドもアクセス可能である。
例えば、複数のビューに同じデリゲートを使用したいが、装飾等の機能は各ビューごとに異なるものにする場合や他の異なる設定を各ビューのプロパティにする場合に活用できる。
同様に、モデルの各プロパティにアクセスおよび表示することもできる。

以下の例では、デリゲート内の1つのTextアイテムは、ListModel内のlanguageプロパティを表示している。
また、デリゲート内の1つのTextアイテムの色は、ListView内のfruit_colorプロパティに依存している。

 Rectangle {
    width: 200
    height: 200
 
    ListView {
       model: fruitModel
       delegate: fruitDelegate
       anchors.fill: parent
 
       property color fruit_color: "green"
    }
 
    ListModel {
       id: fruitModel
       property string language: "en"
 
       ListElement {
          name: "Apple"
          cost: 2.45
       }
 
       ListElement {
          name: "Orange"
          cost: 3.25
       }
 
       ListElement {
          name: "Banana"
          cost: 1.95
       }
    }
 
    Component {
       id: fruitDelegate
 
       Row {
          id: fruit
 
          Text {
             text: " Fruit: " + name
             color: fruit.ListView.view.fruit_color
          }
 
          Text {
             text: " Cost: $" + cost
          }
 
          Text {
             text: " Language: " + fruit.ListView.view.model.language
          }
       }
    }
 }



モデル

データは、デリゲートがバインドすることができる名前付きのデータロールを介してデリゲートに提供される。

以下の例では、typeとageという2つのロールを持つListModel、および、これらのロールにバインドして値を表示するデリゲートを持つListViewを作成している。

 import QtQuick 2.15
 
 Item {
    width: 200
    height: 250
 
    ListModel {
       id: myModel
 
       ListElement {
          type: "Dog"
          age: 8
       }
 
       ListElement {
          type: "Cat"
          age: 5
       }
    }
 
    Component {
       id: myDelegate
       Text { text: type + ", " + age }
    }
 
    ListView {
       anchors.fill: parent
       model: myModel
       delegate: myDelegate
    }
 }


ロールのアクセスをより細かく制御する場合、および、デリゲートをビュー外部で使用する場合は、以下のように記述する。

 required property <データ型> <プロパティ名>


デリゲートに必須プロパティが含まれている場合、名前付きのロールは提供されない。
代わりに、QMLエンジンは、まず、必須プロパティ名がモデルロールの名前と一致するかどうかをチェックして、一致する場合、その必須プロパティはモデルの対応する値にバインドされる。

 import QtQuick 2.15
 
 Item {
    width: 200
    height: 250
 
    ListModel {
       id: myModel
 
       ListElement {
          type: "Dog"
          age: 8
          noise: "meow"
       }
 
       ListElement {
          type: "Cat"
          age: 5
          noise: "woof"
       }
    }
 
    component MyDelegate : Text {
       required property string type
       required property int age
 
       text: type + ", " + age
 
       // 間違った記述: Component.onCompleted: () => console.log(noise)
       // 必須プロパティであるnoiseがないため、ReferenceErrorが発生する
       // 必要プロパティが存在することで、スコープにノイズが注入されるのを防ぐことができる
    }
 
    ListView {
        anchors.fill: parent
        model: myModel
        delegate: MyDelegate {}
    }
}


モデルのプロパティとデリゲートのプロパティの間に名前の衝突がある場合、代わりに修飾されたモデル名でロールにアクセスすることができる。

例えば、モデル内とデリゲート内の両方に同名のプロパティが存在する時、
デリゲート内のプロパティではなく、モデル内のプロパティを参照する場合、<モデルのID名>.<プロパティ名>と記述することにより参照できる。

また、モデル内のインデックスを含む特別なインデックスロールもデリゲートで使用できる。

※注意
このインデックスは、アイテムがモデルから削除されると-1に設定されることに注意すること。
インデックスロールにバインドする場合、インデックスが-1になる可能性、つまり、アイテムが有効でなくなる可能性を考慮したロジックにする。
(通常、アイテムはすぐに破棄されるが、一部のビューではdelayRemoveプロパティによりデリゲートの破棄を遅らせることができる)

名前付きロールを持たないモデル(例. ListModel等)は、モデルデータロールによりデータが提供される。
モデルデータロールは、1つのロールしか持たないモデルにも提供される。この場合、モデルデータロールには、名前付きロールと同じデータが含まれる。

※注意
モデルロール、モデルデータロール、インデックスロールにおいて、デリゲートが必須プロパティを含んでいる場合、プロパティ名が一致しなければアクセスできないことに注意する。

QMLには、いくつかのデータモデルが用意されており、それらはQMLの組み込み型に含まれている。
加えて、モデルはQt C++で作成することができ、そのモデルをQQmlEngineクラスを使用してQMLコンポーネントから利用することもできる。

モデルの作成については、QMLの基礎 - モデルの作成QMLの基礎 - カスタムQMLタイプを参照すること。

モデルからアイテムを配置する場合は、Repeaterを使用することで実現できる。

ListModel

ListModelは、QMLで指定された単純な型の階層である。
利用可能な役割は、ListElementプロパティで指定する。

 ListModel {
    id: fruitModel
 
    ListElement {
       name: "Apple"
       cost: 2.45
    }
 
    ListElement {
       name: "Orange"
       cost: 3.25
    }
 
    ListElement {
       name: "Banana"
       cost: 1.95
    }
 }


上記のListModelは、nameロールとcostロールの2つのロールを持っている。
これらは、例えば、ListViewのデリゲートでバインドすることができる。

 ListView {
    anchors.fill: parent
    model: fruitModel
 
    delegate: Row {
       Text {
          text: "Fruit: " + name
       }
 
       Text {
          text: "Cost: $" + cost
       }
    }
 }


ListModelには、JavaScriptからListModelを直接操作するためのメソッドが用意されている。
この時、最初に挿入されたアイテムが、モデルを使用している全てのビューで利用可能なロールを決定する。

例えば、空のListModelがあり、JavaScript経由で入力された場合、最初の挿入によって提供されたロールがビューに表示される唯一のロールとなる。

以下の例では、初めてMouseAreaの場所が押下する時、fruitModel(ListModel)にはcostロールとnameロールの2つのロールが追加される。
次のロールが追加された場合も、モデルを使用するビューでは、最初の2つのロールのみが処理される。

モデルで利用可能なロールをリセットするには、ListModel::clearメソッドを呼び出す必要がある。

 ListModel {
    id: fruitModel
 }
 
 // ...略
 
 MouseArea {
    anchors.fill: parent
 
    onClicked: {
       fruitModel.append({"cost": 5.95, "name":"Pizza"})
    }
 }


XmlListModel

XmlListModelでは、XMLデータソースからモデルを構築することができる。
ロールは、XmlRole型で指定する。

XmlListModelは、以下のモジュールをインポートする必要がある。

 import QtQuick.XmlListModel 2.15


以下の例では、タイトル、リンク、説明の3つのロールを持っている。
queryプロパティは、XmlListModelがXMLドキュメント内の各<item>エレメントに対して、モデルアイテムを生成することを指定する。

 XmlListModel {
    id: feedModel
    source: "http://rss.news.yahoo.com/rss/oceania"
    query: "/rss/channel/item"
 
    XmlRole {
       name: "title"
       query: "title/string()"
    }
 
    XmlRole {
       name: "link"
       query: "link/string()"
    }
 
    XmlRole {
       name: "description"
       query: "description/string()"
    }
 }


RSSニュースのデモでは、XmlListModelを使用してRSSフィードを表示する方法を記載している。

ObjectModel

ObjectModelは、ビューで使用されるビジュアルアイテムを含んでいる。
ObjectModelをビューで使用する場合、ObjectModelには既に視覚的なデリゲート(アイテム)が含まれているため、ビューはデリゲートを必要としない。

以下の例では、3つの色付きの長方形をListViewに配置している。

 import QtQuick 2.15
 import QtQml.Models 2.15
 
 Rectangle {
    ObjectModel {
       id: itemModel
 
       Rectangle {
          height: 30
          width: 80
          color: "red"
       }
 
       Rectangle {
          height: 30
          width: 80
          color: "green"
       }
 
       Rectangle {
          height: 30
          width: 80
          color: "blue"
       }
    }
 
    ListView {
       anchors.fill: parent
       model: itemModel
    }
 }


整数型モデル

整数は、ある数の型を含むモデルとして使用できる。
この場合、モデルにはデータの役割はありません。

以下の例では、5つの要素を持つListViewを作成している。

※注意
整数モデルのアイテム数の制限は、100,000,000であることに注意すること。

 Item {
    width: 200
    height: 250
 
    ListView {
       anchors.fill: parent
 
       model: 5
       delegate: itemDelegate
    }
 
    Component {
       id: itemDelegate
 
       Text {
          text: "I am item number: " + index
       }
    }
 }


C++データモデル

C++でモデルを定義して、それをQMLで使用することができる。
この仕組みは、既存のC++データモデルや複雑なデータセットをQMLに公開するのに便利である。

詳細は、Using C++ Models with Qt Quick Viewsの記事を参照すること。


Repeater

Repeaterは、モデルのデータをもとに、ポジショナで使用するアイテムをテンプレートから作成する。
Repeaterとポジショナを組み合わせることで、多くのアイテムを簡単に並べることができる。

Repeaterはポジショナの中に配置されて、ポジショナが配置したアイテムを生成する。

Repeaterは、モデルのプロパティで指定されたモデルのデータの各要素、および、Repeater内の子アイテムとして定義されたテンプレートアイテムを組み合わせて、多数のアイテムを作成する。
アイテムの総数は、モデルのデータ量によって決まる。

以下の例では、Grid内でRepeaterを使用して、Rectangleを格子状に配置している。
Repeaterは、Gridが5x5の配列で配置しており、計24個のRectangleを作成している。

 import QtQuick 2.15
 
 Rectangle {
    width: 400
    height: 400
    color: "black"
 
    Grid {
       x: 5
       y: 5
       rows: 5
       columns: 5
       spacing: 10
 
       Repeater {
          model: 24
 
          Rectangle {
             width: 70
             height: 70
             color: "lightgreen"
 
             Text {
                text: index
                font.pointSize: 30
                anchors.centerIn: parent
             }
          }
       }
    }
 }


Repeaterが作成するアイテムの数はRepeater.countプロパティで保持されるが、このプロパティは読み込み専用のため設定することができない。
そのため、上記の例のように、Repeater.modelプロパティにアイテムの数を設定する。

モデルが文字列リストの場合、デリゲートは文字列を保持する読み取り専用のRepeater.modelDataプロパティにも保持される。
以下の例では、Repeater.modelプロパティのリスト配列をText.textプロパティに順に設定して、Textを配置している。

QML Model and View 5.png
 Column {
    Repeater {
       model: ["apples", "oranges", "pears"]
 
       Text {
          text: "Data: " + modelData
       }
    }
 }


また、Repeaterが作成するアイテムのテンプレートとして、デリゲートを使用することもできる。
これは、delegateプロパティで指定する。


モデルデータの変更

モデルのデータを変更するには、モデルのプロパティに最新の値を割り当てる。
QMLのListModelはデフォルトで編集可能であるが、C++のモデルはsetDataメソッドを実装しないと編集できない。
なお、整数やJavaScriptの配列モデルは、読み取り専用である。

以下の例では、EditableModelはQAbstractItemModelを継承したC++モデルでsetDataメソッドを実装しており、かつ、QML型として登録されている場合、
データは、以下のようにモデルに書き込まれまる。

 ListView {
    anchors.fill: parent
 
    model: EditableModel {}
 
    delegate: TextEdit {
       width: ListView.view.width
       height: 30
       text: model.edit
 
       Keys.onReturnPressed: {
          model.edit = text
       }
    }
 }


※注意
editロールは、Qt::EditRoleと同じである。組み込みのロール名については、roleNames()を参照すること。
実際のモデルでは、通常、カスタムロールを登録する。

モデルのロールがrequired <プロパティ名>にバインドされている場合、そのプロパティに割り当ててもモデルは変更されない。
代わりに、モデルへのバインドが解除される。(他のプロパティへの割り当てが既存のバインドを解除するのと同様である)

必須プロパティを使用してモデルデータを変更する場合、modelも必須プロパティにしてmodel.propertyNameに割り当てる。

詳細は、Using C++ Models with Qt Quick Viewsを参照すること。


トランジションの使用

トランジションは、ポジショナに追加、ポジショナ内での移動、ポジショナから削除されたアイテムをアニメーション化するために使用する。

アイテムを追加するトランジションは、ポジショナの一部として作成されたアイテムや、ポジショナの子になるために再配置されたアイテムに適用される。
アイテムを削除するためのトランジションは、ポジショナ内のアイテムが削除された場合や、ポジショナから削除されてドキュメント内に新しい親が与えられた場合に適用される。

※注意
アイテムの不透明度をゼロに変更しても、ポジショナからアイテムが消えることはない。
これらのアイテムは、visibleプロパティを変更することで、削除および追加することができる。