はじめに
Raspberry Pi で GUI を作るときに便利なのが Tkinter です。
ただし、Tkinter の mainloop() は単一スレッドでイベントを処理するため、重い処理をそのまま書くと画面が固まることがあります。
本記事では Thread(スレッド) を使って GUI と処理を分離し、固まらないアプリを作る方法を紹介します。
TkinterとThreadとは?
- Tkinter: Python 標準の GUI ライブラリ。ボタンやラベル、テキスト入力などを簡単に作れます。
- Thread(スレッド):プログラム内で複数の処理を“同時進行”で動かす仕組み。重い処理を GUI と分離することで、画面のフリーズを防げます。
実行環境
- Raspberry Pi 4B (Raspberry Pi OS Bookworm)
- Python 3.11
- Tkinterはデフォルトで導入済
Tkinterの導入確認
デスクトップ環境でターミナルを開き、次を実行します。
python3 -m tkinter
テスト用の小さなウィンドウが出れば OK です。
デモ:固まる/固まらないを体験
同じウィンドウから「悪い例(フリーズ)」と「良い例(スレッド)」を試せるデモです。
thread_demo.py
import tkinter as tk
from tkinter import ttk
import threading
import time
import queue
class App(tk.Tk):
def __init__(self):
super().__init__()
self.title("Tkinter & Thread デモ")
self.geometry("420x220")
self.label = ttk.Label(self, text="0", font=("Arial", 28))
self.label.pack(pady=8)
btns = ttk.Frame(self)
btns.pack(pady=4)
self.btn_bad_sleep = ttk.Button(btns, text="悪い例(sleepでフリーズ)", command=self.bad_example_sleep)
self.btn_bad_sleep.grid(row=0, column=0, padx=6, pady=4)
self.btn_bad_cpu = ttk.Button(btns, text="悪い例(CPU計算でフリーズ)", command=self.bad_example_cpu)
self.btn_bad_cpu.grid(row=0, column=1, padx=6, pady=4)
self.btn_good = ttk.Button(btns, text="良い例(スレッド)開始", command=self.start_worker)
self.btn_good.grid(row=1, column=0, padx=6, pady=4)
self.btn_stop = ttk.Button(btns, text="停止", command=self.stop_worker, state=tk.DISABLED)
self.btn_stop.grid(row=1, column=1, padx=6, pady=4)
self.status = ttk.Label(self, text="準備完了")
self.status.pack(pady=6)
# スレッド通信用
self.q = queue.Queue()
self.stop_event = threading.Event()
self.worker_thread = None
# メインループで定期的にキューを処理
self.after(50, self._drain_queue)
# --- 悪い例1:sleepでメインスレッドを止める ---
def bad_example_sleep(self):
self.status.config(text="悪い例(sleep)実行中:GUIが固まります…")
# ここで mainloop がブロックされる → 再描画/クリック受付が止まる
time.sleep(3)
self.status.config(text="悪い例(sleep)終了:操作できるように戻りました")
# --- 悪い例2:CPUを回し続けてメインスレッドを占有 ---
def bad_example_cpu(self):
self.status.config(text="悪い例(CPU計算)実行中:GUIが固まります…")
start = time.monotonic()
acc = 0
# 目安:数秒間ひたすら計算して mainloop を占有
while time.monotonic() - start < 5:
# 無意味な計算でCPUを占有(描画イベントが処理されない)
for i in range(20000):
acc += (i * i) % 97
self.status.config(text=f"悪い例(CPU計算)終了:acc={acc}")
# --- 良い例:別スレッドで処理、UIはafterで更新 ---
def start_worker(self):
if self.worker_thread and self.worker_thread.is_alive():
return
self.stop_event.clear()
self.btn_good.config(state=tk.DISABLED)
self.btn_stop.config(state=tk.NORMAL)
self.status.config(text="良い例(スレッド)実行中…")
self.worker_thread = threading.Thread(target=self._worker_job, daemon=True)
self.worker_thread.start()
def stop_worker(self):
self.stop_event.set()
self.btn_stop.config(state=tk.DISABLED)
self.status.config(text="停止指示を送りました…")
def _worker_job(self):
i = 0
try:
while not self.stop_event.is_set():
i += 1
time.sleep(0.05) # 何かの重い処理の代わり
self.q.put(("count", i))
self.q.put(("status", f"良い例(スレッド)処理中… {i}"))
finally:
self.q.put(("done", None))
def _drain_queue(self):
try:
while True:
kind, value = self.q.get_nowait()
if kind == "count":
self.label.config(text=str(value))
elif kind == "status":
self.status.config(text=value)
elif kind == "done":
self.btn_good.config(state=tk.NORMAL)
self.btn_stop.config(state=tk.DISABLED)
if self.stop_event.is_set():
self.status.config(text="スレッド停止完了")
else:
self.status.config(text="スレッド終了")
except queue.Empty:
pass
self.after(50, self._drain_queue)
if __name__ == "__main__":
App().mainloop()```
- 「悪い例」はメインスレッドを占有するため、ウィンドウがリサイズ不能・無反応になります。
- 「良い例」は処理を別スレッドへ分離し、UI 更新は queue + after() でメインスレッド側から行うため、操作が固まりません。
まとめ
Tkinter は便利ですが、重い処理をメインスレッドに書くとフリーズします。Thread で処理を分離し、UI 更新は queue と after() でメインスレッドから実行するのが定石です。Raspberry Pi でもこの方法なら 軽快に動作します。