PySimpleGUIでGUIアプリを作ってみた~その4~ Presenterで画面を操作する

PySimpleGUI

気づけば4記事目です。特に構成等考えずに思いつくままに書いた結果、話があっちいったりこっちいったりしてます。いつか元気があれば整理したいと思います。

さて、第3回の記事でイベント処理の基本を紹介しました。ボタンのクリック等に合わせて(key, values)が取得できるので、keyに合わせてやりたい処理を実装すれば良いという話です。

今回は上記でいう「やりたい処理」にフォーカスを当てて、具体的にどのように実装していったかを紹介します。

主にPresenterをどのような思想で実装したかというオブジェクト指向的な部分と、ViewにどのようにアクセスしていくかというPySimpleGUIのエレメント操作の部分の話になります。

Presenter実装方針

基本方針として以下のような思想で実装していきました。

  • 関数を4つに分類する(to Model, from Model, to View, from View)
  • イベント処理を上記4タイプの関数を組み合わせて作っていく
  • イベントの振り分けは別途handlerを用意する

presenter実装

具体的には以下のようなコードになります。実装コードから一部抜粋したコードになるので、それぞれの細かい処理はわかりにくいかもしれませんがご容赦ください。

import PySimpleGUI as sg

from src.models.hplc_data import ExperimentalData  # Modelです
from src.views.popup import NamePeakPopup  # オリジナルpopupです


class Presenter:
    def __init__(self, window: sg.Window):
        self.window = window  # メインwindow(View)をもちます。
        self.exp = ExperimentalData()  # Modelをもちます。(こちらも引数にしてもいいかもしれません)
        # ModelとViewを仲介するのでこのようにして両方にアクセスできるようにしておくとやりやすい気がします

    # -----------Modelを更新するタイプの関数(to Model)------------
    def set_peak_name(self, rrt_to_name: dict):
        self.exp.set_imp_name_in_exp(rrt_to_name)

    # -------Modelからデータを取得するタイプの関数(from Model)--------
    def get_new_table(self, sample_name):
        return self.exp.tables[sample_name]

    # -------------Viewを更新するタイプの関数(to View)------------
    def update_table(self, table_name):
        new_table = self.get_new_table(table_name)
        self.window['-TABLE-'].update(values=new_table.detail())

    def enable_button(self, key):
        self.window[key].update(disabled=False)

    @staticmethod
    def show_success(message):
        sg.popup(message)

    # --------Viewからデータを受け取るタイプの関数(from View)---------
    def receive_rrt_to_name(self):
        rrt_list = sorted(list(self.exp.rrt_set))
        popup = NamePeakPopup(rrt_list)
        rrt_to_name = popup.get_rrt_to_name()
        del popup
        return rrt_to_name

    # ---------------- Event process----------------
    # 4タイプの関数を組み合わせてイベント処理をつくる
    # 別途handlerを用意して
    def peak_name_event(self, _):  # 1つの関数で1つのイベント処理を担当する
        rrt_to_name = self.receive_rrt_to_name()  # from View
        if not rrt_to_name:
            return

        self.set_peak_name(rrt_to_name)  # to Model
        first_sample = self.exp.sample_name_list[0]
        self.update_table(first_sample)  # to View
        self.enable_button('-Exclude-')  # to View
        self.show_success('Name Peaks has completed')  # to View        

viewやmodelに対する処理を細かく分離することで、使い回しや編集が容易になります。

新しいイベント処理を書く場合は、まずviewやmodelへの細かい処理を記載したあと、#Event process以下にイベント関数を追加していく形になるので、見通しも良くなるかなと思います。

なお、Modelを更新する処理について、具体的な処理の内容はModel自体に記載しておき、PresenterはModelの関数を実行するだけという形にしています。

余談ですがfrom Modelの関数はget_xxxx、from Viewの関数はreceive_xxxxxという命名規則にしてみました。この方がパッとみてどのタイプの関数なのか見分けがつくかなと思います。(to Viewとto Modelを区分けする命名法は思いつきませんでした・・・)

handler実装

handlerによる振り分けは以下のように実装しました。

