1
1

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-03-11

その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()関数が紐付けられなくなるので,閉じるボタンを押すと特に何もなくウインドウが閉じるようになる.

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1