QMLの基礎 - モデルとビュー
概要
ソフトウェアはデータを生成して、データを表示する必要がある。
Qt Quickには、データを表示するためのモデル、ビュー、デリゲートという概念が存在する。
モデル、ビュー、デリゲートは、データの可視化をモジュール化し、開発者がデータの様々な側面をコントロールできる。
また、データにほとんど変更を加えることなく、リストビューとグリッドビューを入れ替えることができる。
同様に、データのインスタンスをデリゲートにカプセル化することで、データの表示方法や処理方法を決定することができる。
- モデル
- データとその構造を含む。
- モデルを作成するためのQMLタイプがいくつかある。
- ビュー
- データを表示するコンテナである。
- ビューは、リストやグリッドでデータを表示する。
- デリゲート
- データがビューにどのように表示されるかを指定する。
- デリゲートはモデルの各データを受け取り、それをカプセル化する。データはデリゲートを通してアクセスできる。
- デリゲートは、データを編集可能なモデルに書き戻すこともできる。(例.
TextField
のonAccepted
ハンドラ内) - データを可視化するには、ビューの
model
プロパティをモデル、delegate
プロパティをコンポーネントやその他の互換性のあるタイプにバインドする。
ビューによるデータの表示
ビューは、アイテムのコレクションのためのコンテナである。
豊富な機能を持ち、スタイルや動作の要求に合わせてカスタマイズすることができる。
Qt Quickの基本セットには、以下に示す標準的なビューが用意されている。
- ListView
- 水平または垂直のリストにアイテムを配置する。
- GridView
- 利用可能なスペース内のグリッドにアイテムを配置する。
- PathView
- アイテムをパス上に配置する。
上記のビューには、それぞれ専用のプロパティと動作がある。
ビューの装飾
ビューでは、header
、footer
、section
等の装飾用プロパティを使用して、見た目をカスタマイズすることができる。
これらのプロパティに対して、オブジェクト(通常は別のビジュアルオブジェクト)をバインドすることにより、ビューを装飾することができる。
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"
}
}
マウスとタッチの操作
ビューは、コンテンツのドラッグやフリックを処理するが、個々のデリゲートとのタッチ操作は処理しない。
デリゲートがタッチ入力に反応して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
}
}
}
}
ビューのデリゲート
ビューは、リストの中のアイテムを視覚的に表現するために、デリゲートが必要となる。
ビューは、デリゲートにより定義されたテンプレートにしたがって、各アイテムのリストを生成する。
モデル内のアイテムは、アイテムのプロパティだけでなく、インデックスのプロパティにもアクセスできる。
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
を配置している。
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
プロパティを変更することで、削除および追加することができる。