ffmpegの処理をtkinterでGUI化（その3: キャンセルボタン）

[その1: プログレスバーとリアルタイム出力表示](https://qiita.com/ytakeda-paleo/items/c3d33aef9d1c715702de)
[その２: コンソール風出力表示](https://qiita.com/ytakeda-paleo/items/c93ef9e1776414a0b263)
の続きです．
この記事の続きは[こちら](https://qiita.com/ytakeda-paleo/items/ddebf778a4eabafdb30e)

# やりたいこととGUIの内容
- 処理を中止するキャンセルボタンをつける．
- 完成したもの↓
![Animation2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/736226/76fe7548-a415-b1ef-00b0-697e3bcd15b7.gif)


## 環境
Windows 10 Pro, Python 3.7, miniconda(conda 4.11.0)

# コード
```python
import os, subprocess
import tkinter as tk
from tkinter import ttk, messagebox
import threading
import signal

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():
        p = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,shell=True,universal_newlines=True)
        
        def cancel_animation():
            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
        cancel_button.configure(command = cancel_animation)

        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()
            bar.stop()
            cancel_button.configure(state = "disabled")

    th1 = threading.Thread(target=process)
    th1.start()


root = tk.Tk()
button = tk.Button(root, text = "start", command = start)
button.pack()

root.mainloop()
```

# ポイント
## キャンセル処理
- `cancel_animation()`関数内にキャンセルボタン押下時の動作を書いている．
```python
def cancel_animation():
    os.kill(p.pid, signal.CTRL_C_EVENT)
    bar.stop()
    for line in p.stdout:
        try:
            info.insert(tk.END, line)
            info.see("end")
        except KeyboardInterrupt:
            pass
```
- ffmpegでの処理は`subprocess`で行われているが，`kill()`や`terminate()`では処理を中止できなかったので，`os.kill`を使った．
```python
os.kill(p.pid, signal.CTRL_C_EVENT)
```
参考: [Stackoverflow - How to kill subprocess python in windows](https://stackoverflow.com/questions/28551180/how-to-kill-subprocess-python-in-windows)
- ちなみに，execコマンドを先につける方法([Qiita - Pythonのsubprocessでkillが効かないときの対処法](https://qiita.com/lobmto/items/49f2a3fbfd913fcd35d5))は有効ではなかった．
## キャンセル処理後のエラー対応
- `os.kill`でプロセスを終了するだけだと，`keyboardInterrupt`ののちに，`cancel_animation()`内じゃない方の`info.insert(tk.END, line)`の行で`RuntimeError: main thread is not in main loop`のエラーが出力される．以下がその例（スクリプトの行番号などは上記掲載のものと異なります）
```
KeyboardInterrupt
Exception in thread Thread-1:
Traceback (most recent call last):
    self.run()
  File "C:\hogehoge\lib\threading.py", line 870, in run
    self._target(*self._args, **self._kwargs)
  File "C:\hogehoge\animation.py", line 182, in process
    info.insert(tk.END, line)
  File "C:\hogehoge\__init__.py", line 3272, in insert
    self.tk.call((self._w, 'insert', index, chars) + args)
RuntimeError: main thread is not in main loop
```
- `RuntimeError: main thread is not in main loop`が出ている状態でプログレスバーウインドウを閉じて，もう一度メインウインドウから実行ボタンを押した場合は，標準出力がテキストウィジェットに全く表示されない．
- エラーの根本的な原因は，tkinterを別スレッドで起動していることにあるらしい．
参考: [Qiita - tkinterを別スレッドで起動したときは、そのスレッド自身で停止処理して変数を後始末してからスレッド終了すること](https://qiita.com/shiracamus/items/cd1d5f2d8fabd4e8a366)
[Teratail - Tkinterのエラーの対処方法がわかりません](https://teratail.com/questions/220006)
- とりあえず，`cancel_animation()`内で処理中止後も標準出力を表示し続けるようにしたら`RuntimeError: main thread is not in main loop`は出なくなった．ついでに，`KeyboardInterrupt`を除外するようにした．
- プログレスバーウインドウを閉じたあとにもう一度実行し直しても問題なく動作する．
- ちなみに，`p.stdout`を利用している通り，処理中止後にffmpegから出力される`Exiting normally, received signal 2.`といった文言は標準エラー出力ではなく標準出力．
## エトセトラ
- ffmpegでの処理が完了した後はキャンセルボタンを押せなくする．
```python
(省略)
else:
    p.terminate()
    bar.stop()
    cancel_button.configure(state = "disabled")
```

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