Qt Quick 入門 第10回: プロパティバインディング

今回は QML の [qt 'プロパティ' l=qdeclarativeintroduction m=#properties] とその重要な特徴である「[qt "プロパティバインディング" l=propertybinding]」に関する説明します。

プロパティの設定方法

これまでの連載でもプロパティを使ってきましたが、改めて QML でのプロパティの設定方法についておさらいしてみましょう。

QML では基本的に、各要素のプロパティを以下のように ':' を使って設定します。このプロパティの設定方法は JSON の書式とよく似ています。

Rectangle {
width: 360
height: 360
}

この例では即値で指定していますが、プロパティに設定できるのは JavaScript の式となります。以下にその例を挙げてみましょう。

Rectangle {
x: 10
y: otherItem.y
width: 2 * otherItem.width
height: Math.max(otherItem.height, 50)
color: height > 100 ? "lightgreen" : "lightred"
border.width: { if (withBorder) 1; else 0 }
}

このように、関数を呼び出したり文を活用するなど、様々な方法でプロパティに値を設定することが出来ます。

プロパティバインディングとは

上記のプロパティの例のうち、最初の "x: 10" 以外のプロパティの設定方法を「プロパティバインディング」と呼びます。プロパティバインディングとは、QML でプロパティに値を設定するときの方法の一つで、':' を使って他のプロパティを含んだ JavaScript の式を設定(バインド)することをいいます。

プロパティバインディングでは、プロパティにある時点の値ではなく、互いの関係を表す式を設定します。このため、バインドした式の値が変わると、プロパティの値もその関係式に従って変化します。それによって、多くの場合でプロパティの値の変化に対応するコードを記述する必要がなくなり、コードを短くシンプルに出来るというメリットがあります。

リストA

それでは、その特性を段階を追って確認してみましょう。まずは以下のソース(リストA)を見てください。

// リストA
import QtQuick 1.0

Rectangle {
id: rootRect
width: 360; height: 150

Rectangle {
id: rect1
x: 30; y: 30
width: 50
height: 30
color: "red"
}

Rectangle {
id: rect2
x: 30; y: 90
width: rect1.width * 2
height: 30
color: "blue"
}
}

リストAの実行画面

この QML プログラムは赤(rect1)と青(rect2)の二つの [qml Rectangle] を表示しています。rect2 では19行目でその [qml '' width e=item] を rect1 の倍に設定しています。これがプロパティバインディングです。

        width: rect1.width * 2

このリストでは rect1.width は変化しないため、値の追従は確認できません。リストを変更して動きを加えましょう。

リストB

rect1 に以下のコードを追加します。

        MouseArea {
anchors.fill: parent
onClicked: rect1.width += 10
}

上記のコードで、rect1 はクリックされるごとにその幅が 10 増えることになります。追加後のリスト(リストB)を下記に示します。それでは実行してみてください。

// リストB
import QtQuick 1.0

Rectangle {
id: rootRect
width: 360; height: 150

Rectangle {
id: rect1
x: 30; y: 30
width: 50
height: 30
color: "red"
MouseArea {
anchors.fill: parent
onClicked: rect1.width += 10
}
}

Rectangle {
id: rect2
x: 30; y: 90
width: rect1.width * 2
height: 30
color: "blue"
}
}

リストBの実行画面

赤い rect1 をクリックしてその幅が増えると、同時に青い rect2 もその幅が増加していったのが確認できたでしょうか。このようにプロパティバインディングでは、バインドした式(':' の右側)内のプロパティの値が変化した場合に、バインドされたプロパティ(':' の左側)の値も自動的に追従します。

プロパティアサインメント

プロパティバインディングは便利な機能ですが、プロパティの値の変化に追従したくない場合には逆に不便です。そのような場合、通常の ':' を用いてプロパティを設定するのではなく、JavaScript の文を書ける場所で '=' を使って値を代入します。これを「プロパティアサインメント」と呼びます。

リストC

プロパティバインディングとプロパティアサインメントの違いを確認して見ましょう。リストBの rect2 に以下のコードを追加して実行してみてください。

        MouseArea {
anchors.fill: parent
onClicked: rect2.width = rect1.width + 10
}

追加後のリスト(リストC)を以下に示します。

// リストC
import QtQuick 1.0

Rectangle {
id: rootRect
width: 360; height: 150

Rectangle {
id: rect1
x: 30; y: 30
width: 50
height: 30
color: "red"
MouseArea {
anchors.fill: parent
onClicked: rect1.width += 10
}
}

Rectangle {
id: rect2
x: 30; y: 90
width: rect1.width * 2
height: 30
color: "blue"
MouseArea {
anchors.fill: parent
onClicked: rect2.width = rect1.width + 10
}
}
}

このコードでは rect2 をクリックするとその width を rect1 の width + 10 にします(28行目)。ところが、一度 rect2 をクリックすると、その後の rect1 のクリックでは rect2 の幅が変化しなくなります。このように、JavaScript を用いて代入を行った場合は、式ではなくその時点での値が代入されます。すなわち、プロパティバインディングではありません。

リストCの実行画面

なお、下記のコードのように通常のプロパティの設定方法である ':' の代わりに '=' を使うことは出来ません。

// NG
Rectangle {
width = otherItem.width
height = otherItem.height
}

他の要素の値を使いつつもバインドしたくない場合には、下記のように [qml Component onCompleted s=onCompleted] シグナルハンドラ等を利用して値を設定します。Component::onCompleted() はそのコンポーネントのインスタンスが生成された直後に発行されるシグナルです。

// OK
Rectangle {
width: 10
height: 10

Component.onCompleted: {
width = otherItem.width
height = otherItem.height
}
}

PropertyChanges

代入(プロパティアサインメント)ではプロパティバインディングにならないことを説明しましたが、それではバインドする式を変化させるにはどうすればいいのでしょうか。

前述した例のような場合では、クリックで変更される倍率やオフセットを格納するプロパティを作成し、それらのプロパティを用いた式をバインドすることで対応できるでしょう。興味のある方はプロパティの作り方は 第5回 で説明していますので、そちらを参考に簡単な練習問題として是非試してみてください。

それでは、複雑な式になるとどうでしょう。フラグ用のプロパティや三項演算子などを活用して対応する式を作ることも出来ます。ただし、式が複雑になればなるほどコードはわかりにくくなりますし、参照するプロパティが増えればパフォーマンスも落ちます。このような場合、第5回 で説明した状態遷移と [qml PropertyChanges] を用います。

リストD

それでは、実際に PropertyChanges を用いたコードを動かしてみましょう。

まず、rect2 に以下の states プロパティを追加してください。

        states: [
State {
name: "addWidth"
PropertyChanges {
target: rect2
width: rect1.width + 10
}
}
]

次に rect2 の MouseArea の onClicked を以下に変更します。

            onClicked: rect2.state = "addWidth"

最終的なコード(リストD)は以下になります。

// リストD
import QtQuick 1.0

Rectangle {
id: rootRect
width: 360; height: 150

Rectangle {
id: rect1
x: 30; y: 30
width: 50
height: 30
color: "red"
MouseArea {
anchors.fill: parent
onClicked: rect1.width += 10
}
}

Rectangle {
id: rect2
x: 30; y: 90
width: rect1.width * 2
height: 30
color: "blue"
MouseArea {
anchors.fill: parent
onClicked: rect2.state = "addWidth"
}
states: [
State {
name: "addWidth"
PropertyChanges {
target: rect2
width: rect1.width + 10
}
}
]
}
}

このコードを実行すると、rect2 をクリックした後に rect1 をクリックすると、rect2 の幅が rect1.width + 10 を維持するように追従します。プロパティにバインドされる式が変更されました。

リストDの実行画面

このようにコードとしては若干複雑になりますが、プロパティバインディングの式を変化するには状態遷移を利用してください。

まとめ

プロパティバインディングについて、簡単にまとめます。

  • ':' を用いてあるプロパティに別のプロパティを用いた式を設定(バインド)すること
  • 右辺の式に含まれるプロパティの値が変化すると、それに追従して左辺のプロパティの値が変化する
  • JavaScript で '=' を用いて代入した場合(プロパティアサインメント)、値が代入されプロパティの変化に追従しない(プロパティバインディングではない)
  • バインドする式を変更する場合は状態遷移と PropertyChanges を用いる

プロパティバインディングは、プロパティの値が変わったときの伝搬を自動的に行ってくれる、非常に便利な特性です。これにより QML では複数のプロパティに関連する値が変化した場合でも、変化に対応するコードを記述する必要がなくなり、リサイズなどの対応をよりスマートに行うことが出来ます。ただし便利な分、利用には注意も必要です。本文中では言及しませんでしたが、プロパティバインディングがループすると(警告が表示されますが)想定外の動作を引き起こす可能性もありますし、多用はパフォーマンスネックにもなります。気をつけて利用してください。


Blog Topics:

Comments