wxpythonで時間待機処理を実現する方法

# はじめに
wxpythonで初めて時間待機処理を書こうとしたとき、

1. time.sleep()で時間待機してみる
2. 画面が応答しなくなり原因を調べる
3. wx.TimerやThreadで対応する

という道を辿ると思います。(ですよね？)
この時間待機処理を結構頻繁に使うのでメモしました。

正直この内容はそこらじゅうに転がっているので、参考記事の１つくらいに見て頂ければ幸いです。

# time.sleep()でエラーになる原因
大雑把にいうとGUIの裏で動いているMainloopがsleepで止まってしまう事が原因です。

# 環境
Mac OS
wxpython 4.1.0

# インポート
wxpythonをインストールします

```terminal:
pip install wxpython
```
threadingはインストールなしで使用可能です。wx.Timerを使用する時は使いませんが、代わりにtimeを使います。

```python:
import wx
import threading
import time
```

# 方法1 wx.Timer
一定間隔毎に処理を行う際に有効です。一番わかりやすいのは現在時刻の更新に使うことでしょうか。
<img width="300" alt="WxTimer1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/132585a1-cfa0-511a-d226-9f87bd2b8c04.gif">


```python:
import wx
import threading  # 今回は使用しない
import time  # 今回は使用しない
# 時刻表示に使用
import datetime as dt


class MainFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(400, 200))

        # TimerPanelインスタンス作成
        self.panel = TimerPanel(self, -1)

        # 画面を中央に配置して表示
        self.Center()
        self.Show()


class TimerPanel(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)

        # 現在時刻の文字列作成, self.clock_txt作成, 時刻を表示
        now = dt.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
        self.clock_txt = wx.StaticText(self, -1, label=now)

        # フォント設定, サイズを20に変更後self.clock_txtに反映
        font = wx.Font(20, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL)
        self.clock_txt.SetFont(font)

        # サイザー作成, self.clock_txtをサイザーに追加, パネルにsizerを適応
        sizer = wx.GridSizer(1, 1, gap=(0, 0))
        sizer.Add(self.clock_txt, flag=wx.ALIGN_CENTRE)  # ALIGN_CENTRE：サイザーの中央に設置
        self.SetSizer(sizer)

        self.timer = wx.Timer(self)  # パネル内にタイマー作成
        self.Bind(wx.EVT_TIMER, self.clock)  # 指定した間隔毎にself.clockを実行
        self.timer.Start(1000)  # タイマーを1000ms(=1s)に設定してスタート
        # self.timer.Stop()  # タイマーを止めたい場合はこれ


    def clock(self, event):
        # 現在時刻を取得してself.clock_txtにセット
        now = dt.datetime.now().strftime('%Y/%m/%d %H:%M:%S')
        self.clock_txt.SetLabel(now)
        self.Refresh()


if __name__ == '__main__':
    app = wx.App()
    MainFrame(None, -1, 'TimeEvent')
    app.MainLoop()

```

フレームやパネルを親にしてタイマーを作成し、間隔を設定してスタートします。
タイマーは複数同時に動かすことが可能です。

<img width="300" alt="WxTimer2.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/5757cdf4-2555-6d3a-0ad2-66332ce53814.gif">


上のgifは2つのパネルがあり、それぞれのタイマーで時刻を表示しています。

上パネルが１秒、下が2秒間隔です。

# 方法2 並列処理 threading.Thread
wx.Timerでは実装しづらい処理にthreading.Threadを使います。
この関数は別スレッドを作成し複数の処理を同時に実行できるものです。
GUIが動いているスレッドではtime.sleep()を使ってはいけませんが、別スレッドなら問題ありません。

以下がwx.Timerでは難しいと感じる処理です
- 時間待機間隔が同じではない定期実行(ex:信号機)
- 時間経過ごとに状態が変化する処理->定期実行の際に毎回条件分岐するのが面倒臭い(ex:信号機)

つまり信号機を作るときにthreadind.Thredを使います(限定的すぎない？)

<img width="500" alt="WxThread1.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/1044c1bc-eb6f-926f-1415-0de431466a20.gif">

青、黄、赤それぞれ4, 1, 5秒ずつ表示しています

