4
Help us understand the problem. What are the problem?

posted at

【Python】magic-classで効率的にGUIを作成する

はじめに

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の実装は、次のような設計になっています。

  1. 変数の型アノテーションを活用してウィジェットを用意していく。スクリプトからの操作と干渉しないように、変数そのものの書き換えは最小限に抑える。
  2. デザインやショートカットなど、付加情報はデコレータにまとめる。
  3. クラス定義のコードの見た目がほぼそのままGUIのレイアウトになるようにする。
  4. 特殊文法を導入しない。
  5. 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が起動します。

output.gif

Path型でアノテーションされたOpenSaveは、GUIで呼び出されるとファイルダイアログを開きます。apply_filterは浮動小数sigmaの入力が必要なので、それ用のウィンドウが開きます。関数の中身を定義すれば実際にフィルタをかけたりとかできます。

関数自体は書き換えられていないので、

ui.apply_filter(3.0)
ui.Menu.Open("/path/to/image.png")

のようにスクリプトからの実行もできます。

マクロ記録機能

手動操作から実行可能なスクリプト(マクロ)を生成する機能というのは、Excelをはじめ様々なソフトウェアで用意されています。magic-classではPythonスクリプトを生成するマクロ記録機能が標準搭載されています。次のGIF画像をご覧ください。

output2.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.widgetsWidgetクラスを継承した様々なウィジェットを用意しており、これらは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 時刻を調節できる

※他にも、ウィジェット型を指定すればSpinBoxSliderにしたりもできます。より詳細は公式ドキュメントをご覧ください。

実際に使うと次のようなコードになります。

%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()

image.png

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の標準ライブラリdataclassesFieldを継承したオブジェクトを返すもので、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)

image.png

クラスAの変数a, b, add, sub, mul, div, resultが順にウィジェットに変換されていることが分かると思います。

おわりに

GUIはいくつかのライブラリを試しましたが、magicguiの理念は斬新で比類ないものだと思っています。magicguiの足りない部分をmagic-classでカバーすることで、GUI開発がかなり捗るようになりました。

ぜひ使っていただけると嬉しいです!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
4
Help us understand the problem. What are the problem?