button_start がメインスレッド上で実行されるので、
while ループが終わるまでは他の処理は実行されません。
メインループ → button_start → メインループ →
と順番に実行されるので、
button_start 内で時間の掛かる処理を行い、
メインループに処理が戻らないと応答なしになります。
GUI等のイベント駆動型プログラミングで、よくある誤解なのですが、
イベントハンドラの関数(button_start)は、
勝手に裏で同時に実行される訳ではありません。
イベントハンドラ内でループを使う場合は、
スレッドを使う等が一般的な解決策です。
他に幾つか解決案を紹介します。
- ウインドウが固まらないように、ループ内で定期的に update() を呼ぶ
これは本当に一時的な回避策。使える状況は限られますが、有効な場合も有。
- 別スレッド/プロセスを作り、ループを外部から中断できるフラグを仕込む。
y_waiwaiさん提案の方法に該当します。
- タイマーを使う
after や after_idle を使って、
イベントループの合間に任意の処理を実行します。
但し、1回の処理は、直ぐに終わる程度に分割が必要です。
- ジェネレーター(とタイマー)を使う
同上ですが、ジェネレータを使ってループ文で記述可能な方法。
- 非同期ライブラリ (asyncio等) を使う
1と3、4 は、ネットワークやデータベースが絡む処理、
時間の掛かる計算処理には向きません。
簡単なアニメーション等やスケジューリングの用途には使えます。
2 のスレッドは汎用的な方法ですが、別スレッドからGUIにアクセスする際は、
もうひと工夫が必要なので注意。
「各種処理」の内容次第では、適切な解決策は変わってきます。
1, 2, 3 は探せばサンプルコードは見つかると思いますので、
4 の方法のみコードを紹介します。
5 は選択肢として言及したものの、長くなるので割愛。
興味があれば、「非同期プログラミング」について調べて見て下さい。
python
1
2import tkinter as tk
3from tkinter import ttk
4
5
6def after_timed_gen(root, gen, done=None, _stop=None):
7 """
8 ジェネレーターをTkinterのタイマーで消化する関数
9 after() に指定する 遅延ミリ秒を yield で指定できます。
10 """
11
12 def next_gen():
13 interval = next(gen, _stop)
14 if interval is not _stop:
15 root.after(interval, next_gen)
16 else:
17 if done:
18 done()
19
20 root.after_idle(next_gen)
21 return gen
22
23
24if __name__ == "__main__":
25 root = tk.Tk()
26 label = ttk.Label(root)
27 label.pack()
28
29 def countUp(count=0):
30 while True:
31 count += 1
32 label.config(text="Count: {}".format(count))
33
34 # メインスレッド内で GUIの応答を止めることなく、
35 # time.sleep(1) としたい場合の代替策です。
36 yield 1*1000
37
38 gen = None
39
40 def button_start():
41 global gen
42 gen = after_timed_gen(root, countUp())
43
44 def button_stop():
45 global gen
46 if gen:
47 gen.close()
48 gen = None
49
50
51 button1 = ttk.Button(root, text='Start', command=button_start)
52 button1.pack()
53
54 button2 = ttk.Button(root, text='Cancel', command=button_stop)
55 button2.pack()
56
57 root.mainloop()
スレッドを使う方法の停止・中断デモ source
※ブラウザ上で実行できます
バッドをするには、ログインかつ
こちらの条件を満たす必要があります。
2020/07/23 23:19