2
3

pysimplegui + pyinstallerで簡単なツールを配布する

Last updated at Posted at 2023-07-16

内容

pythonで自分用に作っていた簡単なデバッグツールを、内輪限定で配布することになった。
pythonを入れていない人やCLIに慣れていない人もいたため適当にGUIをつけ、.exe化したので、その手順や過程で覚えたことを書きます。

最小限のGUIをつけて配布する

以下のコードのparameter/somefuncを書き換えます

import PySimpleGUI as sg

# parameter
Param1 = 3

# 自作処理
def somefunc():
    import time
    time.sleep(Param1)
    return 'End'


def main():
    # GUIのコード
    # レイアウト定義
    layout = [[sg.Button('Run'), sg.Button('Exit')]]

    # Window生成
    window = sg.Window('ツール名', layout)

    # Event Loop to process "events" and get the "values" of the inputs
    while True:
        event, values = window.read()
        if event == sg.WIN_CLOSED or event == 'Exit':  # if user closes window or clicks exit
            break
        elif event == 'Run':            # Runボタン実行時
            somefunc()

    window.close()


if __name__ == '__main__':
    main()

pyinstallerを使って、上記.pyファイルを.exe化します。
カレントディレクトリにqiita.pyという名前で保存したとして、以下のコマンドを実行します

pip install pyinstaller
pyinstaller -wF ./qiita.py

distというフォルダに、qiita.exeファイルが生成されます。ダブルクリックで実行できます。

過程や結果を出力する

