PySimpleGUIでGUIアプリを作ってみた ~その1~

PySimpleGUI

GUIアプリという表現が適切なのかはわかりませんが、デスクトップアプリを作ってみたので日記も兼ねてこんな事出来ますよというのを主に実装面について書きなぐっていきます。とりあえず第一弾。主にポップアップの話をします。

アプリの機能概要が知りたい方はこちら

コード全部見たい方はこちら(Github)

記事のコンセプト

  • Pythonでデスクトップアプリ(GUI)を作りたい人向け
  • GUIアプリの作成にPySimpleGUIがオススメですよというメッセージ
  • PySimpleGUIで実装した機能(公式サイトに載ってないちょっと複雑めのポップアップ)の紹介

PySimpleGUIについて

Pythonでデスクトップアプリを作ろうと考えてGoogle検索すると、高い確率でTkinterに出会います。ただ、Tkinterは非常にシンプルなんですが、実装していてなんか色々やりにくかったです。

特に気になったのは以下の3つです。私の力不足な部分も多分にあるとは思うんですが、公式の解説が不親切なのは如何ともしがたいところでした。

  • 公式ドキュメントが割としょぼい(公式を見てもわからないことだらけです)
  • 入力データを受け取って内部で処理したい時に、xxxVarを使うのがやりづらい
  • ウィジェット(テキストとかボタンとか)の配置が非直感的でなかなかうまくいかない

色々困っていた時に、たまたま見かけたPySimpleGUIを使ってみたらかなり実装しやすかったのでこちらにチェンジしました。特に公式の解説が充実しているのが個人的には嬉しかったです。

Tkinterで私と同じような悩みを感じていて、PySimpleGUIに興味を持った方は、是非他の方の記事や公式を見てみてください。特に基本的な使い方は公式でわかりやすく解説されています。(英語がわからない??そんなのみんな一緒です。google翻訳で頑張りましょう)

Tkinterを使うのであればPySimpleGUIを使ってみたらという話

PySimpleGUIが今キてるかもしれない

公式クックブック

実装した機能紹介

今回私が実装した機能の中から、公式そのままコピペじゃ実装できない程度には複雑めの機能を紹介します。基本的な仕組みは繰り返しますが公式かリンクの記事を読んでください。

オリジナルのポップアップ

なにかボタンを押すとポップアップが出てきて、数値を入力するなどして処理を進めていくというのはよくあるケースだと思われます。

PySimpleGUIの場合ポップアップが最初からいくつも用意されていてファイルダイアログやテキスト入力、OK or Cancelボタンなど、それ欲しかった!と思うようなものがすぐに使えます。

標準のポップアップを知りたい方は以下の記事が

ただ、ちょっと複雑めのポップアップを実装する方法が、公式を見た限りでは分からなかったので以下紹介します。

私がやりたかったのは以下の内容です。

  • すでにメイン画面に何かデータが入力されていて、そのデータを反映したチェックボックス付きのポップアップを表示させること
  • ポップアップでチェックされた項目をset型のデータとして受け取ること

具体的には下図のような感じです。メイン画面に名前(Name)が入力されていて、それらのリストをチェックボックス付きで表示させました。

以下作業環境はPython 3.7, PySimpleGUI 4.16.0です。

チェックボックス(データを反映させるタイプ)

他にもやり方があるとは思いますが、私は以下のように実装してみました。

入力されている名前リスト(これはwindow.read()で取得できます:公式参照、あるいは内部で変数として保持しておきます)を引数とするクラスとして実装しました。

import PySimpleGUI as sg


class ExcludePopup:
    def __init__(self, imp_name_list):  # 名前のリストを引数として受け取る
        self.layout = [[sg.Text('Check Exclude Peak', size=(20, 1))]] + \
                 #  名前のリストからチェックボックスを作る(リスト内包表記になっています)
                 [[sg.Checkbox(name)] for name in imp_name_list if name is not None] + \
                 [[sg.OK(), sg.Cancel()]]

        self.window = sg.Window(title='Set Exclude', layout=self.layout)
        self.excluded = set()
        self.imp_name_list = imp_name_list

    def get_excluded(self):
        while True:
            event, values = self.window.read()

            if event in ('Exit', 'Quit', 'Cancel', None):
                break  # Cancelされたら画面を閉じます

            elif event == 'OK':
                # values.values()でチェックボックスの状態がTrue or Falseのリストで得られます
                for name, check in zip(self.imp_name_list, values.values()):
                    if check:
                        self.excluded.add(name)  # チェックが入っているやつをセットに入れていきます
                break  # Okでも画面を閉じます

        self.window.close()  # windowを閉じて
        return self.excluded  # チェックボックスから得た情報を返します

上記で実装したポップアップを以下のようにメインの画面で呼び出します。

import PySimpleGUI as sg


from views.popup import ExcludePopup  # 上で作成したクラスをimport

#  ボタンのみの簡単なレイアウトです
output_frame = [sg.Frame('Output', font='Any 15', layout=[
                [sg.Button('Ok', key='btn')]
                ])]

layout = [output_frame]

window = sg.Window(title='LC Data Manager',layout=layout)

while True:
    event, values = window.read()

    if event in ('Exit', 'Quit', None):
        break

    elif event == 'btn':  # ボタンが押された時に
        name_list = ['ab', 'rr', 'yy']  # 名前のリストを何らかの手法で呼び出します
        popup = ExcludePopup(name_list)
        excluded = popup.get_excluded()  # 名前のリストを引数にしたポップアップを呼び出します 
        del popup  # エラーのもとになるみたいなので、ポップアップを閉じる時にしっかりdeleteしておきます
        print(excluded)

