LoginSignup
3
2

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

Posted at

目次

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

公式サンプル

時間がかかる処理をマルチスレッドでおこなうサンプルがPySimpleGUIのGitHubにある。

[Do Long Task]ボタンをクリックするとメッセージが表示され、5秒間停止し、その後追加でメッセージが表示される。
また、[Click Me]ボタンをクリックするとロングタスクが実行中でもお構いなしで即座に別メッセージを表示する。というものだ。
gui_demo.png

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

start_thread()

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

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

func

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

# 公式のサンプルコード
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_CLOSEDNoneは同値。だから end_key=None とするとウィンドウの右上バッテンを押したときと同じ挙動を起こしてしまう。
もちろん作る人次第だが、バッテンを押すとプログラムを終了させることが多いだろう。スレッドが終わったらウィンドウが閉じてGUIプログラムが終了してしまう、そんなGUIアプリがあってよいだろうか。いや、あってはならない。
ということで気をつけましょう。

また、サンプルコードでは

サンプルコード
window.start_thread(funcは略, ('-THREAD-', '-THEAD ENDED-'))

とend_keyの値が文字列ではなくタプルになっている。
これで何をしているかというと、

サンプルコード
        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の理解が必要不可欠だ。
公式のドキュメントはこちら

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

戻り値については前回はスルーしたが、今回は深く調べていこう。
 event   発火したイベント(押されたボタンのキーなど)
 values   valuesという変数名を使うことが多いが、実は辞書

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

write_event_value()

イベントを発火させたのと同等に辞書の登録をおこなう。
公式のドキュメントはこちら

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

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

サンプルコードでは

サンプルコード
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!')

とここでもキーとしてタプルを設定している。
別スレッドで実行されているこの関数の中でこのようなイベントが発火すると、メインスレッドの中で先程も書いた

サンプルコード
        elif event[0] == '-THREAD-':
            print('Got a message back from the thread: ', event[1])

に反応してGUI画面上にメッセージが表示されるというわけだ。
あ、そうそう、サンプルコードでは sg.Output() を使っているのでprint()でGUI上にテキストが表示される。このあたりは前回の記事を読んでください。

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

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

# {"-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

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

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

gui_6.gif

ソース

折りたたみ
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()

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

終わりに

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

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2