```python:
import wx
import time
import threading


class TrafficLight(threading.Thread):
    def __init__(self, panel):
        super().__init__()
        self.panel = panel
        # サブスレッドをデーモン化、これをTrueにしない場合画面を閉じてもサブスレッドが動き続ける
        self.setDaemon(True)

    def run(self):
        while True:
            # 黄パネル->黒, 赤パネル->赤
            self.panel.yellow_panel.SetBackgroundColour('#000000')
            self.panel.red_panel.SetBackgroundColour('#ff0000')
            self.panel.Refresh()
            time.sleep(5)

            # 赤パネル->黒, 青パネル->青
            self.panel.red_panel.SetBackgroundColour('#000000')
            self.panel.blue_panel.SetBackgroundColour('#00ff00')
            self.panel.Refresh()
            time.sleep(4)

            # 青色パネル->黒, 黄パネル->黄
            self.panel.blue_panel.SetBackgroundColour('#000000')
            self.panel.yellow_panel.SetBackgroundColour('#ffff00')
            self.panel.Refresh()
            time.sleep(1)


class MainFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(600, 200))

        self.main_panel = MainPanel(self, -1)
        
        # 画面を中央に表示
        self.Center()
        self.Show()


class MainPanel(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)
        
        # 青パネル、色を黒に指定
        self.blue_panel = wx.Panel(self, -1)
        self.blue_panel.SetBackgroundColour('#000000')
        
        # 黄パネル、色を黒に指定
        self.yellow_panel = wx.Panel(self, -1)
        self.yellow_panel.SetBackgroundColour('#000000')

        # 赤パネル、色を黒に指定
        self.red_panel = wx.Panel(self, -1)
        self.red_panel.SetBackgroundColour('#000000')
        
        # スタート用ボタン
        self.button = wx.Button(self, -1, 'start')
        
        # レイアウト関係
        sizer1 = wx.FlexGridSizer(2, 1, gap=(0, 0))
        sizer2 = wx.GridSizer(1, 3, gap=(0, 0))

        sizer1.Add(sizer2, flag=wx.EXPAND)
        sizer1.Add(self.button, flag=wx.ALIGN_RIGHT)
        sizer1.AddGrowableCol(0)
        sizer1.AddGrowableRow(0)

        sizer2.Add(self.blue_panel, flag=wx.EXPAND)
        sizer2.Add(self.yellow_panel, flag=wx.EXPAND)
        sizer2.Add(self.red_panel, flag=wx.EXPAND)
        self.SetSizer(sizer1)
        
        # サブスレッドインスタンス作成、selfを引数へ
        self.traffic_light = TrafficLight(self)
        
        # ボタンイベント
        self.button.Bind(wx.EVT_BUTTON, self.start)

    def start(self, event):
        # サブスレッドインスタンス作成、selfを引数へ
        traffic_light = TrafficLight(self)
        # サブスレッド動作開始
        traffic_light.start()
        self.start_button.Disable()  # ボタン無効化, スレッドの複数作成を防止

if __name__ == "__main__":
    app = wx.App()
    MainFrame(None, -1, "信号機")
    app.MainLoop()
```

threading.Threadのrunメソッドをオーバーライドしています。
runメソッドにはself以外渡せません。必要な属性はインスタンス作成時に渡します。また、１つのスレッドにつき１回の実行しかできないのでボタンが押されたときにインスタンス化しています。

## threadを任意のタイミングで終了する
上の信号機はwhile Trueで動いている為止めることができません。しかし止めれるようにしたいです。そこでTrafficLightクラスの書き換えとグローバル変数・関数を追加します。

GUIはストップボタンを追加しています。ストップボタンが押されたらthread_stop_flagがTrueになり、スレッドの動作が終了します。

<img width="500" alt="WxThread" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/779817/c8b35dbf-06b9-fe9a-32a4-189bf8d3a11f.gif">


