ffmpegの処理をtkinterでGUI化（その4: exe化しても正しく動くようにする）

[その1: プログレスバーとリアルタイム出力表示](https://qiita.com/ytakeda-paleo/items/c3d33aef9d1c715702de)
[その２: コンソール風出力表示](https://qiita.com/ytakeda-paleo/items/c93ef9e1776414a0b263)
[その３: キャンセルボタン](https://qiita.com/ytakeda-paleo/items/07013d25814d656b5cb7)
の続きです．

# やりたいこと
- [その３](https://qiita.com/ytakeda-paleo/items/07013d25814d656b5cb7)のスクリプトを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`という名前の実行ファイルを作る．（もちろん名前は何でも良い）

# コード
```python
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](https://qiita.com/ytakeda-paleo/items/c3d33aef9d1c715702de)・[その２](https://qiita.com/ytakeda-paleo/items/c93ef9e1776414a0b263)・[その３](https://qiita.com/ytakeda-paleo/items/07013d25814d656b5cb7)で使った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化されたファイルが実行される場合で場合分けした．
```python
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
- キャンセルボタンではなくウインドウの閉じるボタンが押された場合は，本当に閉じてよいか一度確認してからキャンセル処理を行い，ウインドウを閉じる．
```python        
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()`関数が紐付けられなくなるので，閉じるボタンを押すと特に何もなくウインドウが閉じるようになる．


最後までお読みいただきありがとうございました．