はじめに
Slintアドベントカレンダーも4日目となりました。昨日は@task_jpさんによる「.slint のドキュメントの構成をまとめました」でした。
Slintは、宣言型UI言語であるSlint言語に関するドキュメントと、ロジックを実装する各種言語向けAPIとから構成されていますが、UI言語側のドキュメントと思って頂いて良いかと思います。まだまだ歴史が浅く、ドキュメンテーションもこなれていない感がありますが、それほど量は多くないので、気になる方はこちらを頼りにドキュメントを眺めて見てください。
本日のお題
Slintアドベントカレンダー2日目の記事として、「Slint-python (Alpha) を使ってみる」 としてSlint-Pythonで簡単なアナログ時計を作成しました。また、本日Qtアドベントカレンダー4日目の記事としてPyside6を使いQMLで同じアナログ時計の実装を記事にしています。
元々Slint言語は、QMLにインスパイアされた言語です。宣言型言語を謳う言語の中でもQMLは少し特異な言語ですが、Slintは非常によく似た言語です。
一方でQtにおける学習コストの高いC++言語とスマホなど向けな近年のダイナミックなUIデザインに向かない命令型UI記述に対する解決策として、Qtで実装されたインタプリタ言語としてスタートしたQMLと、UI設計に注力し、より軽量にネイティブ動作を主軸として設計されたSlintには設計思想に違いが出ています。
そこで、QMLとSlintを対比してみたいなと思います。
宣言型UI(Declarative UI)について
ところで宣言型・宣言的UIという単語が最近良く使われます。QMLは2009年にはDeclarative UIとして発表されていたのですが、その後も宣言型を謳うものとして以下のようなものがあります。
- ブラウザ向け
- Elm(Haskell, 2012)
- React(JavaScript, 2013)
- Vue.js(JavaScript, 2014)
- Angular(JavaScript, 2016)
- 非ブラウザ向け
- QML (EcmaScript, 2009)
- Flutter (Dart, 2017)
- SwiftUI (Swift, 2018)
- Jetpack Compose(Kotlin, 2020)
- Slint (Rust, 2021)
どれが人気という話をするつもりはありませんが、利用者やWebでの情報発信の多さではやはりブラウザ向けのフレームワークのほうが多いようです。
またFlutter,Jetpack ComposeはGoogle社、SwiftUIはApple社によるもので、モバイル開発で採用されているもののため知名度は圧倒的に高いです。
このため、多くのエンジニアが宣言型UIと聞いて思い浮かべるのはQMLやSlintではないので、Qt界隈のエンジニアが思い浮かべる宣言型UIとそれ以外はちょっと違和感があるかもしれません。
大きく違ってはいないと思うのですが、QMLやSlintでは、おおよそ以下のようなイメージで宣言型・命令型を区別しています。
命令型UI記述
せっかくなので旧来のQt Widgetsにおける命令型UI記述をPysideで表現してみましょう。
import sys
from PySide6.QtCore import Qt
from PySide6.QtGui import QFont, QColor, QPalette
from PySide6.QtWidgets import QApplication, QLabel, QMainWindow, QWidget, QSizePolicy, QVBoxLayout
class HelloWorldWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Hello World")
self.setGeometry(100, 100, 600, 400)
self.set_background_color("#2E3B55")
central_widget = QWidget(self)
self.setCentralWidget(central_widget)
layout = QVBoxLayout()
central_widget.setLayout(layout)
label = QLabel("Hello, World!")
label.setAlignment(Qt.AlignCenter)
font = QFont("Arial", 24, QFont.Bold)
label.setFont(font)
label_palette = label.palette()
label_palette.setColor(QPalette.WindowText, QColor("#FFFFFF"))
label_palette.setColor(QPalette.Window, QColor("#546E7A"))
label.setPalette(label_palette)
label.setAutoFillBackground(True)
label.setContentsMargins(20, 20, 20, 20)
label.setFixedSize(300, 100)
label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
layout.addWidget(label, alignment=Qt.AlignCenter)
def set_background_color(self, color_code):
palette = self.palette()
palette.setColor(QPalette.Window, QColor(color_code))
self.setPalette(palette)
if __name__ == "__main__":
app = QApplication(sys.argv)
window = HelloWorldWindow()
window.show()
sys.exit(app.exec())
タイトルをつけ、ウィンドウのサイズと位置を指定し、配色を決め、文字表示を作り、文字位置を決め、フォントを指定し、文字色を決め…というのをひたすら命令文として書き下すことになります。順序などはある程度自由にできてしまいますし、一見してどのようなUIになるのかわかるのはシンプルなUIだけになるでしょう。
宣言型言語であるQMLをつかうと以下のような表記になります。
import QtQuick
import QtQuick.Controls
ApplicationWindow {
visible: true
width: 600
height: 400
title: "Hello World - QML"
Rectangle {
anchors.fill: parent
color: "#2E3B55"
Rectangle {
width: 300
height: 100
color: "#546E7A"
anchors.centerIn: parent
Text {
text: "Hello, World!"
anchors.centerIn: parent
font.pixelSize: 24
font.bold: true
color: "#FFFFFF"
}
}
}
}
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
import sys
if __name__ == "__main__":
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load("main.qml")
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
命令型UIが順に命令を羅列しているのに対し、宣言型UIが部品の関連性とそのプロパティとを直感的に理解できる構文で記載されていることが見て取れるかと思います。
QMLやSlint以外の宣言型は、ロジックも入れてプログラムするものが多いようです。QMLもEcmaScriptに対応しており、ある程度のビジネスロジックもQML側に持たせることが可能ですが、パフォーマンスやデバッグの観点からできれば複雑なロジックをQML側に入れるのは避けたほうが良いと言われています。
Slint言語としてはUIを構築するのに必要な制御文以外は無く、現状ではそもそもロジックはSlint言語側に入れられないと考えたほうが無難です。
Python側ロジックにみる設計思想の違い
さて、例として上げたのでQMLを呼び出すmain.pyの記載があるのでPyside側はほぼ同じですが、先日あげた記事のPythonコードをまずは見比べてみましょう。
Pyside6(QML)
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
import sys
if __name__ == "__main__":
app = QApplication(sys.argv)
engine = QQmlApplicationEngine()
engine.load("ui/clock.qml")
if not engine.rootObjects():
sys.exit(-1)
sys.exit(app.exec())
Slint-Python(Slint)
import slint
import datetime
class App(slint.load_file("ui/clock.slint").Clock):
@slint.callback
def timeChanged(self):
dt = datetime.datetime.now()
self.hours = dt.hour
self.minutes = dt.minute
self.seconds = dt.second
app = App()
app.run()
Pyside6は、C++フレームワークであるQt6のPythonバインディングとして設計されています。Python側の実装はC++向けに用意されたAPI呼び出しをPythonで行うという表現となります。QML側で時刻取得機能を有しているため、PythonコードはQMLをロードして実行するということだけに特化しています。
Slintは基本的にSlint言語をネイティブ(対象言語)向けにコンパイルして利用するという設計思想です。対象言語から利用するときの自然さはSlintのほうが馴染みやすいコードになっています。
SlintはQMLと違いロジックに関する機能をほとんど持ちません。そのためPythonコードには日付を取得してSlint側で用意したプロパティに値を入れるという処理があるのですが、それでも行数的にはPyside6より短いです。
UIコード比較
それでは、QMLとSlintの差をざっくり眺めてみましょう。
import文
importは他のモジュールの機能を利用する宣言となります。
QML
import QtQuick
import QtQuick.Controls
clock.qmlの実装時に、QMLではimport文を2つ指定しています。QtQuickとQtQuick.Controlsです。QtQuickには基本的なTypeが、ControlsにはApplicationWindowが定義されています。
QMLでのインポート文は以下のフォーマットになっています。
import <ModuleIdentifier> [<Version.Number>] [as <Qualifier>]
Slint
Slintにはimport文が無いわけではなく、利用したエレメントはすべて組込みエレメントだったため何もインポートしていません。たとえばButtonやLineEditなどのウィジェットを必要とする場合は以下のようなimport文が必要になります。
import { LineEdit, Button } from "std-widgets.slint";
Slintでのimport文のシンタックスは以下のような形になります。
import { export1 } from "module.slint";
import { export1, export2 } from "module.slint";
import { export1 as alias1 } from "module.slint";
import { export1, export2 as alias2, /* ... */ } from "module.slint";
注意すべき違い
アナログ時計の例では出てきていませんが、モジュールについてはQMLとSlintで違いがあります。
QMLではディレクトリによりモジュールを表現します。このためディレクトリ名がモジュール名となります。またqmldirファイルを使いバージョニングも行うことができ、版数管理ができるようになっています。
ディレクトリ下には ComponentName.qmlという形式でコンポーネントが定義されます。
Slintにおいて、モジュールは.slintの拡張子をもつファイルで表現されます。"component 識別子"で、識別子が名前のコンポーネントとなります。1つのファイル内には複数のコンポーネントを定義可能です。現状では言語としてバージョニングなどは用意されていません。ファイルを指定してのimportなので、ファイル名などで分けるなどの必要があります。
また、componentは原則privateのため外部から参照するためには"export"の宣言が必要になります。
export component Clock inherits Window {
width:320px;
height:240px;
:
}
プロパティと型について
以下はClockで定義したActiveWindow/Windowのプロパティです。
width: 320
height: 240
color: "#646464"
property int hours
property int minutes
property int seconds
width: 320px;
height: 240px;
background: #646464;
in property<int> hours;
in property<int> minutes;
in property<int> seconds;
例えばwidth/heightは識別子が同じですが型が異なります。QMLではint型、Slintではlength型です。Slintの場合、型によっては単位指定が必要になります。length型の場合、px,pt,in,mm,cm等となります。
プロパティを追加できるのはQML/Slintとも共通ですが、Slintの場合はデフォルトでprivate扱いとなります。外部からのアクセスはin, out, in-outのいずれかを指定する必要があります。Clockの場合、in指定で外部からの入力を許可しています。
Image {
id: base
anchors.centerIn: parent
source : "https://raw.githubusercontent.com/qt/qtdoc/refs/heads/dev/examples/demos/clocks/images/clock-night.png"
:
}
Image {
source: @image-url("clock-night.png");
:
}
もう一つImageのsourceも同一名ですが、QMLではurl型、Slintではimage型で別物です。QMLの場合はURLを指定するとその場所から取得されますが、Slintではロード済みimageの指定のため、image-urlマクロが利用されています。こちらは少なくともSlint-Pythonではネットワーク取得ができていないため、先日の記事では先にダウンロードしていました。
ハンドラの登録
GUIでは発生したイベントに対する処理を登録する必要があります。呼び出される処理をハンドラ等と呼びますが、QMLとSlintでは用語と書きっぷりが少し違います。タイマーのコードを比較してみましょう。
Timer {
interval: 100;
running: true;
repeat: true;
onTriggered: clock.timeChanged()
}
Timer {
interval: 100ms;
running: true;
triggered() => { timeChanged(); }
}
QMLのTimerは指定時間経過時にtriggered()"シグナル"を発行します。QMLではon+SignalNameという名前でシグナルハンドラを定義できます。Clock.qmlではclockのtimeChanged()を呼び出します。
Slintでは、Timer指定時間経過時にtriggered()"コールバック”が発生します。コールバックに対し "=>" 演算子を使ってハンドラを定義できます。clocl.slint例ではcallback宣言しているtimeChanged()を呼び出しPython側コードを呼び出しています。
画像の回転について
SlintとQMLの記事ではさっくりと流していましたが、実はここは大きな違いです。
Image {
:
transform: Rotation {
id: secondRotation
origin.x: second.width/2
origin.y: second.height - 18
angle: clock.seconds * 6
Behavior on angle {
SpringAnimation { spring: 2; damping: 0.2; modulus: 360 }
}
}
}
Image {
:
rotation-origin-x: parent.width/2 - self.x;
rotation-origin-y: parent.height/2 - self.y;
rotation-angle: root.seconds * 6deg;
animate rotation-angle {
duration: root.seconds > 0? 250ms : 0ms;
easing : ease-in-out;
}
}
QMLはImageの基底タイプであるItemのもつtransformに対してRotationを設定しています。それに対しSlintでは、Imageのもつrotation系プロパティに値を設定しています。一見何が違うのって思うかもしれませんが…
こいつを slint で書き換えてみてるんだけど、Rectangle が rotation できなくてめんどくさいかも?ってなってる。 pic.twitter.com/qpM1XByD3u
— Tasuku Suzuki (@task_jp) October 19, 2024
はい。QMLだと色々なTypeがTrnasofm(回転だけではなくScale, Translate, Matrix4x4での変形)対象ですが、SlintだとImageとPath程度にしか回転プロパティがありません。軽量でシンプルであるが故に、まだ手の届いていない部分も結構あります。
アニメーションについては、変動させるプロパティに対し、QMLではBehaviorを、Slintではanimateを設定することになります。英語圏の人間ではないのでanimateのほうがしっくりきますが、QMLはいくつかのアニメーションが用意されているのに対し、SlintはCSSと同じ加減速のパラメータしかありません。
総評
細かいことを言い出すときりがないのですが、QMLとよく似た文法ですが、色々と違いもあるのでQMLができるからといって直ぐにSlintを使いこなせるかというと厳しい部分もでてきます。
pipでインストールされるサイズ
python3 -m venv pysideenv
source pysideenv/bin/activate
pip install pyside6
Collecting pyside6
Downloading PySide6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (5.3 kB)
Collecting shiboken6==6.8.1 (from pyside6)
Downloading shiboken6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (2.5 kB)
Collecting PySide6-Essentials==6.8.1 (from pyside6)
Downloading PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (3.7 kB)
Collecting PySide6-Addons==6.8.1 (from pyside6)
Downloading PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl.metadata (4.0 kB)
Downloading PySide6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl (532 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 532.7/532.7 kB 6.7 MB/s eta 0:00:00
Downloading PySide6_Addons-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl (160.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 160.3/160.3 MB 6.4 MB/s eta 0:00:00
Downloading PySide6_Essentials-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl (95.3 MB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 95.3/95.3 MB 9.3 MB/s eta 0:00:00
Downloading shiboken6-6.8.1-cp39-abi3-manylinux_2_28_x86_64.whl (203 kB)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 203.1/203.1 kB 13.6 MB/s eta 0:00:00
Installing collected packages: shiboken6, PySide6-Essentials, PySide6-Addons, pyside6
Successfully installed PySide6-Addons-6.8.1 PySide6-Essentials-6.8.1 pyside6-6.8.1 shiboken6-6.8.1
du -sh pysideenv/
660M pysideenv/
python3 -m venv slintenv
source slintenv/bin/activate
pip install slint
Collecting slint
Using cached slint-1.8.0a1-cp310-abi3-manylinux_2_35_x86_64.whl.metadata (10 kB)
Using cached slint-1.8.0a1-cp310-abi3-manylinux_2_35_x86_64.whl (16.0 MB)
Installing collected packages: slint
Successfully installed slint-1.8.0a1
du -sh slintenv/
53M slintenv/
660MBと53MBこちらは段違いですね。まぁ、QMLのほうが圧倒的に高機能なので仕方がないですが。
実行時メモリ使用量
pyside6, Slint-Pythonを利用したときのアナログ時計のメモリ比較です。Pyside6の記事では画像をネットワークからダウンロードしていましたが、これが不利の要因になってもいけないので、先にダウンロードしてローカル参照するように修正して計測しました。
QML
Slint
QML側は124MiB程度、Slint側は77MiB程度で推移するようです。まぁ、全体的にはSlintのほうがメモリ消費は少ないようです。使っている機能が少ないので、ディスク使用量ほどのインパクトはないですね。
topで監視してみる
え、プロファイルを取れって‥そろそろ面倒になってきたので。
24318 hermit4 20 0 558672 97456 73264 S 0.0 0.3 0:13.67 python3
24330 hermit4 20 0 903968 147620 106492 S 0.0 0.4 3:51.82 python3
24330 hermit4 20 0 903968 147620 106492 S 5.0 0.4 3:51.97 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.68 python3
24330 hermit4 20 0 903968 147620 106492 S 5.0 0.4 3:52.12 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.69 python3
24330 hermit4 20 0 903968 147620 106492 S 5.6 0.4 3:52.29 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.70 python3
24330 hermit4 20 0 903968 147304 106492 S 6.0 0.4 3:52.47 python3
24318 hermit4 20 0 558672 97456 73264 S 1.0 0.3 0:13.73 python3
24330 hermit4 20 0 903968 147304 106492 S 5.3 0.4 3:52.63 python3
24318 hermit4 20 0 558672 97456 73264 S 0.0 0.3 0:13.73 python3
24330 hermit4 20 0 903968 147304 106492 S 5.3 0.4 3:52.79 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.74 python3
24330 hermit4 20 0 903968 147304 106492 S 5.6 0.4 3:52.96 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.75 python3
24330 hermit4 20 0 903968 147304 106492 S 5.3 0.4 3:53.12 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.76 python3
24330 hermit4 20 0 903968 147304 106492 S 5.3 0.4 3:53.28 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.77 python3
24330 hermit4 20 0 903968 147304 106492 S 5.0 0.4 3:53.43 python3
24318 hermit4 20 0 558672 97456 73264 S 0.0 0.3 0:13.77 python3
24330 hermit4 20 0 903968 147304 106492 S 5.0 0.4 3:53.58 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.78 python3
24330 hermit4 20 0 903968 147304 106492 S 5.0 0.4 3:53.73 python3
24318 hermit4 20 0 558672 97456 73264 S 0.7 0.3 0:13.80 python3
24330 hermit4 20 0 903968 147304 106492 S 5.6 0.4 3:53.90 python3
24318 hermit4 20 0 558672 97456 73264 S 0.0 0.3 0:13.80 python3
24330 hermit4 20 0 903968 147304 106492 S 5.0 0.4 3:54.05 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.81 python3
24330 hermit4 20 0 903968 147304 106492 S 4.6 0.4 3:54.19 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.82 python3
24330 hermit4 20 0 903968 147304 106492 S 5.0 0.4 3:54.34 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.83 python3
24330 hermit4 20 0 903968 147304 106492 S 5.3 0.4 3:54.50 python3
24318 hermit4 20 0 558672 97456 73264 S 0.3 0.3 0:13.84 python3
メモリサイズからわかるかと思いますが、PID 24330がQML側です。CPU使用率みるとQMLのほうが負荷は高そうですね。
実行結果
アドベントカレンダー用の記事をカキカキしている。Pyside6-QMLとSlint-PythonだとQMLのほうが回転後の画像がきれいな気がする。 pic.twitter.com/eUoUUf1g2t
— 緑野翁 (@hermit4) December 3, 2024
動画を効率よくQiitaに上げる方法がわからないからつぶやいて埋め込んでおきますが、実行結果はQMLのほうが美しく感じます。どうも回転させた場合を比較するとPython-Slintはエイリアシングが目立ちます。流石に長い年月研鑽を積んでいるQMLにはまだ勝てない部分かもしれません。
ライセンス
実装や動作とは関係ありませんが、ライセンスもフレームワーク採用の重要な項目でしょう。オープンソースライセンスとしては、SlintはGPLv3, QMLはGPLv3かLGPLv3のいずれか選択になります。オープンソースライセンスだけ見れば、選択肢の多いQMLの方が良さそうです。
Slint, Qtとも商用ライセンスが用意されています。Qtは割とお高め、Slintは会社規模・ビジネス規模に応じた変動価格となっています。
Slint商用ライセンスは、僕みたいなフリーランスが利用するなら現在のレートで年間1万5千円くらいです。サブスクリプションをキャンセルしても、有効期間に取得したバージョンには”perpetual fallback license”が付与され利用を続けることができるので、スタートアップ企業や個人が利用するならかなりお得な金額ですね。
個人でQtの商用ライセンスを維持していた頃は、為替レートがだいぶ穏便で、ライセンスもまだお安い時期だったにも関わらず更新価格で30万円超えてましたし。
その他、Slintは組込み以外の分野(デスクトップやモバイル、Webアプリケーション)ではロイヤリティフリーライセンスが用意されています。Slintを使っていることを条件に沿って表示する義務がありますが、ライセンス条項を守れば無償で利用できます。
まとめ
今回は、Pyside6とSlint-Pthon(Alpha)を使ってアナログ時計を作った結果を比較してみました。もともとSlintはRaspberry Pi Picoでも動作が確認できているフレームワークで省メモリに努めています。そのあたりはPythonにも現れておりPysideと比べれば軽量です。ただし流石に100ms周期というのんびりした更新速度だと実行速度に差は感じられませんでした。
QMLのほうがアンチエイリアスもしっかり聞いて見え、アニメーションもスプリングのようなアニメーションが実装されていることから、アナログ時計っぽい動作に感じます。
まだPython-Slint自体がアルファということもありますが、利用時にはある程度トレードオフがあることを理解したうえで、どちらを使うのか考えて行く必要がありそうです。