その1: プログレスバーとリアルタイム出力表示
その2: コンソール風出力表示
の続きです.
この記事の続きはこちら
やりたいこととGUIの内容
環境
Windows 10 Pro, Python 3.7, miniconda(conda 4.11.0)
コード
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()
関数内にキャンセルボタン押下時の動作を書いている.
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
を使った.
os.kill(p.pid, signal.CTRL_C_EVENT)
参考: Stackoverflow - How to kill subprocess python in windows
- ちなみに,execコマンドを先につける方法(Qiita - Pythonのsubprocessでkillが効かないときの対処法)は有効ではなかった.
キャンセル処理後のエラー対応
-
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を別スレッドで起動したときは、そのスレッド自身で停止処理して変数を後始末してからスレッド終了すること
Teratail - Tkinterのエラーの対処方法がわかりません - とりあえず,
cancel_animation()
内で処理中止後も標準出力を表示し続けるようにしたらRuntimeError: main thread is not in main loop
は出なくなった.ついでに,KeyboardInterrupt
を除外するようにした. - プログレスバーウインドウを閉じたあとにもう一度実行し直しても問題なく動作する.
- ちなみに,
p.stdout
を利用している通り,処理中止後にffmpegから出力されるExiting normally, received signal 2.
といった文言は標準エラー出力ではなく標準出力.
エトセトラ
- ffmpegでの処理が完了した後はキャンセルボタンを押せなくする.
(省略)
else:
p.terminate()
bar.stop()
cancel_button.configure(state = "disabled")
最後までお読みいただきありがとうございました.