[Tkinter] Eventオブジェクトでスレッドを制御する

#はじめに
[前回の記事](https://qiita.com/kotai2003/items/dc0b23fa95844fc1ba45)で、Tkinter GUIでスレッドを利用して、応答性をよくする方法を紹介しました。

今回は、このスレッドの動作を制御する方法をご紹介します。

#やりたいこと

下記の図を見てみてください。
スタートボタンを押すと、数字がカウントされるGUIプログラムを作りたいと思います。途中で下部のストップボタンを押すと、数字のカウントを一旦止めてもらいます。
また、上部のスタートボタンを押すことで、数字のカウントが再開されるイメージです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/5228924d-d564-7c98-9285-cd14e48e9cd7.png)


#スレッドにはStartがあってもStopがない。
最初は、数字をカウントする関数をWhile文を用いて作り、スタートボタンを押すとスレッドを立ち上げ、**ストップボタンを押すとスレッドを中止する**ことを考えました。

最初に考えたコードがこちらです。スレッドをよく使うユーザがこのコードを見ると怒るかもしれません。

```python
def _start_func(self):
    self.thread_main=threading.Thread(target=self._main_func)
    self.thread_main.start()

def _stop_func(self):
    self.thread_main=threading.Thread(target=self._main_func)
    self.thread_main.stop()
```
1番目の_start_func()は間違ってないです。しかし、2番目の_stop_func()は大間違いがあります。そうです。スレッドには.start()メソッドがあっても、**.stop()メソッドはありません。** **スレッドは途中で強制的に立ち下げるように設計されていません。** 

それではどうすればいいでしょうか？

その時は、スレッドを立ち上げて、それを**臨時停止**したり、**再開**したりすることになります。ここで登場するヒーローが**Event**オブジェクトです。

#Eventオブジェクトの利用
Eventオブジェクトは、スレッドの動作を内部フラグの値で管理します。
内部フラグの値がTrueなら、スレッドを動かす。Falseなら、スレッドを停止する。それだけです。
その内部フラグの制御に関する重要なメソッドを下記の表に整理します。

| Event関連メソッド |　説明 | 備考   |
|:----------------:|:-----------------:|:------------------:|
| wait()      | スレッドを待機させる(=臨時停止する。）| 内部フラグがTrueになるまで、スレッドを待機させる。    |
| set()       | スレッドを動かす  | 内部フラグの値をTrueにする。    |
| clear()     | スレッドを止める。  |内部フラグの値をFalseにする。                |
| is_set()    |  内部フラグの値を返す |スレッドの運転状態を判断するときに利用。     |

メソッドの名前がbegin()ではなく、set()などになって、最初は理解が難しいかもしれませんが、内部フラグをTrue、Falseにsetし、その動作を制御すると考えれば、理解しやすいと思います。

#プログラムの構造

Eventオブジェクトを利用し、やりたいことのプログラムの構造を書くと次のようになります。

まず、プログラムが立ち上がる際に、スレッドも同時にstartメソッドで立ち上げておきます。
次にイベントのインスタンスとして、startedを用意しておきます。このstartedの内部フラグでスレッドの動作を制御することになります。

スレッドのtargetになる関数_main_func()の冒頭に**started.wait()**を入れておきます。_main_func（）は、while文でできており、while文はaliveで制御します。（aliveは最後にプログラムを立ち下げるときに使いますが、ここではコードだけに掲載し、説明は割愛します。）

GUIのスタートボタンに、内部フラグをTrueにする.set()メソッドをバインドし、GUIのストップボタンに、内部フラグをFalseにするclear()メソッドをバインドします。

_main_func()のWhile文の内部の構造は、まず.is_set()メソッドにより、内部フラグの状態を確認します。内部フラグの値がTrueの場合、スレッドで実行したい処理を行います。内部フラグがFalseと分かった場合、.wait()メソッドでスレッドを待機させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/e7ee87a3-66da-6435-1ae4-0f99c1b86992.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/2eb7e520-16fb-0cb4-eae1-4ae8429dea4e.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/87672426-f7f2-d26c-f7ae-3e9c416d7141.png)


#プログラムのコード
全体のプログラムコードを掲載します。

