tkinterを用いた時の定期実行処理【Python3】

# 概要

tkinterでの定期実行処理方法について、以下の2つを示します。

- threadingモジュールを使用した場合
- scheduleモジュールを使用した場合

簡単な処理であれば、threadingモジュールやscheduleモジュールを使わなくても良いですが、重たい処理になるとtk自体の動作（ボタンを押す／画面を移動させるなど）が重くなります。そのため、面倒でも以上のような方法で実装すると良いかもしれません。

# threadingモジュールを使用した場合

以下はサンプルコードです。「self.label」の値を１ずつ増やすプログラムです。やろうと思えばミリ秒単位の定期実行が可能です。pythonなので、時間精度はそこまで良くありませんが、そこまで時間精度を気にする必要がなければこれで十分です。

```python
import tkinter as tk
import threading

class GUI(tk.Frame):
    def __init__(self,master = None):
        super().__init__(master)
        self.master = master
        master.geometry("300x300")
        master.title("Test")
        
        self.count = 0 # labelに定義する値のバッファ
        
        self.test = tk.Frame(self.master)# 必要ないっちゃないですが、画面を管理するために一旦別フレームを生成しています。
        self.test.pack()
        self.label = tk.Label(self.test)
        self.label.pack()
        self.label["text"] = str(self.count) # labelの値を初期化
        
        self.timeEvent()# タイマー起動
        
    # タイマー起動用関数
    def timeEvent(self):
        th = threading.Thread(target=self.update)# スレッドインスタンス生成
        th.start()# スレッドスタート
        self.after(1000, self.timeEvent)# ここで、再帰的に関数を呼び出す
        
    # スレッド処理実体
    def update(self):
        self.count += 1
        print(self.count) # デバッグメッセージ
        self.label["text"] = str(self.count) # labelの値を更新      

if __name__ == "__main__":
    gui = tk.Tk()
    app = GUI(master = gui)
    app.mainloop()

```

もっと時間精度が欲しい場合は、少し処理が複雑になりますが、以下のような書き方が出来ます。2倍は時間精度が良くなると思いますが、可読性が少し落ちます。

```python
import tkinter as tk
import threading
import time

class GUI(tk.Frame):
    def __init__(self,master = None):
        super().__init__(master)
        self.master = master
        master.geometry("300x300")
        master.title("Test")
        
        self.count = 0
        
        self.test = tk.Frame(self.master)
        self.test.pack()
        self.label = tk.Label(self.test)
        self.label.pack()
        self.label["text"] = str(self.count)
        self.buftime = time.time()# タイマー初期化
        self.timeEvent()
    
    
    def timeEvent(self):
        tmp = time.time()
        if(tmp - self.buftime) >= 1: # 1sec経過した場合
            th = threading.Thread(target=self.update)
            th.start()
            print(tmp - self.buftime)
            self.buftime = tmp
        self.after(1, self.timeEvent)
          
    def update(self):
        self.count += 1
        self.label["text"] = str(self.count)     





if __name__ == "__main__":
    gui = tk.Tk()
    app = GUI(master = gui)
    app.mainloop()
```

# scheduleモジュールを使用した場合

「schedule」モジュールを使用しても同様のことは出来ます。ただ、こちらはpipでインストールの必要があります。また、ミリ秒単位での定期実行は調べた範囲では出来ない様子です。

秒単位であれば、こちらの方が可読性が高くなると思うで、目的に合わせて選択すると良いかもしれません。

```sh
pip install schedule
```

```python
import tkinter as tk
import schedule

class GUI(tk.Frame):
    def __init__(self,master = None):
        super().__init__(master)
        self.master = master
        master.geometry("300x300")
        master.title("Test")
        
        self.count = 0
        
        self.test = tk.Frame(self.master)
        self.test.pack()
        self.label = tk.Label(self.test)
        self.label.pack()
        self.label["text"] = str(self.count)
        
        schedule.every(1).seconds.do(self.update)# scheduleにタスクを追加
        self.sc()
        
    def sc(self): # scheduleモジュールにて定期的にタスク状態を確認
        schedule.run_pending()
        self.after(1, self.sc) # 再帰的に実行
    
    def update(self):
        self.count += 1
        print(self.count)
        self.label["text"] = str(self.count)        

if __name__ == "__main__":
    gui = tk.Tk()
    app = GUI(master = gui)
    app.mainloop()
```

# （補足）Pythonにおけるスレッド処理について

Pythonの「threading」モジュールは、インスタンス一回宣言ごとに一回のみ実行されます。これはthreadingモジュールの仕様です。以下に例を示します。

```python:error.py
import threading

th = threading.Thread(target=self.update)
th.start()
th.start() # ←ここでErrorになる「RuntimeError: threads can only be started once」
```