[Tkinter] GUIの応答性をよくする

#1.はじめに
今日はGUIアプリケーションを作るとき、Threadingを利用して、その応答性をよくする方法について説明します。

#2.やりたいこと
下記の図のようなシンプルなGUI画面を設計します。上部のボタン(Button)がクリックされると、下部に0から９まで順番に表示されるものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/b8d51a21-8fca-c982-6ae5-e7f9c03b31b6.png)
早速、Tkinterでコードを書いてみましょう。

#3.応答性が悪いコード
まず、下記のようなコードを書いてみました。
ボタンを押すと、_main_func()を実行する構造となっています。_main_func()は、For文を利用して0から9まで表示する関数です。

結果の表示には、Tkinterのラベルと、print文の2種類を用いました。

##3.1.プログラミングコード(1)
```python:tkinter_wo_threading.py
import tkinter as tk
from tkinter import ttk
from tkinter import font
import time

class Application(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()
        self.master.geometry("300x300")
        self.master.title("Tkinter Freezes after clicking Buttons")


        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()

    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._main_func)
        self.btn_Start.grid( column=0, row=0,  pady=10 , sticky='NESW')

        # Label Title
        self.lbl_title = ttk.Label( self.main_frame)
        self.lbl_title.configure( text='Calculation Results Shown Here' )
        self.lbl_title.grid( column=0, row=1,  padx = 20, pady=20 ,sticky='EW')

        # Label Result
        self.lbl_result = ttk.Label( self.main_frame )
        self.lbl_result.configure( text='' )
        self.lbl_result.grid( column=0, row=2,  padx = 100, pady=10 ,sticky='EW')


    def _main_func(self):

        for i in range(10):
            print(i)
            self.lbl_result.configure(text = i, font = self.font_lbl_big)
            time.sleep(0.1)


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

if __name__ == "__main__":
    main()
```
##3.2.実行結果
このコードを実行してみます。
ボータンを押すと、for文のループが終わるまで、プログラムが応答しなくなります。print文で結果を連続に表示してくれますが、GUIは止まったままです。そして、for文のループが終わると最後の数字だけ表示してくれます。最初に期待したものと違いますね。

これはPythonがコードを1行ずつ逐次実行することが原因です。このままだと、GUIプログラムでの一つのイベント処理が終わるまで、ほかのイベントがスタートすらできなくなる応答性が悪くなります。
![with_thread.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/8938bdc0-a8bd-5899-bbf4-44f74e623d8a.gif)

#4.Threadingを利用し、応答性をよくしたコード
この時、Threadingを利用します。Threadingに関する内容は、参考資料を参照にしました。

##4.1.応答性を改善したプログラミングコード(1)
まず、threadingを導入します。

```python
import threading
```
順番として、ボタン（Button）のコールバック関数を_start_thread()に変更します。
その_start_thread()に、_main_func()をスレッドとしてstartする機能を入れておきます。ターゲットとして指定し、スタートを書けるイメージです。

```python

def _start_thread(self):
        self.thread_main = threading.Thread(target = self._main_func)
        self.thread_main.start()

```
全体のコードです。

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

class Application(tk.Frame):
    def __init__(self,master):
        super().__init__(master)
        self.pack()
        self.master.geometry("300x300")
        self.master.title("Tkinter Freezes after clicking Buttons")


        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()

    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_thread)
        self.btn_Start.grid( column=0, row=0,  pady=10 , sticky='NESW')

        # Label Title
        self.lbl_title = ttk.Label( self.main_frame)
        self.lbl_title.configure( text='Calculation Results Shown Here' )
        self.lbl_title.grid( column=0, row=1,  padx = 20, pady=20 ,sticky='EW')

        # Label Result
        self.lbl_result = ttk.Label( self.main_frame )
        self.lbl_result.configure( text='' )
        self.lbl_result.grid( column=0, row=2,  padx = 100, pady=10 ,sticky='EW')


    #-----------------------------------------------------------
    # Start Thread
    # -----------------------------------------------------------
    def _start_thread(self):
        self.thread_main = threading.Thread(target = self._main_func)
        self.thread_main.start()

    def _main_func(self):

        for i in range(10):
            print(i)
            self.lbl_result.configure(text = i, font = self.font_lbl_big)
            time.sleep(0.1)


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

if __name__ == "__main__":
    main()

```
##4.2.実行結果
Threadingを利用したコードの実行結果です。
![without_thread.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/208980/6444dc8f-0f8a-c76b-ab40-3b1916119ab9.gif)

最初に意図した通りにプログラムが動くことが確認できました。プログラムの反応性もよくなり、ほかのイベント処理も可能となりました。

#まとめ

スレッドを利用し、GUIプログラムの応答性をよくすることができました。

#参考資料
1.[python#Threading](https://qiita.com/soup01/items/42810b541fc3adc09542)
2.[Pythonのthreadingとmultiprocessingを完全理解](https://qiita.com/kaitolucifer/items/e4ace07bd8e112388c75)
3.[[Python] スレッドで実装する](https://qiita.com/tchnkmr/items/b05f321fa315bbce4f77)