```python:
import wx
import time
import threading

thread_stop_flag = False


def wait_time(seconds):
    """
    渡された待機時間の間stop_flagを見ながら待機
    stop_flagがTrueの時while文を抜ける
    :param seconds: int
        待機時間
    :return: None
    """
    wait_start = time.time()
    while time.time() - wait_start <= seconds:
        if not thread_stop_flag:
            time.sleep(1)
        else:
            break


class TrafficLight(threading.Thread):
    def __init__(self, panel):
        super().__init__()
        self.panel = panel
        # サブスレッドをデーモン化、これをTrueにしない場合画面を閉じてもサブスレッドが動き続ける
        self.setDaemon(True)

    def run(self):
        while not thread_stop_flag:  # 変更点2
            # 黄パネル->黒, 赤パネル->赤
            self.panel.yellow_panel.SetBackgroundColour('#000000')
            self.panel.red_panel.SetBackgroundColour('#ff0000')
            self.panel.Refresh()
            wait_time(5)

            # 赤パネル->黒, 青パネル->青
            self.panel.red_panel.SetBackgroundColour('#000000')
            self.panel.blue_panel.SetBackgroundColour('#00ff00')
            self.panel.Refresh()
            wait_time(4)

            # 青色パネル->黒, 黄パネル->黄
            self.panel.blue_panel.SetBackgroundColour('#000000')
            self.panel.yellow_panel.SetBackgroundColour('#ffff00')
            self.panel.Refresh()
            wait_time(1)

        # 変更点3, 全て黒に戻す
        self.panel.blue_panel.SetBackgroundColour('#000000')
        self.panel.yellow_panel.SetBackgroundColour('#000000')
        self.panel.red_panel.SetBackgroundColour('#000000')
        self.panel.Refresh()


class MainFrame(wx.Frame):
    def __init__(self, parent, id, title):
        wx.Frame.__init__(self, parent, id, title, size=(600, 200))

        self.main_panel = MainPanel(self, -1)

        # 画面を中央に表示
        self.Center()
        self.Show()


class MainPanel(wx.Panel):
    def __init__(self, parent, id):
        wx.Panel.__init__(self, parent, id)

        # 青パネル、色を黒に指定
        self.blue_panel = wx.Panel(self, -1)
        self.blue_panel.SetBackgroundColour('#000000')

        # 黄パネル、色を黒に指定
        self.yellow_panel = wx.Panel(self, -1)
        self.yellow_panel.SetBackgroundColour('#000000')

        # 赤パネル、色を黒に指定
        self.red_panel = wx.Panel(self, -1)
        self.red_panel.SetBackgroundColour('#000000')

        # スタート, ストップ用ボタン
        self.start_button = wx.Button(self, -1, 'start')
        self.stop_button = wx.Button(self, -1, 'stop')

        # レイアウト関係
        sizer1 = wx.FlexGridSizer(2, 1, gap=(0, 0))
        sizer2 = wx.GridSizer(1, 3, gap=(0, 0))
        sizer3 = wx.GridSizer(1, 2, gap=(0, 0))

        sizer1.Add(sizer2, flag=wx.EXPAND)
        sizer1.Add(sizer3, flag=wx.ALIGN_RIGHT)
        sizer1.AddGrowableCol(0)
        sizer1.AddGrowableRow(0)

        sizer2.Add(self.blue_panel, flag=wx.EXPAND)
        sizer2.Add(self.yellow_panel, flag=wx.EXPAND)
        sizer2.Add(self.red_panel, flag=wx.EXPAND)

        sizer3.Add(self.start_button)
        sizer3.Add(self.stop_button)
        self.SetSizer(sizer1)

        # ボタンイベント
        self.start_button.Bind(wx.EVT_BUTTON, self.start)
        self.stop_button.Bind(wx.EVT_BUTTON, self.stop)

    def start(self, event):
        global thread_stop_flag
        thread_stop_flag = False
        # サブスレッドインスタンス作成、selfを引数へ
        traffic_light = TrafficLight(self)
        # サブスレッド動作開始
        traffic_light.start()
        self.start_button.Disable()  # ボタン無効化スレッドの複数作成を防止

    def stop(self, event):
        # グローバル変数のthread_stop_flagをTrueへ=threadのwhile文条件をFalseにする
        global thread_stop_flag
        thread_stop_flag = True
        self.start_button.Enable()


if __name__ == "__main__":
    app = wx.App()
    MainFrame(None, -1, "信号機")
    app.MainLoop()

```
大したことしてないのに長い気がします。他に良い書き方があれば教えて下さい、、、

# 終わりに
読んで頂きありがとうございます。当然ですがthreadingを使った方法は信号機以外に使い道があります。例えば定期実行ではなく１回だけ一連の動作を実行したい時などです。私はデバイスの初期設定なんかに使っています。

# 参考記事
以下の記事を参考にしています。

エラーの原因
[Python - 初心者です。sleep文を入れたら、アプリが正常に作動しなくなりました。｜teratail](https://teratail.com/questions/96422)

スレッドの中断
[おまいらのthreading.Eventの使い方は間違っている - Qiita](https://qiita.com/tag1216/items/2dcb112f8018eb19a999)