window.close()

上記の実装で下図のような画面が得られます。左の小さい画面がメイン画面で、OKを押すと右側のポップアップが表示されます。チェックをいれたデータがOKを押すと返ってくるので、あとは好きなように処理できます。

余談ですが、ポップアップ画面はメイン画面とは違うファイルに記入しておいて、メインからImportする方がコードがスッキリする気がします。オブジェクト指向とやらと四苦八苦しながらフォルダ構成を考えてみたのでいつか晒してみたいと思います。

2つの画面を扱う際の注意点

注意 2つのwindowを扱う際、2つ目のwindowを開く時は、毎回layoutも改めて宣言しなければいけないことに注意してください。

具体的には以下のような実装ではサブ画面を一度開いて閉じたあと、もう一度開こうとした際にエラーが発生します。layoutは使い捨てだというイメージを持っておくと良いかと思います。

このため今回の実装でもポップアップを呼ぶ際は毎回コンストラクタでlayoutを宣言しています。

import PySimpleGUI as sg

main_layout = [[sg.Text('Main Window'), sg.OK()]]
main_window = sg.Window('Main', layout=main_layout)

# サブ画面のレイアウトを先に宣言する
# この方法だとサブ画面を1度しか開くことができず、2回目以降エラーが発生
sub_layout = [[sg.Text('Sub Window'), sg.Cancel()]]

while True:
    main_event, main_value = main_window.read()

    if main_event is None:
        break

    elif main_event == 'OK':
     # メイン画面でOKが押された時にサブ画面を開く
        sub_window = sg.Window('Sub', layout=sub_layout)
        while True:
            sub_event, sub_value = sub_window.read()

            if sub_event is None:
                break
        sub_window.close()

main_window.close()

上記では最初にサブ画面のレイアウトを1度だけ宣言して使いまわそうとしているのでエラーが発生します。エラーを回避するためには以下のようにします。

import PySimpleGUI as sg

main_layout = [[sg.Text('Main Window'), sg.OK()]]
main_window = sg.Window('Main', layout=main_layout)


while True:
    main_event, main_value = main_window.read()

    if main_event is None:
        break

    elif main_event == 'OK':
        # サブ画面を開く時に毎回layoutを宣言する
        # エラーが発生せず、何度でもポップアップを開閉できる!
        sub_layout = [[sg.Text('Sub Window'), sg.Cancel()]]
        sub_window = sg.Window('Sub', layout=sub_layout)
        while True:
            sub_event, sub_value = sub_window.read()

            if sub_event is None:
                break

        sub_window.close()

main_window.close()

スクロールバー & テキストボックス

またポップアップに表示させる情報が多い場合は以下のようにsg.Columnを使うとスクロールバーで表示させられます。ちなみに以下はチェックボックスではなくテキストボックスを複数表示させるポップアップになります。

import PySimpleGUI as sg


class NamePeakPopup:
    def __init__(self, rrt_list):
        # スクロール表示させたい画面構成を指定
        cols = [[sg.Text('RRT', size=(5, 1)), sg.Text('Name', size=(15, 1))]] + \
               [[sg.Text(str(rrt), size=(5, 1)), sg.Input(size=(15, 1))] for rrt in rrt_list]
     
     # 上記で指定した内容をsg.Columnの引数に入れる。 scrollableをTrueにする
        self.layout = [[sg.Column(cols, scrollable=True , vertical_scroll_only=True, size=(200, 400))],
                       [sg.OK(), sg.Cancel()]]
        self.window = sg.Window(title='Name Peaks', layout=self.layout)
        self.rrt_list = rrt_list
        self.rrt_to_name = {} # dict

    def get_rrt_to_name(self):
        while True:
            event, values = self.window.read()

            if event in ('Exit', 'Quit', 'Cancel', None):
                break

            elif event == 'OK':
                for rrt, name in zip(self.rrt_list, values.values()):
                    if not name:
                        name = None
                    self.rrt_to_name[rrt] = name
                break

        self.window.close()
        return self.rrt_to_name  # 今回はdict型で返しています
import PySimpleGUI as sg


from views.popup import NamePeakPopup


output_frame = [sg.Frame('Output', font='Any 15', layout=[
                [sg.Button('Ok', key='btn')]
                ])]

layout = [output_frame]

window = sg.Window(title='LC Data Manager',layout=layout)

while True:
    event, values = window.read()

    if event in ('Exit', 'Quit', None):
        break

    elif event == 'btn':
        name_list = list(range(50)) # 表示させたいリスト(ここでは0~49)
        popup = NamePeakPopup(name_list)
        dict = popup.get_rrt_to_name()
        del popup
        print(excluded)

window.close()

上記のコードで得られるのは以下のような画面です。引数にしたリストを表題としたテキストボックスがスクロールバー付きで複数表示できます。また、OKボタンを押した際に表題と入力内容がdict型で返るようになっています。

まとめ

PySimpleGUIで実装したポップアップの話でした。

メインのWindowとは別のWindowを持ったクラスとして実装し、適宜メインからデータを受け取り画面を開く&メインにデータを返す感じで実装すれば応用が効くのではないかと思います。

他のやり方もあるよ!というコメントお待ちしてます!

コメント

タイトルとURLをコピーしました