🍃

最もシンプルなGUI設計パッケージ「PyamlQt」について

2022/03/20に公開

2022/07/16 追記

後継のライブラリ「ReadableWidgets」を公開しています。現在はこのリポジトリ上で開発しています。

本日、PyamlQtをPyPIでリリースしました。PyamlQtとは、yaml形式で記述されたデザインを使ってGUIを構築するPyQt6依存のパッケージです。

この記事はPyamlQt:v0.1.1時点での解説です。最新のソースコードはGitHub (PyamlQt)の方をご覧ください。

https://pypi.org/project/PyamlQt/

GitHubは以下のリンクから確認できます。バグ報告や拡張機能など、コントリビュート大歓迎です!

https://github.com/Ar-Ray-code/PyamlQt

この記事では、PyQtを使ったGUIをより簡単に作成できるPyamlQtパッケージを作成した経緯やこれからの方針について書きます。

アップデート情報は私のGitHubはてなブログで、初心者向けのレシピはQiitaに掲載する予定です。

PyamlQt

PyamlQt、「ぴゃむるきゅーと」と読みます。かなり読みにくい…「やむるきゅーと」で良い気がしてきました。

PyamlQtとは名称の通り、PyQtとyamlが関係するパッケージで、デザインがこれまでのxml形式やPythonの代わりにyamlで定義されています。このパッケージを使うことでyamlをロードしたら勝手にPyQtのGUIができます。(まぁ、機能は制限されますが…)

プログラムやロボットの簡単なコントローラや初心者には大いに役立つのでは?とちょっと期待していたりします。

https://twitter.com/Ray255Ar/status/1505429594178228225

発端

かつて私はさまざまなプロジェクトでPyQtを使用してきました。

↓例(GitHubリポジトリ)

https://github.com/Ar-Ray-code/urarachallenge_analyzer

https://github.com/Ar-Ray-code/rclshark2

Qtは他のGUI作成ツールと比べてもかなりGUIが綺麗です。他にPythonで人気なGUI作成ツールといえばtkinter、Kivyがあります。

(Kivyはライセンス的にもGUI的にもいいツールらしいので試してみたいですね。)

C++でもQtを触っているので、その流れでPyQtは動作確認用GUIを作るときにはよくお世話になっているのですが、簡単なGUIを作る時にどうしても面倒なのがプログラムの流用時です。

GUI構築ライブラリには覚えきれないほどのツール・要素があり、毎回最初から書いていたら「プログラムを試したいだけなのにGUI構築に時間がかかってしまった」という本末転倒な結末を迎えてしまうため、大抵は私自身が作ってアップしているリポジトリをコピペして適宜編集…というサイクルで効率よく(?)開発をしていました。

しかし、ある日唐突に嫌気が差したのです。

「デザイン構文、分かりにくい〜〜(怠惰)」

そもそもPython上に書かれたデザイン設定を読むのがとても面倒だし、だからといってQtデザイナで作成されたxmlファイル(.ui)を読むのは辛い。

Qtデザイナでファイルを開いて設定を確認すれば…ってそんなにガチなものを作りたいわけでもないので、気が乗りません。

そこで、もっと読みやすい形式を使ってデザインを定義すればもっと読みやすく、再利用しやすいプログラムになるのではないかと考え、PyamlQtを作成することにしました。

形式は、とっても読みやすいと評判のyaml形式です。


PyamlQtは、実際に動作を記述するPythonファイルとPyamlQtパッケージ、デザインを定義するyamlファイルで構成されます。

