Python ツール #4 ― GUI でコマンド実行
GUI からコマンドをキックする Python スクリプトを開発してみました。
開発するに至った動機
Python で CUI のツールやユーティリティを開発して、なかなか良いものができたら、それを GUI でラップして .exe を開発して、その .exe を配布するという考えがあります。
Python で簡単に GUI を作る方法を探していたら、たまたま Flet という GUI ライブラリがあることを知りました。
今回、Flet の使用方法を学習してみることにしました。Python 内でツールの処理が完結するものでもよかったのですが、あえて Python からコマンドをキックするものを開発してみました。1
実行イメージ
GUI 起動時
[Parameter] テキストボックスにコマンドのパラメーター(繰返し回数)を入力し、[Execute] ボタンをクリックすると、コマンド(dummy_command.bat)をキックする。
[Close] ボタンをクリックすると、GUI を終了する。
コマンド実行中
[Execute] ボタン、[Close] ボタンはクリック不可にされている。
順次、コマンドの標準出力・標準エラーを表示していく。
右上端の [×] ボタンのクリック時
[Confirm] ダイアログが表示される。[Yes] ボタンをクリックするまでは、コマンドは実行され続け、コマンドの標準出力・標準エラーも表示され続ける。[Close] ボタンをクリックすると、GUI を終了する。
コマンド終了時
[Execute] ボタン、[Close] ボタンがクリック可に戻っている。
標準出力・標準エラーの行が多い場合は、画面左端にスクロールバーが表示される。
Flet
Flutter ベースの Python GUI ライブラリです。
Python の基礎知識を持っている、かつ Web 開発で Vuetify や React-Boot などを使用したことがあるという人は、簡単に開発できると思います。
ソースコードの簡単な説明
-
dummy_command.bat(コマンド)。あえて shift-jis で作成しています。
dummy_command.bat@rem @rem dummy_command.bat @rem @rem This file's encoding is shift-jis. @echo off setlocal echo start of dummy_command set parameter=%1 if "%parameter%" == "" ( echo Parameter is not specified. goto :abnormal_end ) echo parameter : %parameter% set "string=あいうえお かきくけこ さしすせそ たちつてと なにぬねの はひふへほ まみむめも やゆよ らりるれろ わをん" set "spacer= : " for /l %%i in (1,1,%parameter%) do ( powershell sleep 2 echo %%i%spacer%%string% ) echo end of dummy_command (normal end) exit /b 0 :abnormal_end echo end of dummy_command (abnormal end) exit /b 1
バッチファイルに指定したパラメーター回数分、文字列 “あいうえお ...” の出力と 2秒のスリープを繰り返す。
-
gui_command_executor.py(Python スクリプト)。
gui_command_executor.py#!/usr/bin/env python3 ・・・ # Import Libraries import os import subprocess import flet as ft # Constants CMD_DIR = '.\\bin' CMD_NAME = 'dummy_command.bat' CMD_ENCODING = 'shift-jis' DEBUG = False # Windows Size WINDOW_RATIO = 60 WINDOW_WIDTH = 16 * WINDOW_RATIO WINDOW_HEIGHT = 10 * WINDOW_RATIO LIST_SPACING = 5 LIST_PADDING = 10 TEXTBOX_LENGTH = 250 def write_message(page: ft.Page, list_view: ft.ListView, message: str): list_view.controls.append(ft.Text(message)) page.update() return # Execute Command def execute_command(page: ft.Page, list_view: ft.ListView, text_value: str) -> None: list_view.clean() param1 = text_value.strip() if param1 == '': write_message(page, list_view, 'Parameter is not specified.') return write_message(page, list_view, 'call') proc = subprocess.Popen( [CMD_NAME, param1], cwd=CMD_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding=CMD_ENCODING, text=True, shell=True) if DEBUG: print('start of read stdout') while proc.poll() is None: line_string = proc.stdout.readline() if not line_string: break if DEBUG: print('stdout > %s' % line_string.strip()) write_message(page, list_view, line_string.strip()) if DEBUG: print('end of read stdout') proc.wait() ret = proc.returncode proc.terminate() if DEBUG: print('return : %d' % ret) write_message(page, list_view, 'exit : %d' % ret) return # Main def main(page: ft.Page) -> None: os.environ['PATH'] += ';' + CMD_DIR def yes_click(e): # noqa page.window_destroy() def no_click(e): # noqa confirm_dialog.open = False page.update() confirm_dialog = ft.AlertDialog( modal=True, title=ft.Text('Confirm'), content=ft.Text('Are you sure?'), actions=[ ft.ElevatedButton('Yes', on_click=yes_click), ft.OutlinedButton('No', on_click=no_click), ], actions_alignment=ft.MainAxisAlignment.END, ) def window_event(e): if e.data == 'close': page.dialog = confirm_dialog confirm_dialog.open = True page.update() return def click_execute_button(e): # noqa text_value = str(compo_text.value) compo_execute_button.disabled = True compo_close_button.disabled = True execute_command(page, compo_list, text_value) compo_execute_button.disabled = False compo_close_button.disabled = False page.update() return def click_close_button(e): # noqa page.window_destroy() return page.title = 'Command Executor' page.scroll = ft.ScrollMode.ALWAYS page.window_width = WINDOW_WIDTH page.window_height = WINDOW_HEIGHT page.window_prevent_close = True page.on_window_event = window_event compo_text = ft.TextField(label='Parameter', hint_text="Input command parameter", value='', text_align=ft.TextAlign.LEFT, width=TEXTBOX_LENGTH) compo_execute_button = ft.FilledButton(text="Execute", on_click=click_execute_button) compo_close_button = ft.OutlinedButton(text="Close", on_click=click_close_button) compo_list = ft.ListView(expand=1, spacing=LIST_SPACING, padding=LIST_PADDING) page.add( ft.Row( [ compo_text, compo_execute_button, compo_close_button, ] ), ft.Row( [ compo_list, ] ) ) # Goto Main ft.app(target=main)
main()
GUI を描画し、テキストボックスでバッチファイルに渡すパラメーターを取得し、[Execute] ボタンのクリックで execute_command() を実行する。
execute_command()
バッチファイルを実行し、shift-jis で標準出力・標準エラーを読み込み、画面に表示する。- 後述の .exe の実行にて DOS プロンプト画面が開かれないようにするために、“subprocess.Popen()” のオプションに “shell=True” を指定している。
- 今回は、“os.environ['PATH']” で、コマンドへの相対パスを指定する方法を採用したが、“subprocess.Popen()” でコマンドへの絶対パスを指定する方法でもよい。
.py のコマンド化(.exe 化)
pyinstaller をインストールし、pyinstaller を実行する。“.\dist” 配下に “.exe” が作成される。
> D:
> cd D:\path\of\python_project
> pip install pyinstaller
> pyinstaller gui_command_executor.py --noconsole --onefile
> copy .\dist\gui_command_executor.exe .\
難点:
.exe のファイルサイズが大きい。
.exe を実行したとき、GUI が立ち上がってくるのがめちゃくちゃ遅い(2 ~ 5秒くらい?、フリーズしたかと思ってしまうことあり)。
ソースコードの置き場所
参考
Flet とは
Flet 一通り
-
GUI コマンド・キックを開発する際、よくハマるのが GUI 実行時に DOS プロンプト画面が開かれてしまうことかと思います。
約 20年前、Win32 API の CreateProcess() でハマったことがあります。製品リリース日が迫っていたので会社に二晩泊り込みでした。最終的な修正方法は、たった一行のビット・フラグ指定でした。
なので、あえてコマンド・キックにトライしてみました。案の定、ハマってしまいました。上述の通りフラグ “shell=True” によって対応できました。 ↩