その1: プログレスバーとリアルタイム出力表示
その2: コンソール風出力表示
その3: キャンセルボタン
の続きです.
やりたいこと
- その3のスクリプトをpyinstallerでexe化すると,キャンセルボタンの挙動がおかしくなってしまう.これを解決し,pyinstallerでexe化したファイルでもスクリプトが正常に動作するようにする.
- ついでにウインドウの閉じるボタンが押されたときの挙動もきちんと設定する.
環境
Windows 10 Pro, Python 3.7, miniconda(conda 4.11.0), PyInstaller 5.6.2
pyinstaller
以下のコマンド(オプション)を使ってexe化する.
pyinstaller hogehoge1.py --noconsole -i .\icon-file.ico -n moviecreate
- (見た目の問題で)コンソールは表示させたくないので
--noconsole
. - ここでは
moviecreate.exe
という名前の実行ファイルを作る.(もちろん名前は何でも良い)
コード
import os, subprocess
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import signal
import sys
command = "ffmpeg hogehoge" # ffmpegの処理コマンド
def start():
progress = tk.Toplevel()
progress.geometry("750x350")
progress.title("processing...")
progress.grab_set()
progress.focus_set()
bar = ttk.Progressbar(progress,mode='indeterminate')
bar.pack(side = tk.TOP, fill = tk.X)
info = tk.Text(progress)
info.bind("<Key>", lambda e: ctrlEvent(e))
scrollbar = tk.Scrollbar(progress, orient=tk.VERTICAL, command=info.yview)
scrollbar.pack(side = tk.RIGHT, fill = tk.Y)
cancel_button = tk.Button(progress, text = "cancel")
cancel_button.pack(side = tk.TOP,anchor = tk.W)
info["yscrollcommand"] = scrollbar.set
info.pack(side = tk.TOP, anchor = tk.W, fill = tk.BOTH)
bar.start()
def ctrlEvent(event):
if(event.state & 2**2 == 4 and event.keysym=='c' ):
return
else:
return "break"
def process():
# .pyから実行しているかexeから実行しているか判定
if os.path.basename(sys.executable) == "python.exe":
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True)
elif os.path.basename(sys.executable) == "moviecreate.exe":
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW)
def cancel_animation():
cancel_button.configure(state = "disabled")
os.kill(p.pid, signal.CTRL_C_EVENT)
for line in p.stdout:
try:
info.insert(tk.END, line)
info.see("end")
except KeyboardInterrupt:
pass
progress.protocol("WM_DELETE_WINDOW", "")
cancel_button.configure(command = cancel_animation)
def click_close_to_cancel():
if messagebox.askokcancel("Close window", "File will not be saved. Are you sure?", parent=progress):
cancel_animation()
progress.protocol("WM_DELETE_WINDOW", "")
progress.update()
progress.destroy()
progress.protocol("WM_DELETE_WINDOW", click_close_to_cancel)
for line in p.stdout:
info.insert(tk.END, line)
info.see("end")
try:
outs, errs = p.communicate()
except subprocess.TimeoutExpired:
pass
else:
p.terminate()
cancel_button.configure(state = "disabled")
bar.stop()
progress.protocol("WM_DELETE_WINDOW", "")
th1 = threading.Thread(target=process)
th1.start()
root = tk.Tk()
button = tk.Button(root, text = "start", command = start)
button.pack()
root.mainloop()
ポイント
-
その1・その2・その3で使ったsubprocess
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True)
は,pythonスクリプトから実行する場合は問題なくキャンセルボタンが動作するが,exeから実行する場合はフリーズする. -
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW)
とした場合は逆にexeから実行する場合は問題ないが,pythonスクリプトを(コンソールから)実行するとフリーズする.(pythonスクリプトをダブルクリックで実行する場合もあると思うが,今回は考慮していない.) - そこで,.pyスクリプトから実行されている(環境変数のPATHからpython.exeが起動する)の場合と,exe化されたファイルが実行される場合で場合分けした.
if os.path.basename(sys.executable) == "python.exe":
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True)
elif os.path.basename(sys.executable) == "moviecreate.exe": # moviecreateの部分はexeの名前に合わせる
p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True, creationflags=subprocess.CREATE_NO_WINDOW)
- 前回までは
shell=True
をつけていたが,セキュリティを考えて無くした.
https://docs.python.org/ja/3/library/subprocess.html#security-considerations - キャンセルボタンではなくウインドウの閉じるボタンが押された場合は,本当に閉じてよいか一度確認してからキャンセル処理を行い,ウインドウを閉じる.
def click_close_to_cancel():
if messagebox.askokcancel("Close window", "File will not be saved. Are you sure?", parent=progress):
cancel_animation()
progress.protocol("WM_DELETE_WINDOW", "") #これはいらないかもしれない
progress.update() #これもいらないかもしれない
progress.destroy()
progress.protocol("WM_DELETE_WINDOW", click_close_to_cancel)
- しかしこのままでは,キャンセルボタンを押してキャンセル後にウインドウの閉じるボタンを押したときにも,「本当に閉じてよいか一度確認」されてしまう.これはしつこいので,
cancel_animation()
関数内にprogress.protocol("WM_DELETE_WINDOW", "")
を入れた.これによりキャンセルボタンが押された後は閉じるボタンにclick_close_to_cancel()
関数が紐付けられなくなるので,閉じるボタンを押すと特に何もなくウインドウが閉じるようになる.
最後までお読みいただきありがとうございました.