tkinter - 静かなる名辞
pythonとプログラミングのこと
2020-05-07T20:42:34+09:00
hayataka2049
Hatena::Blog
hatenablog://blog/10328537792367869878
tkinterで遅い処理を別スレッドに投げ画面が固まらないようにする
hatenablog://entry/10257846132709631960
2019-01-20T01:29:26+09:00
2020-04-26T20:49:22+09:00 tkinterでコールバック関数の実行に時間がかかる場合、実行している間ずっとGUIが固まります。そこで、別スレッドに実行を投げてこれを回避することができます。
<div class="section">
<h3>はじめに</h3>
<p> pythonで書いた何かの処理にGUIのwrapperを付けたいので、お手軽にtkinterで作ろう! みたいなことをする人が果たしてどれだけいるのかは知りませんが、tkinterのコールバックで遅い処理をすることがあります。まあ、あるいは、練習でタイマーとかを作るときにwaitさせたくなるというケースの方が多いのかもしれませんが。</p><p> この状況を再現した下のようなコードについて考えてみます。</p>
<pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> time
<span class="synPreProc">import</span> tkinter <span class="synStatement">as</span> tk
<span class="synStatement">def</span> <span class="synIdentifier">f</span>(event):
<span class="synIdentifier">print</span>(<span class="synConstant">"start callback"</span>)
s.set(<span class="synConstant">"start callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"end callback"</span>)
s.set(<span class="synConstant">"end callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"(空白に戻す)"</span>)
s.set(<span class="synConstant">""</span>)
<span class="synStatement">def</span> <span class="synIdentifier">main</span>():
<span class="synStatement">global</span> s
root = tk.Tk()
s = tk.StringVar()
s.set(<span class="synConstant">""</span>)
label = tk.Label(root, textvariable=s)
label.pack()
button = tk.Button(root, text=<span class="synConstant">"Button"</span>)
button.bind(<span class="synConstant">"<1>"</span>, f)
button.pack()
root.mainloop()
<span class="synStatement">if</span> __name__ == <span class="synConstant">"__main__"</span>:
main()
</pre><p> ボタンが押されたとき、コールバック関数の中で動的にラベルを書き換えようという訳ですね。</p><p> でも実行してみるとわかりますが、stdoutに対するprintは出てきても、画面はうまく書き換わりません。</p><p> <figure class="figure-image figure-image-fotolife" title="空白の画面のまま・・・"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hayataka2049/20190120/20190120010543.png" alt="空白の画面のまま・・・" title="f:id:hayataka2049:20190120010543p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>空白の画面のまま・・・</figcaption></figure></p>
</div>
<div class="section">
<h3>原因</h3>
<p> コールバック関数がreturnするまでの間、呼び出し元の処理は止まっています。よって、画面の更新処理などもすべて止まってしまい、StringVarへの書き換えも反映されません。</p><p> ちなみに、この状態のときに操作しようとすると「応答なし」になると思います。</p><p><span style="font-size: 80%">スポンサーリンク</span><br />
<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script></p>
<p><ins class="adsbygoogle"
style="display:block"
data-ad-client="ca-pub-6261827798827777"
data-ad-slot="1744230936"
data-ad-format="auto"
data-full-width-responsive="true"></ins><br />
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script></p><p></p>
</div>
<div class="section">
<h3>対策</h3>
<p> コールバック関数で別スレッドにやりたい処理の実行を投げ、コールバック関数そのものはさっさとreturnします。</p>
<pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> time
<span class="synPreProc">import</span> tkinter <span class="synStatement">as</span> tk
<span class="synPreProc">import</span> threading
<span class="synStatement">def</span> <span class="synIdentifier">f</span>(event):
<span class="synIdentifier">print</span>(<span class="synConstant">"start callback"</span>)
s.set(<span class="synConstant">"start callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"end callback"</span>)
s.set(<span class="synConstant">"end callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"(空白に戻す)"</span>)
s.set(<span class="synConstant">""</span>)
<span class="synStatement">def</span> <span class="synIdentifier">callback</span>(event):
th = threading.Thread(target=f, args=(event,))
th.start()
<span class="synStatement">def</span> <span class="synIdentifier">main</span>():
<span class="synStatement">global</span> s
root = tk.Tk()
s = tk.StringVar()
s.set(<span class="synConstant">""</span>)
label = tk.Label(root, textvariable=s)
label.pack()
button = tk.Button(root, text=<span class="synConstant">"Button"</span>)
button.bind(<span class="synConstant">"<1>"</span>, callback)
button.pack()
root.mainloop()
<span class="synStatement">if</span> __name__ == <span class="synConstant">"__main__"</span>:
main()
</pre><p> 今度は問題なく意図通りの結果が得られます。スレッドをjoinしていませんが、コールバック関数で待つわけには行きませんからこうするしかありません。このスレッドは放っておけば(実行が終われば)勝手に消えるので、たぶん大丈夫です。</p><p><figure class="figure-image figure-image-fotolife" title="このように画面が変化する"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hayataka2049/20190120/20190120010951.png" alt="このように画面が変化する" title="f:id:hayataka2049:20190120010951p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>このように画面が変化する</figcaption></figure></p>
</div>
<div class="section">
<h3>多重実行対策</h3>
<p> 上の方法だとスレッドが幾つでも同時に走り得ます。end callbackになったあたりでもう一度ボタンを押すとカオスな切り替わりが観察できます。</p><p> 一つの考え方としては、実行中はボタンをDISABLEDにしてしまえば良いというものがあります。ただし、これはcommandでコールバックを指定した場合しか効きません。bindを使った場合は別の手立てが必要になります。</p>
<pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> time
<span class="synPreProc">import</span> tkinter <span class="synStatement">as</span> tk
<span class="synPreProc">import</span> threading
<span class="synStatement">def</span> <span class="synIdentifier">f</span>():
<span class="synIdentifier">print</span>(<span class="synConstant">"start callback"</span>)
s.set(<span class="synConstant">"start callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"end callback"</span>)
s.set(<span class="synConstant">"end callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"(空白に戻す)"</span>)
s.set(<span class="synConstant">""</span>)
button.configure(state=tk.NORMAL)
<span class="synStatement">def</span> <span class="synIdentifier">callback</span>():
button.configure(state=tk.DISABLED)
th = threading.Thread(target=f)
th.start()
<span class="synStatement">def</span> <span class="synIdentifier">main</span>():
<span class="synStatement">global</span> s, button
root = tk.Tk()
s = tk.StringVar()
s.set(<span class="synConstant">""</span>)
label = tk.Label(root, textvariable=s)
label.pack()
button = tk.Button(root, command=callback, text=<span class="synConstant">"Button"</span>)
button.pack()
root.mainloop()
<span class="synStatement">if</span> __name__ == <span class="synConstant">"__main__"</span>:
main()
</pre><p><figure class="figure-image figure-image-fotolife" title="このような表示になりクリックが効かなくなる"><span itemscope itemtype="http://schema.org/Photograph"><img src="https://cdn-ak.f.st-hatena.com/images/fotolife/h/hayataka2049/20190120/20190120011933.png" alt="このような表示になりクリックが効かなくなる" title="f:id:hayataka2049:20190120011933p:plain" class="hatena-fotolife" itemprop="image"></span><figcaption>このような表示になりクリックが効かなくなる</figcaption></figure></p><p> bindを使うので別の方法で多重実行を防ぎたいというような場合、ロックを使うのが正攻法だと思います。</p>
<pre class="code lang-python" data-lang="python" data-unlink><span class="synPreProc">import</span> time
<span class="synPreProc">import</span> tkinter <span class="synStatement">as</span> tk
<span class="synPreProc">import</span> threading
<span class="synStatement">def</span> <span class="synIdentifier">f</span>(event):
<span class="synIdentifier">print</span>(<span class="synConstant">"start callback"</span>)
s.set(<span class="synConstant">"start callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"end callback"</span>)
s.set(<span class="synConstant">"end callback"</span>)
time.sleep(<span class="synConstant">2</span>)
<span class="synIdentifier">print</span>(<span class="synConstant">"(空白に戻す)"</span>)
s.set(<span class="synConstant">""</span>)
button.configure(state=tk.NORMAL)
lock.release()
<span class="synStatement">def</span> <span class="synIdentifier">callback</span>(event):
<span class="synStatement">if</span> lock.acquire(blocking=<span class="synIdentifier">False</span>):
button.configure(state=tk.DISABLED)
th = threading.Thread(target=f, args=(event,))
th.start()
<span class="synStatement">else</span>:
<span class="synIdentifier">print</span>(<span class="synConstant">"(すでに実行中)"</span>)
<span class="synStatement">def</span> <span class="synIdentifier">main</span>():
<span class="synStatement">global</span> s, button, lock
lock = threading.Lock()
root = tk.Tk()
s = tk.StringVar()
s.set(<span class="synConstant">""</span>)
label = tk.Label(root, textvariable=s)
label.pack()
button = tk.Button(root, text=<span class="synConstant">"Button"</span>)
button.bind(<span class="synConstant">"<1>"</span>, callback)
button.pack()
root.mainloop()
<span class="synStatement">if</span> __name__ == <span class="synConstant">"__main__"</span>:
main()
</pre><p> threading.Lock.acquireメソッドで、ロックが獲得されていない場合はロックを獲得してTrueを返します。獲得されていた場合の挙動ですが、第一引数blockingをTrueにしていればロックが解放されるまで待ち(デフォルトの挙動)、FalseにすればすぐにFalseを返してくれます。今回はTrueにしたら台無しなのでFalseにし、ifで動作を切り分けることにしています。</p><p><a href="https://docs.python.jp/3/library/threading.html#threading.Lock.acquire">threading --- スレッドベースの並列処理 — Python 3.7.4 ドキュメント</a></p><p> このようにロックを使うことでも多重実行を防止できます。</p>
</div>
<div class="section">
<h3>まとめ</h3>
<p> 何もしなければシングルスレッドで動く、というのはわかっていれば当たり前のことなのですが、それに気づかないと困惑すると思います。</p><p> 固まるのを妥協してしまうのも一つの考え方ですが、遅い処理を走らせているときでも画面を動かすのはそんなに難しい処理という訳でもないので、よかったら皆さんやってみてください。</p>
</div>
hayataka2049