```python:GUI_Contol_Thread_with_Event_Object.py
import tkinter as tk
from tkinter import ttk
from tkinter import font
import threading


class Application(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()

        self.master.geometry("300x300")
        self.master.title("Tkinter GUI with Event")

        self.font_lbl_big = font.Font( family="Meiryo UI", size=30, weight="bold" )
        self.font_lbl_middle = font.Font( family="Meiryo UI", size=15, weight="bold" )
        self.font_lbl_small = font.Font( family="Meiryo UI", size=12, weight="normal" )

        self.create_widgets()
    #--------------------------------------------
    # Setup Threading Start
    #--------------------------------------------
        self.started = threading.Event() # Event Object
        self.alive = True # Loopの条件
        self._start_thread_main()


    def create_widgets(self):

        # Frame
        self.main_frame = tk.LabelFrame( self.master, text='', font=self.font_lbl_small )
        self.main_frame.place( x=25, y=25 )
        self.main_frame.configure( height=250, width=250 )
        self.main_frame.grid_propagate( 0 )
        self.main_frame.grid_columnconfigure( 0, weight=1 )

        #Start Button
        self.btn_Start = ttk.Button(self.main_frame)
        self.btn_Start.configure(text ='Start')
        self.btn_Start.configure(command = self._start_func)
        self.btn_Start.grid(column = 0, row = 0, padx=10, pady = 10,sticky='NESW' )

        # Stop Button
        self.btn_Stop = ttk.Button(self.main_frame)
        self.btn_Stop.configure(text = 'Stop')
        self.btn_Stop.configure(command = self._stop_func)
        self.btn_Stop.grid(column = 0, row = 1, padx=10, pady = 10,sticky='NESW')

        # Label
        self.lbl_result = ttk.Label(self.main_frame)
        self.lbl_result.configure(text = 'Threading Result Shown Here')
        self.lbl_result.grid(column = 0, row = 2, padx= 30, pady=10,sticky='NESW')

        # Kill Button
        self.btn_Kill = ttk.Button(self.main_frame)
        self.btn_Kill.configure(text = 'Kill Thread')
        self.btn_Kill.configure(command = self._kill_thread)
        self.btn_Kill.grid(column=0, row=3, padx = 10, pady=20,sticky='NESW')

    #--------------------------------------------------
    # Callback Function
    #--------------------------------------------------
    def _start_func(self):
        self.started.set()
        print("Threading Begin")
        print( 'Thread status', self.thread_main )


    def _stop_func(self):
        self.started.clear()
        print("\n Threading Stopped")
        print( 'Thread status', self.thread_main )


    def _start_thread_main(self):
        self.thread_main = threading.Thread(target=self._main_func)
        self.thread_main.start()
        print('main function Threading Started')
        print('Thread status', self.thread_main)

    def _kill_thread(self):
        if self.started.is_set() == False:
            self.started.set()
            self.alive = False
            self.thread_main.join()
        else:
            self._stop_func()
            self.started.set()
            self.alive = False
            #self.thread_main.join()
        print("Thread was killed.")
        print( 'Thread status', self.thread_main )

      

    def _main_func(self):
        i = 0
        self.started.wait()
        while self.alive:
            if self.started.is_set() == True:
                i = i + 1
                print( "{}\r".format( i ), end="" )
                self.lbl_result.configure( text=i ,font = self.font_lbl_big )

            else:
                self.lbl_result.configure( text= 'Stopped' ,font = self.font_lbl_big)
                self.started.wait()

        pass

def main():
    root = tk.Tk()
    app = Application(master=root)#Inherit
    app.mainloop()

if __name__ == "__main__":
    main()
```

#実行結果
実行結果です。設計通りGUIプログラムの制御されることを確認が可能となりました。

![bandicam-2020-07-31-19-01-28-328.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/d599a67f-32c5-7f31-991d-2102ad86070c.gif)

#まとめ
1. Tkinter GUIの応答性をよくするために、スレッドを利用します。
2. スレッドの動作を制御するとき、Eventオブジェクトを使います。
3. Eventの内部フラグを制御する四つのメソッドを利用し、スレッドの制御が可能となります。

#参考文献
1.[Python公式文書　Event オブジェクト](https://docs.python.org/ja/3/library/threading.html#event-objects)
2.[[Tkinter] GUIの応答性をよくする](https://qiita.com/kotai2003/items/dc0b23fa95844fc1ba45)
3.[おまいらのthreading.Eventの使い方は間違っている](https://qiita.com/tag1216/items/2dcb112f8018eb19a999)
4.[Python スレッドの停止と再開の簡易サンプル](https://qiita.com/BlueSilverCat/items/44a0a2a3c45fc3e88b19)