Presenterを引数にしてHandlerに渡し、keyに応じてPresenterのイベント関数を呼び出します。

今回はHandlerもPresenterも一つしかありませんが、もっと複雑なアプリを開発する際は、XxxHandler-XxxPresenter, YyyHandler-YyyPresenterのようにペアを増やしていけばいいのかなと思っています。

from src.presenter.presenter import Presenter


class Handler:
    def __init__(self, presenter: Presenter):  # presenterを引数にします
        self.presenter = presenter
        self.func_dict = {
            # key: event_funcの形でそれぞれのイベントを登録します
            '-PeakName-': self.presenter.peak_name_event,
        }

    def handle(self, event_key, values):
        if event_key not in self.func_dict:
            return
        event_func = self.func_dict[event_key]  # keyに応じた関数を呼び出して実行します
        event_func(values)

main関数

上記のようにPresenterやHandlerを実装した結果、メイン関数は以下のようになりました。結構スッキリしてると思うんですがどうでしょう。

def main():
    from src.presenter.presenter import Presenter
    from src.presenter.handler import Handler
    from src.views.view import InterFace

    interface = InterFace()
    presenter = Presenter(window=interface.window)
    handler = Handler(presenter)

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

        if event is None:
            break

    interface.close()


if __name__ == '__main__':
    main()

エレメント(画面の要素)の操作

上述した内容は思想に関するものなので、こんな風に実装していったら、コードが読みやすいし編集しやすいんじゃないかという内容でした。

ここからはPySimpleGUIでエレメント(画面の要素:ボタンやテキスト等)をどのように操作していくかを具体的に紹介していきます。

なお、PySimpleGUIでは画面の要素のことをElementと読んでいるようなので(Tkinterでいう所のウィジェットです)、本記事でもエレメントという言葉を使用していきます。

サンプルのまえがき

例として以下の画面を使用します。

リストボックス、テーブル、ボタンの3つのエレメントからなる画面です。テーブルはmath, englishの2つのカラムを持っています。

また内部のデータとして、AさんとBさんという二人の人物、また、それぞれの数学・英語のテストの点数のデータがあるとします。

import PySimpleGUI as sg

# 内部データとして以下のような値を持っているとする
points = {
    'A': [100, 80],
    'B': [70, 60]
}

list_box_style = {
    'values': ['None'],
    'select_mode': 'LISTBOX_SELECT_MODE_SINGLE',
    'enable_events': True,
    'size': (10, 10),
    'key': '-NameList-',
}

table_style = {
    'values': [[None, None]],
    'headings': ['math', 'english'],
    'num_rows': 10,
    'key': '-TABLE-'
}

# リストボックス、テーブル、ボタンからなる画面
layout = [[sg.Listbox(**list_box_style), sg.Table(**table_style)],
          [sg.Button('Listbox update', key='-Update-')]]

window = sg.Window(title='Sample', layout=layout)

while True:
    event, values = window.read()
    if event is None:
        break

window.close()

このサンプル画面に関して、以下の処理を実装します。

  • リストボックスを更新してAさん,Bさんを表示させる
  • テーブルにAさんまたはBさんの点数を表示させる
  • テーブルに表示させるのはリストボックスで選択された人物の点数とする

エレメントにアクセスする

エレメントを操作する流れは非常にシンプルで、操作したいエレメントをwindow[key]で参照したのち、各エレメントに用意されている関数を実行するだけです。

上記サンプル画面でリストボックスを参照したい場合は以下のように書きます。

listbox_element = window['-NameList-']

エレメントの関数を実行する

操作したいエレメントを参照できたら、あとはそのエレメントの持っている関数の中で都合のいいものを実行するだけです。

各エレメントがどのような関数を持っているかはバリエーションがたくさんあって紹介しきれないので、是非ドキュメントをながめてみてください。ここではリストボックスの持っている関数をいくつか紹介します。

update()

文字通りエレメントを更新する関数です。

Updateボタンを押した際に、リストボックスにA, Bを表示させる処理であれば、以下のように記載します。

# layout宣言部分は省略しています
window = sg.Window(title='Sample', layout=layout)

