はじめに
Pythonの豊富なライブラリを活かせば、非常に強力なGUIツールを作ることができます。特にデータの可視化においては、1次元データはpyqtgraph
、多次元の画像データはnapari
がとても優秀で、これらのライブラリを使えるだけでできることが格段に増えます。
しかし、GUIを書いたことのある方であれば必ず感じることがあります。
「「さすがにもうちょっと楽にできないのか!」」
これをどうにかできないかと思い、Qtをベースにしたmagic-classというパッケージを開発しています。今回は宣伝もかねてmagic-class
の簡単な紹介をします!
※英語ですが今頑張ってドキュメントも書いています。興味があったらこちらも参考にしていただけると嬉しいです。
magic-classの動機
これまでのパッケージの問題点
magic-class
で解決したかったのは次の問題点です。
- GUIのコードはPythonの読みやすさを全く活かせない。たいてい非常に読みづらくなりメンテナンスが難しい。
- 「ボタンを押す→引数を指定する→関数を呼ぶ」という超基本な操作ですら、関数ごとにウィジェットを用意する必要がある。
- ボタンもメニューの各項目もツールバーの各項目も、やってることほぼ同じなのになぜかメソッド名や設計が異なる。
- GUIで手動操作しつつJupyterなどでスクリプトベースで操作する「ハイブリッド」なGUIがPythonの主流になっているにもかかわらず、それを作ろうとするとさらに書くことが増える。
もっと簡潔に悩みを書くとすれば...
「高度なことはできなくてもよいから、シンプルで使いやすいGUIを直観的に作りたい」
ということになります。
magic-class
ではどうか?
magic-class
の実装は、次のような設計になっています。
- 変数の型や型アノテーションを活用してウィジェットを用意していく。スクリプトからの操作と干渉しないように、変数そのものの書き換えは最小限に抑える。
- デザインやショートカットなど、付加情報はデコレータにまとめる。
- クラス定義のコードの見た目がほぼそのままGUIのレイアウトになるようにする。
- 特殊文法を導入しない。
- APIは可能な限り統一する。
1.~3.は、ウィジェットの細かなデザインでコードが汚れるのを防ぎます。
4.もかなり重要で、これを意識しないと、Web開発で知られるDjango
のように、固有なルールを導入しすぎてPythonを書き慣れた人でも再学習しなければいけないというような状況が生じます。
5.は、前述したように通常のウィジェットとメニューでわずかにメソッドが異なるせいで無駄に検索したりデバッグしたりという問題を防ぎます。
実際にはこんな感じ
詳細は後述するとして、magic-class
をつかったコードはこんな感じになります。これは画像をロードし、フィルタをかけ、保存することができるGUIです(関数の内容は省略)。
from magicclass import magicclass, magicmenu, bind_key
from magicclass.widgets import Image
from pathlib import Path
@magicclass # デコレートしたクラスを通常のウィジェットに変換する
class A:
@magicmenu # デコレートしたクラスをメニューに変換する
class Menu:
@bind_key("Ctrl-O") # ショートカットを登録する
def Open(self, path: Path):
"""画像を開く"""
@bind_key("Ctrl-S")
def Save(self, path: Path):
"""画像を保存する"""
image = Image() # 画像表示ウィジェット
def apply_filter(self, sigma: float):
"""画像にフィルタをかけてimageを更新"""
これだけでクラスA
はGUI用に拡張され、
ui = A()
ui.show()
により次のGIF画像のようにGUIが起動します。
Path
型でアノテーションされたOpen
とSave
は、GUIで呼び出されるとファイルダイアログを開きます。apply_filter
は浮動小数sigma
の入力が必要なので、それ用のウィンドウが開きます。関数の中身を定義すれば実際にフィルタをかけたりとかできます。
関数自体は書き換えられていないので、
ui.apply_filter(3.0)
ui.Menu.Open("/path/to/image.png")
のようにスクリプトからの実行もできます。
マクロ記録機能
手動操作から実行可能なスクリプト(マクロ)を生成する機能というのは、Excelをはじめ様々なソフトウェアで用意されています。magic-class
ではPythonスクリプトを生成するマクロ記録機能が標準搭載されています。次のGIF画像をご覧ください。
コードはこちら
from magicclass import magicclass, vfield
from magicclass.widgets import QtPlotCanvas
@magicclass
class A:
@magicclass(widget_type="groupbox", layout="horizontal") # ウィジェットのデザインを指定
class Parameters:
a = vfield(float, widget_type="FloatSlider", options={"min": 1.0, "max": 10.0, "step": 0.1})
b = vfield(float, widget_type="FloatSlider", options={"min": 1.0, "max": 10.0, "step": 0.1})
plt = QtPlotCanvas() # プロット用のウィジェット
def set_title(self, title: str):
self.plt.title = title
@Parameters.a.connect
@Parameters.b.connect
def _plot(self):
a = self.Parameters.a
b = self.Parameters.b
x = np.linspace(0, 1, 100)
# グラフの更新
self.plt.layers.clear()
self.plt.add_curve(np.cos(x*a), np.sin(x*b))
ui = A()
ui.show()
ui.macro.widget.show() # マクロウィンドウを開く
左のメインウィンドウでの操作が、右のマクロウィンドウにリアルタイムで記録されているのが分かります。マクロ記録機能は汎用性が高いにもかかわらず、実装が非常に大変なので、とても有用な機能だと思います。
使い方
ここからは、magic-class
の使い方をもう少し説明していきます。
インストール
pipでインストールできます。
pip install magic-class
型とウィジェットの対応
magic-class
の型とウィジェットの対応やその実際の実装は、magicgui
に従っています。
magicgui
は、magicgui.widgets
にWidget
クラスを継承した様々なウィジェットを用意しており、これらはAPIがとてもよく統一されている素晴らしい設計になっています。例えば、文字列を入力するLineEdit
も、整数を入力するSpinBox
も、テーブルデータを入力/表示するTable
も、値の参照/代入はすべてwidget.value = ...
で行われます。
さらに、デコレータ@magicgui
を用いて関数をGUI化でき、その際、次の対応表に従って引数の型アノテーションやデフォルト引数の型に対応するWidget
型のウィジェットを用意します。
型 | ウィジェット | ウィジェットの説明 |
---|---|---|
str |
LineEdit |
文字列の入力欄 |
pathlib.Path |
FileEdit |
パスの入力欄と、ファイルダイアログを開くボタン |
int |
SpinBox |
数値を上下ボタンで調節できる |
float |
FloatSpinBox |
数値を上下ボタンで調節できる |
bool |
CheckBox |
チェックを入れる/外すことでTrue/Falseを変更 |
enum.Enum |
ComboBox |
選択肢を選ぶドロップダウンメニュー |
datetime.datetime |
DateTimeEdit |
日時/時刻を調節できる |
datetime.date |
DateEdit |
日時を調節できる |
datetime.time |
TimeEdit |
時刻を調節できる |
※他にも、ウィジェット型を指定すればSpinBox
をSlider
にしたりもできます。より詳細は公式ドキュメントをご覧ください。
実際に使うと次のようなコードになります。
%gui qt # QtベースのGUIなので、Jupyterではこれを実行する
from magicgui import magicgui
from datetime import date
@magicgui
def diary(t: date, text: str):
print(f"{t}: {text}")
diary.show()
magicgui
からmagic-class
へどう拡張したか
magic-class
では、@magicgui
を真似て、クラスをデコレートしてGUI化するデコレータをいくつか用意しています。
@magicclass
→ メインウィンドウとなる通常のウィジェット
@magicmenu
→ メニュー
@magiccontext
→ 右クリックで現れるコンテキストメニュー
@magictoolbar
→ ツールバー
これらでデコレートするクラスの書き方に違いはありません。
さらに、@magicclass
に関しては、引数widget_type=...
を与えることで、スクロールバー付きだったり、タブに分けたりしたウィジェットに切り替えることができます。もちろんクラスの書き方に変更はありません。詳細はドキュメントにあります。
magic-class
がクラスをウィジェットに変換する際の要点は
(1) クラスのメソッド一つ一つを@magicgui
でデコレートして、それを呼び出すボタン(またはメニュー)を追加する
(2) magicgui.widgets
にあるWidget
オブジェクトはそのまま追加する
(3) クラス内に他のmagic-classが現れた場合、再帰的にウィジェットを作成してから追加する
の3点です。さらに、
(2') 型からウィジェットに変換する関数vfield
を用意する
ことで、すべてを型→ウィジェットの変換で済ませることができました。vfield
はPythonの標準ライブラリdataclasses
のField
を継承したオブジェクトを返すもので、dataclasses.field
と使い方が非常に似ています。
a = vfield(int) # intなので、GUIではSpinBoxに変換
a = vfield(1) # 1はintなので、1にセットされたSpinBoxに変換
のように使います。
簡単な例
電卓を実装した例です。
from magicclass import magicclass, vfield
@magicclass
class A:
a = vfield(int)
b = vfield(int)
def add(self):
self.result = str(self.a + self.b)
def sub(self):
self.result = str(self.a - self.b)
def mul(self):
self.result = str(self.a * self.b)
def div(self):
self.result = str(self.a / self.b)
result = vfield(str)
クラスA
の変数a
, b
, add
, sub
, mul
, div
, result
が順にウィジェットに変換されていることが分かると思います。
おわりに
GUIはいくつかのライブラリを試しましたが、magicgui
の理念は斬新で比類ないものだと思っています。magicgui
の足りない部分をmagic-class
でカバーすることで、GUI開発がかなり捗るようになりました。
ぜひ使っていただけると嬉しいです!