はじめに
スリントアドベントカレンダーも無事に埋まって10日目を迎えました。え、私が遅刻気味‥ごめんなさい。並行で調査しているものもあって進みが悪くて。
昨日は、@task_jpさんによる「PSD ファイルをロードする Qt 6 のモジュールを作りました」でした。デザイナーさんが用意したPSDファイルのデザインからQML, Slint, Flutterコードへ変換するツールの紹介です。僕は残念ながらデザイナーさんがつくような素敵なお仕事はまだ受けたことがないですが、ぜひそういう環境にいる方は使ってみてフィードバックしてあげてください。
本日のお題
Slint言語入門と称して、いずれ技術同人誌を書くためのたたき台作成中です。まぁ、Viewの伸びを見ると、紙刷ると爆死しそう(そこ、いつものこととか言わない)ですが、めげずに頑張っていきます。
本日は概要に少し加筆し、昨日の続きとしてプロパティ・式・コールバック周りについて触れて行きたいと思います。
Slint言語入門
プロパティとバインディング
エレメントとプロパティでも解説したように、すべてのエレメントはプロパティを持つことができます。組込みエレメントには色やサイズなどプロパティが用意されています。またこれら既存プロパティに加え、タイプ・名前・(オプションで初期値)を指定して追加のプロパティを定義できます。
プロパティには、値か式を割り当てることができます。
import { Button } from "std-widgets.slint";
export component Example inherits Window {
public property<int> my-property; // int初期値の0が割当られる
width: 42px;
height: { 42px } // ブロックでの設定
Button {
property <int> counter: 3; // counterは初期値3
clicked => { self.counter += 3 }
text: self.counter * 2; // 式での設定
}
}
上記の例では、textに self.counter * 2という式がバインドされています。このバインディングによりself.counterの依存関係が登録され self.counterの変更が監視されます。値が変更されるとtextに割り当てられた式がダーティー状態(結果が無効状態)になります。この状態でtextプロパティが参照されるとバインドされた式が再評価されます。これが先に説明した「リアクティブ」と「遅延評価」です。
ここまでに説明したバインディングはプロパティ参照を含む式による一方向のバインディングでしたが、<=>構文を使うことで双方向バインディングを設定できます。
export component Example {
in property<brush> rect-color <=> r.background;
in property rect-color2 <=> r.background;
r:= Rectangle {
width: parent.width;
height: parent.height;
background: blue;
}
}
上記のように双方向にリンクすることで、rect-color,rect-color2の変更結果はr.backgroundにバインドされ、r.backgroundの変更は、rect-color, rect-clor2にバインドされます。これにより、r.background, rect-color, rect-color2は常に同じ値を含むようになります。
式と演算子
式はプロパティ、値、演算子、関数等の組み合わせからなり演算結果として値を返します。式はプロパティにバインディングされると、式内のプロパティ変更を監視し変更を受けて式の結果を無効とし(リアクティブ性)、次のプロパティ参照時(遅延評価)に再計算されます。プロパティとバインド可能な式は、計算結果とプロパティの型が一致するか暗黙のうちに変換可能である必要があります。
Slintで式に利用可能な演算子は以下の通りです。
- 算術演算子 (数値型に対する演算)
- '+' (加算)
- '-' (減算)
- '*' (乗算)
- '/' (除算)
- 代入演算子
- '='
- '+='
- '-='
- '*='
- '/='
- 文字演算子 (stringに対する演算)
- '+' (連結)
- 比較演算子 (同じ型に対する比較)
- '==' (等しい)
- '!=' (等しくない)
- '>' (より大きい)
- '>=' (以上)
- '<' (より小さい)
- '<=' (以下)
- 三項演算子
- '[condition] ? [value-if-true] : [value-if-false]'
文と条件文とループ文
文とはプログラムの動作や制御を記述する基本単位です。 コンポーネントやエレメント内部でインスタンス化やプロパティ値の変更、コールバックハンドラや関数内部での動作定義などに利用されます。
バインド文
プロパティと式をバインドします。
text: self.counter * 2;
代入文
プロパティへの値の代入を行います。
foo = 42;
条件文
一般的なプログラミング言語と同じく if-else等の条件文が利用できます。
clicked => {
if (condition) {
foo = 42;
} else if (other-condition) {
bar = 28;
} else {
foo = 4;
}
}
値を返すようにすることでプロパティとバインドすることもできます。
export component Example inherits Window {
preferred-width: 50px;
preferred-height: 50px;
foo := Rectangle {
background: {
if area.pressed {
blue
} else {
red
}
}
}
area := TouchArea {
}
}
上記のような条件式が単純なら三項演算子による式のほうが簡単ですが。
export component Example inherits Window {
preferred-width: 50px;
preferred-height: 50px;
foo := Rectangle {
background: area.pressed ? blue: red;
}
area := TouchArea {
}
}
エレメントの生成制御も行えます。
export component Example inherits Window {
preferred-width: 50px;
preferred-height: 50px;
if area.pressed : foo := Rectangle { background: blue; }
if !area.pressed : Rectangle { background: red; }
area := TouchArea {}
}
ループ文
配列を利用して行うループ処理も用意されています。
export component Example inherits Window {
preferred-width: 50px;
preferred-height: 50px;
VerticalLayout {
for i in ["aaa","bbbb","cccc"] : Text {
text: i;
}
}
}
またループ文ではそのインデックスも利用できます。
export component Example inherits Window {
preferred-width: 300px;
preferred-height: 100px;
for my-color[index] in [ #e11, #1a2, #23d ]: Rectangle {
height: 100px;
width: 60px;
x: self.width * index;
background: my-color;
}
}
コールバック
コンポーネントは状態の変化を外部に伝えるコールバックを宣言できます。コールバックはcallbackキーワードを使って宣言し、状態変更時に呼び出すと通知として扱われます。QMLにおけるシグナルに相当するものと考えて良いでしょう。組込みエレメントにもコールバックが用意されています。
export component Button inherits Rectangle {
callback clicked;
}
また、コールバックには、 => をつかってコールバックハンドラを用意できます。コールバックハンドラはコールバックにバインドされる無名の処理ブロックです。
export component Button inherits Rectangle {
callback clicked;
area := TouchArea {
clicked => {
root.clicked()
}
}
}
たとえば、上記の例ではTouchAreaのclickedコールバックにバインディングされたコールバックハンドラで、Buttonコンポーネントのclickedコールバックを呼び出しています。これによりButtonコンポーネントはclickedコールバックを発行します。コールバックハンドラはバックエンドコード(Rust/C++/Javascript/Python)側でも実装できます。バックエンドコード側については別途機会を設けて説明することとしてここでは割愛します。
コールバックにはパラメータを宣言しハンドラへ引数を渡すことも可能です。
export component Example inherits Rectangle {
callback hello(int, string);
}
また、返却値を宣言し、ハンドラからの返却値を受け取ることも可能です。
export component Example inherits Rectangle {
callback hello(int, int) -> int;
}
また、<=> を使ってコールバックエイリアスを宣言できます。先のclickedのようなコールバックをより簡潔に記述できます。
export component Button inherits Rectangle {
callback clicked <=> area.clicked;
area := TouchArea {}
}
なお、特殊なコールバックとして、changedコールバックがあります。このコールバックはプロパティの変動に対してコールバックが行われるよう指定できます。
import { LineEdit } from "std-widgets.slint";
export component Example inherits Window {
VerticalLayout {
LineEdit {
changed text => { t.text = self.text; }
}
t := Text {}
}
}
テキストが変更するとコールバック呼び出しがイベントループのキューに登録され、次のイベントループサイクル以降に実施されます。コールバックは、プロパティの値が実際に変更された場合にのみ呼び出されます。プロパティの値が、同じサイクル中で複数回変更された場合、コールバックは1度だけ呼び出されます。さらに、プロパティの値が変更され、コールバックが実行される前に元の状態に戻った場合、コールバックは呼び出されません。
このコールバックは、無限ループを作れてしまうため、数回の反復後にループが停止するようになっています。このようなchangedコールバックシーケンスがある場合、そのシーケンスは中断され、それ以上のコールバックが行われなくなります。便利なコールバックですが、バインディングによる代替が可能な場合は利用を避け、過度に利用しないようにしてください。
changed bar => { foo = bar + 1; }
foo: bar + 1;
宣言型バインディングは依存関係を自動的に管理します。changedを使用すると、通常は遅延評価されるバインディングが強制的に即時評価されます。この方法ではバインディングの純粋性が損なわれ、グラフィカル エディターによる編集が複雑になります。変更されたイベントが過度に蓄積されると、特にループを伴うシナリオでは、変更コールバックによってプロパティが変更され、同じプロパティの変更がトリガーされたり、バグにつながるリスクがあります。
関数
概要でも述べた通りコンポーネントあるいはエレメント内の要素の一部として、式や文を組み合わせた一連の処理をまとめ再利用可能な関数として定義することができます。関数の定義にはfunctionキーワードを利用します。
export component Example {
function my-function(parameter: int) -> string {
return "result";
}
}
コールバックとは異なり、関数はSlint言語で実装を定義する必要があります。またデフォルトではprivateあつかいてあるため、外部でのアクセスはpublic/protectedのキーワードを使って外部公開する必要があります。
export宣言されたコンポーネントのpublic宣言された関数はバックエンドコード側から呼び出すことも可能です。
名前解決
プロパティ・コールバック・関数の利用時にはターゲットを指定して直接呼び出す方法と、ターゲットを指定せず名前解決する方法です。
ターゲット指定は、所属するルート要素(コンポーネント)を意味するroot, 親要素を意味するparent、自分自身を意味するselfの他、エレメントに := を使ってつけられた識別子を利用できます。ターゲット指定された呼び出しは、ターゲット自体に宣言・定義されている必要があります。
ターゲットを指定しない場合、自分自身から祖先要素に向かって該当する名前を検索し、最初に見つかったものが利用されます。異なる要素に定義されている限り、同じコンポーネント内で同じ名前が許容されます。
export component Example {
property <int> secret_number: my-function();
public pure function my-function() -> int {
return 1;
}
VerticalLayout {
public pure function my-function() -> int {
return 2;
}
Text {
text: "The secret number is " + my-function();
public pure function my-function() -> int {
return 3;
}
}
Text {
text: "The other secret number is " + my-function();
}
}
}
上記の例の場合は、secret_numberプロパティは1に設定され"The secret number is 3","The other secret number is 2" という文字が画面上に表示されます。
同名は便利といえば便利なのですが、使いすぎるとわかりにくくなるので、よく考えて設計しましょう。
まとめ
本日は、昨日の構文に引き続き、Slint言語の基本的な構文にあたるプロパティ・式・コールバック・関数周りについてまとめました。ここ数回分は遅刻気味でごめんなさい。まぁ、焦って色々書いても良いことはないですし、残りは次回の記事に回そうと思います。
明日は、@gony さんが「Slintでモーダルダイアログを実現する」を書いてくださるそうです。楽しみにお待ちしましょう。