while True:
    event, values = window.read()
    if event is None:
        break

    elif event == '-Update-':  # ボタンが押された時に
        window['-NameList-'].update(values=['A', 'B'])  # リストボックスを更新する

        # 以下の書き方でも大丈夫です お好きな方で
        # list_box_element = window['-NameList-']
        # list_box_element.update()

window.close()

これでボタンを押した際に画面のリストボックスの中身が以下のように書きかえられます。

get()

リストボックスの中身をクリックすると以下のようにハイライトされます。(ハイライトカラーも引数で設定できます)

get()を実行することで、選択されている要素を取得することができます。

例えば以下のように実装すれば、リストボックスの要素が選ばれたタイミングで、その要素の値を取得することができます。

# layout宣言部分は省略しています
window = sg.Window(title='Sample', layout=layout)

while True:
    event, values = window.read()
    if event is None:
        break

    elif event == '-Update-':
        window['-NameList-'].update(values=['A', 'B'])
        # 以下の書き方でも大丈夫です お好きな方で
        # list_box_element = window['-NameList-']
        # list_box_element.update()

    elif event == '-NameList-':  # リストボックスの中身が選択された時
        name = window['-NameList-'].get()  # 選択されている要素を取得
        print(name)
        # ['A'] (リストで取得するので注意しましょう)

window.close()

組み合わせる

上記のような関数を組み合わせて色々とイベント処理を実装してきます。

冒頭に記載した、リストボックスで選ばれた人の点数データをテーブルに表示させる機能を実装すると以下のようになります。

# layout宣言部分は省略しています
window = sg.Window(title='Sample', layout=layout)

# 以下のような内部データを持っているとします
# csvやjsonファイルを読み込んで以下のようなデータを作るのでも良いですね
points = {
    'A': [100, 80],
    'B': [70, 60]
}

while True:
    event, values = window.read()
    if event is None:
        break

    elif event == '-Update-':  # ボタンが押された時
        names = points.keys()  # 内部データから名前リストをよびだして
        window['-NameList-'].update(values=names)  # リストボックスを更新

    elif event == '-NameList-':  # リストボックスの名前が選択された時
        name = window['-NameList-'].get()[0]  # 選択された名前を取得して
        if name not in points:
            continue

        point_data = points[name]  # 内部データを呼び出し
        window['-TABLE-'].update(values=[point_data])  # テーブルを更新

window.close()

おまけ(ボタンの順番を制御する)

他にこんなことできますよという紹介です。

例えば3つのボタンを用意して、それぞれ処理を実行していくけれど、ちゃんと順番に押してもらわないと困るというケースがあるかと思います。(特に個人でちょっとしたアプリを作る場合なんかはよくある悩みではないでしょうか?)

なにかしらのフラグを管理しておいて順番通りに押さないとエラーメッセージを出すというようなやり方も可能ですが、そもそもボタンを押せないようできれば思わぬエラーで困る確率が減りそうです。

以下のように実装します。

import PySimpleGUI as sg


layout = [[sg.Button('最初に押して', key='First')],
          [sg.Button('2番目に押して', key='Second', disabled=True)],
          [sg.Button('3番目に押して', key='Third', disabled=True)]]

window = sg.Window(title='Sample', layout=layout)

while True:
    event, values = window.read()
    if event is None:
        break

    elif event == 'First':
        window['Second'].update(disabled=False)

    elif event == 'Second':
        window['Third'].update(disabled=False)

window.close()

Buttonにはdisabledという引数があり、これをTrueにすることで画像のようにボタンがしろくなり(色も設定できます)、クリックしてもイベントが発生しないようになります。

これをupdateでdisabled=Falseにすることで有効かできます。個人的には結構便利かなと思ってます。

まとめ

  • Presenterの実装方針について(ModelやViewの操作を細切れにして実装していくといいかも)
  • Viewを操作するには、まずwindow[key]で対象のエレメントにアクセス
  • エレメントごとに用意されている関数を実行する。(update(), get()など)

なお、モデルの操作については様々なケースが想定される&GUIの操作とは基本無関係であるため割愛させていただきました。

コメント

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