PySimpleGUIでマルチスレッドで内容を更新する

# 目次

 - [PySimpleGUIで内容を更新する](https://qiita.com/mo256man/items/2b4ac4a6ca344b812c33)
 - PySimpleGUIでマルチスレッドで内容を更新する　　←今ここ

# マルチスレッドの公式サンプルを読み解く

## 公式サンプル
時間がかかる処理をマルチスレッドでおこなうサンプルが[PySimpleGUIのGitHub](https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Multithreaded_Long_Tasks.py
)にある。

https://github.com/PySimpleGUI/PySimpleGUI/blob/master/DemoPrograms/Demo_Multithreaded_Long_Tasks.py

[Do Long Task]ボタンをクリックするとメッセージが表示され、5秒間停止し、その後追加でメッセージが表示される。
また、[Click Me]ボタンをクリックするとロングタスクが実行中でもお構いなしで即座に別メッセージを表示する。というものだ。
![gui_demo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/8693c731-5a94-4e1a-d7ea-779b32954401.png)

正直なところ分かりづらいサンプルなので、ドキュメントを少しずつ追いかけていく。

## start_thread()

別スレッドで関数を呼び出す。そっちで重い処理をしていてもメインスレッドの無限ループはぐるぐる回り続ける。
だからといって別スレッドでGUIを更新してはいけない。メインスレッドでの更新との間で競合が発生するおそれがある。親殺しのパラドックスで宇宙を消滅させるがごとき振る舞いは避けるべきだ。
公式のドキュメントは[こちら](https://www.pysimplegui.org/en/latest/call%20reference/#start_thread)。

**window.start_thread(func, end_key)** という書き方をする。
　func　　　ラムダ式もしくはパラメータなしの関数名
　end_key　　スレッドが完了して戻ってきたときに生成されるキー

### func

公式の説明は「A lambda or a function name with no parms」。
ラムダ式もしくはパラメータなしの関数名を指定する。つまりこういうこと。

```Python:
# 公式のサンプルコード
window.start_thread(lambda: long_operation_thread(seconds, window), end_keyは略)

# 正しく動く（パラメータありの関数のラムダ式）　上記サンプルコードのかたち
window.start_thread(lambda: func(param), end_key=end_key)
# 正しく動く（パラメータなしの関数のラムダ式）
window.start_thread(lambda: func(), end_key=end_key)
# 正しく動く（パラメータなしの関数名）
window.start_thread(func, end_key=end_key)

# パラメータありの関数を直接指定するのは駄目
window.start_thread(func(param), end_key=end_key)
# パラメータなしであってもラムダ式に関数名を指定するのは駄目
window.start_thread(lambda: func, end_key=end_key)
```

### end_key
end_keyは公式に **start_thread(func, end_key = None)** と書かれており`None`がデフォルトで設定されているように見えるがそれは間違い。指定必須で、無いとエラーになる。
しかも、公式に従って`None`を指定すると困ったことが起こってしまう。
`sg.WIN_CLOSED`と`None`は同値。だから end_key=None とするとウィンドウの右上バッテンを押したときと同じ挙動を起こしてしまう。
もちろん作る人次第だが、バッテンを押すとプログラムを終了させることが多いだろう。スレッドが終わったらウィンドウが閉じてGUIプログラムが終了してしまう、そんなGUIアプリがあってよいだろうか。いや、あってはならない。
ということで気をつけましょう。

また、サンプルコードでは
```Python:サンプルコード
window.start_thread(funcは略, ('-THREAD-', '-THEAD ENDED-'))
```
とend_keyの値が文字列ではなくタプルになっている。
これで何をしているかというと、
```Python:サンプルコード
        elif event[0] == '-THREAD-':
            print('Got a message back from the thread: ', event[1])
```
と0番目の値がスレッド終了時に発火するイベントとしての役割で、1番目の値はそのときに表示するメッセージに過ぎない。
文法上は間違っていないとはいえ、サンプルとして不適切だろこんなの。


# あらためてPySimpleGUIの基本関数の勉強

## read()
PySimpleGUIのキモ。
イベント駆動するにあたり**event, values = window.read()** という構文を使うが、eventはともかくvaluesを活用している例は多くはない。
だがマルチスレッドを使うにはvaluesの理解が必要不可欠だ。
公式のドキュメントは[こちら](https://www.pysimplegui.org/en/latest/call%20reference/#read)。

**read()** には必須でない以下のパラメータを設定することができる。
　timeout　　タイムアウトになるまでの時間（ミリ秒）。省略可能でデフォ値は `None`。
　timeout_key　タイムアウト時に発火するイベント。省略可能でデフォ値は `"__TIMEOUT__"`。

戻り値については[前回](https://qiita.com/mo256man/items/2b4ac4a6ca344b812c33)はスルーしたが、今回は深く調べていこう。
　event　　　発火したイベント（押されたボタンのキーなど）
　values　　　valuesという変数名を使うことが多いが、実は**辞書**

辞書にはsg.InputText()やsg.Multiline()のテキストのほか、sg.Slider()で指定されている値などがウィジェットのキーとともに格納されている。

## write_event_value()

イベントを発火させたのと同等に辞書の登録をおこなう。
公式のドキュメントは[こちら](https://www.pysimplegui.org/en/latest/call%20reference/#write_event_value)。

**window.write_event_value(key, value)** という書き方をする。
　key　　　　辞書のキー
　value　　　値

window.write_event_value()で登録されたアイテムもwindow.read()で取得することができる。

サンプルコードでは
```Python:サンプルコード
def long_operation_thread(seconds, window):
    window.write_event_value(('-THREAD-', 'Starting thread - will sleep for {} seconds'.format(seconds)), None)
    time.sleep(seconds)
    window.write_event_value(('-THREAD-', '** DONE **'), 'Done!')
```
とここでもキーとしてタプルを設定している。
別スレッドで実行されているこの関数の中でこのようなイベントが発火すると、メインスレッドの中で先程も書いた
```Python:サンプルコード
        elif event[0] == '-THREAD-':
            print('Got a message back from the thread: ', event[1])
```
に反応してGUI画面上にメッセージが表示されるというわけだ。
あ、そうそう、サンプルコードでは **sg.Output()** を使っているのでprint()でGUI上にテキストが表示される。このあたりは[前回の記事](https://qiita.com/mo256man/items/2b4ac4a6ca344b812c33)を読んでください。

## もう少しスマートにできないものか

サンプルコードがやろうとしていることはわかった。
「print()させるためのイベント」と「そこで表示させる文字列」をメインスレッドに送りたいがためにタプルを使っているのだ。
とはいえ「print()させるためのイベント」が `'-THREAD-'`なのはどうかと思う（これが解読の妨げになった）し、そもそもこの方法は美しくない。
write_event_value() は辞書を定義するのだからイベントと表示させたい文字列を辞書として登録すればいいではないか。
つまり、こうだ。
テキストを表示したいガジェットが複数あり指定する必要があるなどの場合は適宜改良してください。

```Python:
# {"-PRINT-": key}という辞書を作る
def window_print(text):
    window.write_event_value("-PRINT-", text)

# 別スレッドで呼び出される関数
def func_in_another_thread():
    text = "ABC"
    window_print(text)

# メイン
def main():
    while True:
        event, values = window.read()
        if event == "-TASK-":
            window.start_thread(func_in_another_thread, end_key=end_key)
        elif event == "-PRINT-":
            window[key].print(values[event])
```

# 完成品

## 時計が止まらずテキストも更新される例

前回はメインの無限ループの中で時間がかかる処理をしていたのでその間は時計が止まってしまったが、マルチスレッドを使うことで時計を止めずに動かすことができるようになった。
![gui_5.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/09ff8164-4261-0a72-d9c6-0d15daafe8e5.gif)

## 任意のタイミングで割り込みする例

正しく動いていることは確認できたが見ているだけでは面白くない。
自動でテキストが更新される中、こちらで任意のタイミングで割り込みを入れる例…そうだ、アイドルソングに合いの手を入れるってのはどうだろう。コール？ ヲタ芸？ そんなの知らん。

![gui_6.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/520216/520d5b52-4c8c-50ae-68e2-d40f6acdb797.gif)

### ソース

<details><summary>折りたたみ</summary>

```Python
import PySimpleGUI as sg
import time
import datetime

class Gui():
    def __init__(self):
        layout = [[sg.Text(key="-CLOCK-")],
                  [sg.Multiline(size=(40,10), key="-LOG-")],
                  [sg.Button("sing", key="-BTN_SING-"),
                   sg.Button("ハイ！", key="-BTN_HI-"),
                   sg.Button("か～もね ハイ！", key="-BTN_KAMONE-"),
                   sg.Button("clear", key="-BTN_CLEAR-")
                   ]]
        self.window = sg.Window("ドキッ!こういうのが恋なの?", layout)

    def sing(self):
        song = ["こういうのが", "恋なの\n",
                "ドキドキドキーン\n", "",
                "しかもね\n", "",
                "スキスキスキ\n", "",
                "昨日よりも", "…かもね\n", "",
                "ハイ ハイ ハイ\n", "", ""]
        for phrase in song:
            self.print(phrase)
            time.sleep(60/142*4)            # 142bpmで4分の4拍子

    def print(self, text):
        self.window.write_event_value("-PRINT-", text)

    def clear(self):
        self.window.write_event_value("-CLEAR-", None)

gui = Gui()
window = gui.window

def main():
    while True:
        event, values = window.read(timeout=1)
        if event in (sg.WIN_CLOSED, "Exit"):
            break
        elif event == "-BTN_SING-":
            window.start_thread(gui.sing, end_key="-SING_END-")
        elif event == "-BTN_HI-":
            gui.print("　（ハイ！）\n")
        elif event == "-BTN_KAMONE-":
            gui.print("　（か～もね　ハイ！）\n")
        elif event == "-BTN_CLEAR-":
            gui.clear()
        elif event == "-SING_END-":
            gui.print("完")
        elif event == "-PRINT-":
            # 改行なしのプリント　改行が必要なときは文字列の最後に改行コードを入れる
            window["-LOG-"].print(values[event], end="")
        elif  event == "-CLEAR-":
            window["-LOG-"].update("")

        # これくらいなら無限ループの中に書いてもいいんじゃないかな
        now = datetime.datetime.now()
        str_now = now.strftime("%H:%M:%S")
        window["-CLOCK-"].update(str_now)

    window.close()

if __name__ == "__main__":
    main()
```
</details>

…作ってるときはノリノリだったがこうやってアニメGIFを見ても大して面白くないな。音が鳴るわけでもないし。

# 終わりに
これでまたひとつPySimpleGUIの表現力が増えた…が、ウェブアプリを作るのが良いのかPySimpleGUIを使うのが良いのか悩むことに変わりはないぞ。