内容
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)