#はじめに
前回の記事で、Tkinter GUIでスレッドを利用して、応答性をよくする方法を紹介しました。
今回は、このスレッドの動作を制御する方法をご紹介します。
#やりたいこと
下記の図を見てみてください。
スタートボタンを押すと、数字がカウントされるGUIプログラムを作りたいと思います。途中で下部のストップボタンを押すと、数字のカウントを一旦止めてもらいます。
また、上部のスタートボタンを押すことで、数字のカウントが再開されるイメージです。
#スレッドにはStartがあってもStopがない。
最初は、数字をカウントする関数をWhile文を用いて作り、スタートボタンを押すとスレッドを立ち上げ、ストップボタンを押すとスレッドを中止することを考えました。
最初に考えたコードがこちらです。スレッドをよく使うユーザがこのコードを見ると怒るかもしれません。
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()メソッドでスレッドを待機させます。
#プログラムのコード
全体のプログラムコードを掲載します。
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プログラムの制御されることを確認が可能となりました。
#まとめ
- Tkinter GUIの応答性をよくするために、スレッドを利用します。
- スレッドの動作を制御するとき、Eventオブジェクトを使います。
- Eventの内部フラグを制御する四つのメソッドを利用し、スレッドの制御が可能となります。
#参考文献
1.Python公式文書 Event オブジェクト
2.[Tkinter] GUIの応答性をよくする
3.おまいらのthreading.Eventの使い方は間違っている
4.Python スレッドの停止と再開の簡易サンプル