Python GUIソフトのexeファイル作成
目標
Pythonで作成したGUIソフトをexeファイルにし、配布可能なGUIソフトを作成する。
Python環境の設定
最終的にexeファイルを作成する時、Pythonの場合は現在のPython環境にインストールしたライブラリが全てコンパイルされてしまう。その場合、作成されたソフトの動作が遅くなったり、不必要にソフトのファイル容量が重くなる。
Pythonでコンパイルを行う場合は、先に特別環境を作成することが望ましい。
pyenv, pyenv-virtualenv, venv, Anaconda, Pipenv場合によってはDockerなど選択枝はあるが、今回はPipenvを使用する。
使い方は以下参照。上から順にインストール、pythonバージョン指定して初期化。仮想環境へ入る。仮想環境を出る。仮想環境を削除。pipfileから仮想環境再構築。pipenv shellで仮想環境に入ってからrequirements.txtを用いてpipでライブラリをインストール。
$ pip install pipenv
$ pipenv --python 3.8
$ pipenv shell
$ exit
$ pipenv --rm
$ pipenv install
$ pip install -r requirements.txt
Pythonのバージョンとライブラリを確認する。
$ pip list
$ python --version
Python GUIの作成
Tkinter, PySimpleGUI, Kivy, PyQt, wxPython,fletから選択する。
今回は簡単に使えるfletを用いる。
fletはマルチプラットフォーム対応、ホットリロード対応などの利点があり、現状最もモダンなライブラリと考えた。
GUIの書き方について、tkinterなどのよく使うライブラリであれば、ChatGPTに書き方を指南していただくこともできるが、ChatGPTはfletについての知識がない。
簡単な操作を学ぶため、以下の記事のスクリプトを使う。
$ git clone https://github.com/AWtnb/flet_4up_idphoto
上記参考をもとに私が作成したスクリプトを以下に示す。
gui_flet2.py
from pathlib import Path # ファイルパスを操作するためのPathクラスをインポート
import flet as ft # fletライブラリをインポート
import plot_dynamic_max as pdm # プロット関連の関数を含むスクリプトをインポート
def main(page: ft.Page):
# ページのタイトルとテーマを設定
page.title = "structure dynamic grapher"
page.theme_mode = "light" # light or dark
# UI要素のための変数を作成
target_file = ft.Ref[ft.Text]() # CSVファイルの読み込み先を保持する変数
output_folder = ft.Ref[ft.Text]() # グラフの保存先を保持する変数
result_message = ft.Ref[ft.Text]() # 処理結果のメッセージを保持する変数
file_extensions = ["csv"] # 許可されるファイルの拡張子のリスト
ui_rows = [] # UI要素の行を格納するリスト
###################################
# file picker
###################################
def on_file_picked(e: ft.FilePickerResultEvent): # ファイルピッカーでファイルが選択されたときの処理
if e.files:
print(e.files[0], type(e.files[0]))
target_file.current.value = e.files[0].path # 選択されたファイルのパスを取得
output_folder.current.value = str(Path(target_file.current.value).parent) # 選択されたファイルの親ディレクトリを取得
page.update()
file_picker = ft.FilePicker(on_result=on_file_picked) # ファイルピッカーを作成
page.overlay.append(file_picker) # ページにファイルピッカーを追加
def show_file_picker(_: ft.ControlEvent): # ファイルピッカーを表示する関数
file_picker.pick_files(
allow_multiple=False,
file_type="custom",
allowed_extensions=file_extensions
)
ui_rows.append(ft.Row(controls=[
ft.ElevatedButton("Select File", on_click=show_file_picker),
ft.Text(ref=target_file)
]))
#select_file_button = ft.ElevatedButton("Select File", on_click=show_file_picker)
#select_file_button_container = ft.Column(controls=[select_file_button], )
# ボタンを円形にする場合
"""
ui_rows.append(ft.Column(
controls=[
ft.ElevatedButton(
"Select File", on_click=show_file_picker,
style=ft.ButtonStyle(shape=ft.CircleBorder(), padding=30),
),
]
))
"""
###################################
# folder picker
###################################
def on_folder_picked(e: ft.FilePickerResultEvent): # フォルダピッカーでフォルダが選択されたときの処理
if e.path:
output_folder.current.value = e.path # 選択されたフォルダのパスを取得
page.update()
folder_picker = ft.FilePicker(on_result=on_folder_picked) # フォルダピッカーを作成
page.overlay.append(folder_picker) # ページにフォルダピッカーを追加
def show_pick_folder(_: ft.ControlEvent): # フォルダピッカーを表示する関数
folder_picker.get_directory_path()
ui_rows.append(ft.Row(controls=[ # フォルダ選択ボタンと選択結果のテキストを含むUI要素を追加
ft.ElevatedButton("Select Output Folder", on_click=show_pick_folder), # フォルダ選択ボタン
ft.Text(ref=output_folder) # 選択されたフォルダのパスを表示するテキスト
]))
###################################
# execute button
###################################
def execute(_: ft.ControlEvent): # 実行ボタンがクリックされたときの処理
if not target_file.current.value or not output_folder.current.value: # ファイルまたはフォルダが選択されていない場合は処理しない
return
result_message.current.value = "" # 結果メッセージをクリア
page.update() # ページを更新
if str(Path(target_file.current.value).suffix)[1:].lower() in file_extensions: # 選択されたファイルの拡張子が許可されている場合
ui_controls.disabled = True # UIを無効化
save_ = pdm.path_to_graph(target_file.current.value, output_folder.current.value) # グラフ作成関数呼び出し
ui_controls.disabled = False # UIを有効化
result_message.current.value = "FINISHED!" # 処理結果を表示
ui_rows.append(ft.Image(save_, height=500, fit=ft.ImageFit.CONTAIN)) # 生成されたグラフを表示
else:
result_message.current.value = "incorrect file..." # 不正なファイルが選択された場合はエラーメッセージを表示
page.update() # ページを更新
ui_rows.append(ft.Row(controls=[ # 実行ボタンと結果メッセージを含むUI要素を追加
ft.FilledButton("GO!", on_click=execute), # 実行ボタン
ft.Text(ref=result_message) # 処理結果のメッセージを表示するテキスト
]))
###################################
# render page
###################################
ui_rows = [ft.Text(
"地震応答解析最大応答値",
style="labelSmall",
weight="bold",
color=ft.colors.BLUE, # ラベルのスタイルを設定
)] + ui_rows
ui_controls = ft.Column(controls=ui_rows) # UI要素を含むコラムを作成
page.add(ui_controls) # UIをページに追加
ft.app(target=main)
# GUIテスト : flet run gui_flet2.py -d
GUIソフトの機能例
地震応答解析の最大応答値データのグラフ化を行う。
plot_dynamic_max.py
import csv # csv読取にもpandasというライブラリを使うことが多い
import os
from pathlib import Path
import mc_list as mc
from matplotlib import pyplot as plt
import japanize_matplotlib
def read_csv(filename):
csv_data = []
with open(filename, encoding='ms932', newline='') as f:
csvreader = csv.reader(f)
for row in csvreader:
csv_data.append(row)
csv_datat = list(zip(*csv_data)) # 2次元配列転置
csv_dict = {} # dict形式に
for i in range(len(csv_datat)):
row = list(csv_datat[i][1:]) # 反転のためにリスト化
row.reverse() # データの反転
try:
# float加工できるものは加工
csv_dict[csv_datat[i][0]] = [float(x) for x in row]
except: # 加工できないものは文字列のまま
csv_dict[csv_datat[i][0]] = row
return csv_dict
def mm_to_inch(mm):
return mm/25.4
def graph_Q(csv_dict, save):
plt.rcParams['figure.figsize'] = (mm_to_inch(70), mm_to_inch(140)) # figure size in inch, 横×縦
plt.rcParams['font.family'] = 'Times New Roman' # font familyの設定
plt.rcParams['mathtext.fontset'] = 'stix' # math fontの設定
plt.rcParams["font.size"] = 9 # 全体のフォントサイズが変更されます。
plt.rcParams['xtick.labelsize'] = 9 # 軸だけ変更されます。
plt.rcParams['ytick.labelsize'] = 9 # 軸だけ変更されます
plt.rcParams['legend.fontsize'] = 6 # 凡例の文字だけ小さく
plt.rcParams['xtick.direction'] = 'in' # x axis in
plt.rcParams['ytick.direction'] = 'in' # y axis in
plt.rcParams['axes.linewidth'] = 1.0 # axis line width
plt.rcParams['axes.grid'] = True # make grid
plt.rcParams["legend.fancybox"] = False # 丸角
plt.rcParams["legend.framealpha"] = 1 # 透明度の指定、0で塗りつぶしなし
plt.rcParams["legend.edgecolor"] = 'black' # edgeの色を変更
#plt.rcParams["legend.handlelength"] = 1 # 凡例の線の長さを調節
#plt.rcParams["legend.labelspacing"] = 5. # 垂直(縦)方向の距離の各凡例の距離
#plt.rcParams["legend.handletextpad"] = 3. # 凡例の線と文字の距離の長さ
#plt.rcParams["legend.markerscale"] = 2 # 点がある場合のmarker scale
#plt.rcParams["legend.borderaxespad"] = 0. # 凡例の端とグラフの端を合わせる
plt.rcParams['figure.dpi'] = 300
fig = plt.figure()
fig_1 = fig.add_subplot(111)
numeric_y_data_10 = range(len(csv_dict['階'])) # 階のデータ個数に対応するy軸リスト。[0,1,***,30]
waves = list(csv_dict.keys())[1:] # 地震波名
# 各波のデータをプロット
max_Q = 0
for i in range(len(waves)):
print(f"地震波名:{waves[i]}")
wave_name = waves[i][7:11] # 地震波名_短縮系
fig_1.plot(csv_dict[waves[i]], numeric_y_data_10, marker=mc.mc_list[i][0], markersize=3, markeredgewidth=0.5, markeredgecolor='k', color=mc.mc_list[i][1], label=wave_name)
if max(csv_dict[waves[i]]) > max_Q:max_Q=max(csv_dict[waves[i]]) # 全ての波での最大せん断力取得
fig_1.set_xlabel("せん断力 [kN]", font='IPAexGothic')
fig_1.set_ylabel("階", font='IPAexGothic')
# グラフの設定
#plt.xlabel("せん断力")
y_max = max(numeric_y_data_10) + 0.5
y_min = min(numeric_y_data_10) - 0.5
plt.ylim(y_min, y_max) # Y軸の範囲を1から30に設定
plt.yticks(numeric_y_data_10, csv_dict['階']) # 数値と階を対応させる。
plt.xticks(rotation=45) # X軸のラベルを45度回転して表示
xdelta = 2000
plt.xticks(range(0, int(max_Q)+xdelta, xdelta)) # X軸の目盛りを1000刻みに設定
plt.yticks(numeric_y_data_10, csv_dict['階']) # 数値と階を対応させる。
plt.legend()
#plt.show()
# save
fig.savefig(save, bbox_inches="tight", pad_inches=0.05)
def path_to_graph(file_path, dir): # GUIからの呼び出し用
#file_name, file_extension = os.path.splitext(file_path)
name_only = Path(file_path).stem # ファイルの拡張子抜きの名前のみ
save1 = name_only + '.png'
save = os.path.join(dir, save1)
csv_dict = read_csv(file_path)
graph_Q(csv_dict, save)
return save
def main(): # 単体テスト用
file_path = 'せん断力最大応答.csv'
file_name, file_extension = os.path.splitext(file_path)
save = file_name + '.png'
csv_dict = read_csv(file_path)
graph_Q(csv_dict, save)
if __name__ == "__main__":
main()
exeファイルの作成
Pyinstaller, Nuitka(ニュイティカ),cx_Freezeから選択。
Pyinstaller:重いファイルが作成される。起動時間が長い。
Nuitka :ビルド時間が長いが軽いファイルができる。私の環境では起動が安定しなかった。fletには現状対応していないらしい。
cx_Freeze:重いファイルが作成される。起動時間が短い。
今回はcx_Freezeを用いる。
仮想環境内で以下のコマンドを入力する。
$ cxfreeze -c gui_flet2.py --target-dir gui_flet2
以上で自作ソフトが配布可能になる。
Discussion