レイアウトのコードに、マルチライン(テキストエリア)を追加します
レイアウトのコードは2次元配列で記述します

    layout = [[sg.Button('Run'), sg.Button('Exit')],
              # 下3行を追加
              [sg.HSeparator()],
              [sg.Multiline(size=(65, 20), key='MultiLine', autoscroll=True,
                            reroute_stdout=True, write_only=True],]

GUIにマルチラインを追加した状態でコード中でprint()すると、マルチライン上に表示されて確認することができます。

パラメータを可変にする

レイアウト上にインプットエリアやチェックボックスを追加して、設定値を読み取ります。
設定値は、イベントループの変数valueに設定され、keyを使って読み取ることができます。

レイアウトのコード

    layout = [
        # 下2行を追加
        [sg.InputText('初期値1', key='Param1'), sg.Checkbox(
            'チェックボックス', default=True, key='Check1')],
        [sg.Button('Run'), sg.Button('Exit')],
        [sg.HSeparator()],
        [sg.Multiline(size=(65, 20), key='MultiLine', autoscroll=True,
                      reroute_stdout=True, write_only=True, reroute_cprint=True)],]

入力値読み取り個所のコード(Eventloop内)

    # valuesの読み取り(抜粋)
    event, values = window.read()
    if event == 'Run':            # Runボタン実行時
        somefunc(values)

関数(引数で値を読み取り)

    def somefunc(values):
        print(values['Param1'])
        if values['Check1']:
            print("チェックされています。")

時間がかかる処理をスレッド化する

Runボタンを押した後、自作の処理を実行している間GUIは反応しなくなります。
Webへのリクエストなどをするときは時間がかかるので、ボタンを押したときにスレッドを生成して、そのスレッドで実行することでGUIを止めないようにできます。Runボタンを連打させない対策も必要です。

# threadingのインポートを追加
import threading

ボタンを押したときのコード(Eventloop内)

        elif event == 'Run':            # Runボタン実行時
            # ボタンを無効化する(連打対策)
            window['Run'].update(disabled=True)
            # スレッドで実行(この時、args=にてパラメータを設定できる
            threading.Thread(target=somefunc, args=(
                window, values), daemon=True).start()

自作処理の例

def somefunc(window, values):
    import time
    for i in range(1, 6):
        sg.cprint(f"{i}... ", end="", text_color="green")
        time.sleep(1)
    sg.cprint("Finish!", text_color="blue")
    # 完了後、window上のボタンを有効化
    window['Run'].update(disabled=False)

出力にこだわる

sg.Multiline()のオプション、reroute_xxxxには、stdout, stderr, cprintがあります。
rerout_xxxx=Trueに設定した場所に出力されます。(※複数設定した場合、一番最後の要素)

要素名 デフォルト
stdout(標準出力) コンソール
stderr(エラー出力) コンソール
sg.cprint() コンソール(ただしWarningが出る)

sg.cprint()は文字色、背景色を設定できます。

sg.cprint("表示メッセージ", text_color="blue", background_color="lightgray")

設定項目が増えてきたらタブを分ける

プロパティやボタンの数が増えてきた場合、タブを使って分けてしまうとすっきりするのでお勧めです。
タブごとにレイアウト定義(2次元配列)を作り、親のレイアウトで、sg.TabGroup()->sg.Tab()を設定します。
(TabGroupの中のいずれか一つのタブが前面に表示されるイメージ)

    main_tab = [[sg.Text("main setting"), sg.InputText("default value")],
                [sg.Button("Run"), sg.Button("Exit")],
                [sg.HSep()],
                [sg.Multiline(size=(65, 20), key='-ML-', autoscroll=True,
                              reroute_stdout=True, write_only=True, reroute_cprint=True)],
                ]

    config_tab = [[sg.Text("param1"), sg.InputText("default param")],
                  [sg.Text("param2"), sg.InputText("default param")],
                  ]

    layout = [[sg.Text("Sample")],
              [sg.TabGroup([
                  [sg.Tab('Main', main_tab), sg.Tab('config', config_tab)]
              ])],
              ]

設定値の保存、読み込みを可能にして、設定ファイルとセットで配る

GUIの設定値をjsonファイルに出力できるようにします。また、jsonファイルから読み込んだ設定値を反映します。
環境固有の設定などを切り出して配ることができます

    # レイアウト
    config_tab = [[sg.Text("param1"), sg.InputText("default param", key='_config1')],
                  [sg.Text("param2"), sg.InputText(
                      "default param", key='_config2')],
                    # 下2行を追加(ファイル選択・保存ボタン及びファイル選択・読み込みボタン)
                  [sg.Input(key="_savepath"), sg.FileSaveAs(button_text="保存先選択",
                                                            file_types=(("json", "*.json"),), default_extension="setting.json"), sg.Button("保存する")],
                  [sg.Input(key="_loadpath"), sg.FileBrowse(file_types=(
                      ("json config file", "*.json"),)), sg.Button("読み込む")]
                  ]

    # 設定名とkey='xxx'を紐づける辞書を用意しておく
########################################## Load/Save Settings File ##########################################
# "Map" from the settings dictionary keys to the window's element keys
SETTINGS_KEYS_TO_ELEMENT_KEYS = {
    'main1': '_main1', 'cfg1': '_config1', 'cfg2': '_config2'}


    # jsonファイル読み込み、GUIへの反映、jsonファイルへの保存関数
def load_settings(settings_file):
    try:
        with open(settings_file, 'r') as f:
            settings = jsonload(f)
    except Exception as e:
        sg.popup_quick_message(f'exception {e}', 'No settings file load...',
                               keep_on_top=True, background_color='red', text_color='white')
        return None
    return settings


def update_settings(settings, window):
    for key in SETTINGS_KEYS_TO_ELEMENT_KEYS:   # update window with the values read from settings file
        try:
            window[SETTINGS_KEYS_TO_ELEMENT_KEYS[key]].update(
                value=settings[key])
        except Exception as e:
            print(
                f'Problem updating PySimpleGUI window from settings. Key = {key}')


def save_settings(settings_file, settings, values):
    if values:      # if there are stuff specified by another window, fill in those values
        for key in SETTINGS_KEYS_TO_ELEMENT_KEYS:  # update window with the values read from settings file
            try:
                settings[key] = values[SETTINGS_KEYS_TO_ELEMENT_KEYS[key]]
            except Exception as e:
                print(
                    f'Problem updating settings from window values. Key = {key}')

    with open(settings_file, 'w') as f:
        jsondump(settings, f)

    sg.popup('Settings saved')

ボタンクリック時の処理(Eventloop内)

        elif event == '保存する':
            save_settings(values['_savepath'], {}, values)
        elif event == '読み込む':
            settings = load_settings(values['_loadpath'])
            update_settings(settings, window)


2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3