仕組みは簡単です。

  • yamlファイルをPyamlQtに渡して設定を読み込ませる。
  • yamlで定義されたウィジェット (qpushbuttonとか) に対応したQObject型 (widget) とそのウィジェトの名前 (key) をtuppleで返して、そのtuppleを一つづつdictに格納する。
  • widget[key].clicked.connect(... で挙動を指定する。

PyamlQtは従属関係を読むことができないので、QWidgetsしか設定できませんが、基本的にボタン押下時の関数呼び出しと値反映くらいしかGUIを作らない私にとってこの縛りは全く問題になりませんでした。(これ以上を求めるとしてもメインのpyファイルに直接記入できます)

最初は自分のためだけに作っていたのですが、途中から「これかなり使えるぞ!」と思い、その勢い任せでPyPIパッケージにしちゃいました。我ながら本当に気まぐれ…

使い方

PyamlQtはとてもシンプルに記述できますが、残念ながら技術的な問題点からテンプレートを一部流用する必要があります。

以下にそのプログラム例を示します。これは、足し算しかできない電卓のサンプルプログラムです。

Python側

# Template ====で囲まれている部分は必ず記述しなければいけない部分です。QWidgetsは、QMainWindowの元で動くため、現時点ではパッケージ側に押し込むことができませんでした。

しかし、当初50行程度あったテンプレートが2~3行にできたのは私にしてはよくできたほうだと思います。

ちなみに、MainWindowの定義はyaml側で定義するメリットがあまりなかったので、そのまま直で設定しています。

import sys
import os
import yaml

from pyamlqt.create_widgets import create_widgets
from PyQt6 import QtCore, QtWidgets
from PyQt6.QtWidgets import QApplication, QMainWindow

TITLE = "calc"
WIDTH = 700
HEIGHT = 920
PLUS = -2
EQUAL = -3

YAML = os.path.join(os.path.dirname(__file__), "../yaml/calculator.yaml")

class MainWindow(QMainWindow):
    def __init__(self):
        self.number = 0
        super().__init__()

        # geometry setting ---
        self.setWindowTitle(TITLE)
        self.setGeometry(0, 0, WIDTH, HEIGHT)
        self.create_widgets()

    def __del__(self):
        pass
    
    def create_widgets(self):
        # Template ---
        self.widgets, self.stylesheet = self.create_all_widgets(YAML)
        for key in self.widgets.keys():
            self.widgets[key].setStyleSheet(self.stylesheet["style_common"])
        # ------------

        # ここで、0~9と+,=の挙動を定義「button_update」に引数付きで渡す
        self.widgets["button_1"].clicked.connect(lambda: self.button_update(1))
        self.widgets["button_2"].clicked.connect(lambda: self.button_update(2))
        self.widgets["button_3"].clicked.connect(lambda: self.button_update(3))
        self.widgets["button_4"].clicked.connect(lambda: self.button_update(4))
        self.widgets["button_5"].clicked.connect(lambda: self.button_update(5))
        self.widgets["button_6"].clicked.connect(lambda: self.button_update(6))
        self.widgets["button_7"].clicked.connect(lambda: self.button_update(7))
        self.widgets["button_8"].clicked.connect(lambda: self.button_update(8))
        self.widgets["button_9"].clicked.connect(lambda: self.button_update(9))
        self.widgets["button_0"].clicked.connect(lambda: self.button_update(0))
        self.widgets["button_plus"].clicked.connect(lambda: self.button_update(PLUS))
        self.widgets["button_equal"].clicked.connect(lambda: self.button_update(EQUAL))

        # show
        self.show()
# Template ================================================================
    def create_all_widgets(self, yaml_path: str) -> dict:
        widgets, stylesheet_str = dict(), dict()
        with open(yaml_path, 'r') as f:
            self.yaml_data = yaml.load(f, Loader=yaml.FullLoader)
        
            for key in self.yaml_data:
                data = create_widgets.create(self, yaml_path, key, os.path.abspath(os.path.dirname(__file__)) + "/../")
                widgets[key], stylesheet_str[key] = data[0], data[1]

        return widgets, stylesheet_str
# =========================================================================

  # ボタンが押された時に実行
    def button_update(self, number:int = -1):
        if number >= 0:
            self.number = number
        else:
            if number == PLUS:
                self.cache = self.number
                self.number = 0
            elif number == EQUAL:
                self.number = self.cache + self.number
                self.cache = 0
            else:
                pass
        self.widgets["lcd"].display(self.number)

if __name__ == '__main__':
    app = QApplication(sys.argv)
    window = MainWindow()
    sys.exit(app.exec())

yaml側

ほとんどのGUIアプリは位置合わせの基準が「左上」になっていますが、それでは、中心にボタンを配置するのに不便です。

Qtデザイナで設計するなら問題ないかもしれませんが、直接パラメータを編集する時にわざわざ中心を計算するのも面倒だと思ったので、位置合わせの基準を「ウィジェット中心」にしました。これなら中心に1つ大きなボタンを配置するだけのGUIを作るときも位置合わせに手こずることはないでしょう。

type: stylesheet(9行目)に注目してください。フォントや色だけ定義しています。この電卓のボタンは12個ありますが、全てに同じものを適用する際にこれを12個も置くのは無駄だと考えたので、少し工夫しました。yaml側で見えないQLabelを定義した上で、pythonファイル側でstylesheetを更新することで同じ設定を12個全てに適用することができます。

更新されるのはstylesheetだけなので、ボタン側は位置や大きさの定義を書くだけでOKです。そのうち大きさも共通化できるといいかもしれません。

image:
  type: image
  x_center: 350
  y_center: 460
  width: 700
  height: 920
  path: "./image/back-image.png"
style_common:
  type: stylesheet
  font_size: 30
  font_color: "black"
  background_color: "lightgreen"
  font: "Ubuntu"
  font_bold: true
button_1:
  type: qpushbutton
  width: 200
  height: 100
  x_center: 150
  y_center: 400
  text: "1"
button_2:
  type: qpushbutton
  x_center: 350
  y_center: 400
  width: 200
  height: 100
  text: "2"
button_3:
  type: qpushbutton
  x_center: 550
  y_center: 400
  width: 200
  height: 100
  text: "3"
button_4:
  type: qpushbutton
  x_center: 150
  y_center: 500
  width: 200
  height: 100
  text: "4"
button_5:
  type: qpushbutton
  x_center: 350
  y_center: 500
  width: 200
  height: 100
  text: "5"
button_6:
  type: qpushbutton
  x_center: 550
  y_center: 500
  width: 200
  height: 100
  text: "6"
button_7:
  type: qpushbutton
  x_center: 150
  y_center: 600
  width: 200
  height: 100
  text: "7"
button_8:
  type: qpushbutton
  x_center: 350
  y_center: 600
  width: 200
  height: 100
  text: "8"
button_9:
  type: qpushbutton
  x_center: 550
  y_center: 600
  width: 200
  height: 100
  text: "9"
button_plus:
  type: qpushbutton
  x_center: 150
  y_center: 700
  width: 200
  height: 100
  text: "+"
button_0:
  type: qpushbutton
  x_center: 350
  y_center: 700
  width: 200
  height: 100
  text: "0"
button_equal:
  type: qpushbutton
  x_center: 550
  y_center: 700
  width: 200
  height: 100
  text: "="
lcd:
  lcd_number:
  type: qlcdnumber
  x_center: 350
  y_center: 200
  width: 400
  height: 100
  background_color: "lightgreen"

パッケージのインストール

最初にPyPIへのURLを示した通り、pipでインストールできます。

pip install PyamlQt

先ほどのスクリプトを実行すると巨大電卓が現れます。

これから

現時点でも既に私のニーズを満たしたパッケージにはなっていますが、さらに使いやすくするために次のようなアップデートをしていきたいと思っています。ぜひアイデアがあれば貢献頂けるととても嬉しいです。

  • stylesheetの要素の追加
  • include要素の追加(yamlの中にyamlやyamlへのURLを埋め込む)
  • 相対位置指定
  • ウィジェットの表示・非表示設定
  • 特定のアクション時の挙動指定(消えるとか)
  • GUIでデザインできるyamlエディタ or Qtデザイナ→yaml変換

Discussion