LoginSignup
0
0

Python ツール #4 ― GUI でコマンド実行

Last updated at Posted at 2024-05-25

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 を終了する。

Python_tool-GuiCommandExecutor_1-Init.jpg

コマンド実行中

[Execute] ボタン、[Close] ボタンはクリック不可にされている。
順次、コマンドの標準出力・標準エラーを表示していく。

Python_tool-GuiCommandExecutor_2-Running.jpg

右上端の [×] ボタンのクリック時

[Confirm] ダイアログが表示される。[Yes] ボタンをクリックするまでは、コマンドは実行され続け、コマンドの標準出力・標準エラーも表示され続ける。[Close] ボタンをクリックすると、GUI を終了する。

Python_tool-GuiCommandExecutor_3-Cancel.jpg

コマンド終了時

[Execute] ボタン、[Close] ボタンがクリック可に戻っている。
標準出力・標準エラーの行が多い場合は、画面左端にスクロールバーが表示される。

Python_tool-GuiCommandExecutor_4-Ended.jpg

Flet

Flutter ベースの Python GUI ライブラリです。
Python の基礎知識を持っている、かつ Web 開発で Vuetify や React-Boot などを使用したことがあるという人は、簡単に開発できると思います。

ソースコードの簡単な説明

  • プロジェクト構成
    Python_tool-GuiCommandExecutor_0-Project.jpg

  • 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
    
    #
    # gui_command_executor.py
    #
    # Date    : 2024-05-25
    # Auther  : Hirotoshi FUJIBE
    # History :
    #
    # Copyright (c) 2024 Hirotoshi FUJIBE
    #
    
    # 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 一通り

  1. GUI コマンド・キックを開発する際、よくハマるのが GUI 実行時に DOS プロンプト画面が開かれてしまうことかと思います。
    約 20年前、Win32 API の CreateProcess() でハマったことがあります。製品リリース日が迫っていたので会社に二晩泊り込みでした。最終的な修正方法は、たった一行のビット・フラグ指定でした。
    なので、あえてコマンド・キックにトライしてみました。案の定、ハマってしまいました。上述の通りフラグ “shell=True” によって対応できました。

0
0
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
0
0