3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Raspberry PiでTkinterとThreadを使ってGUIを固まらせずに動かす方法

Posted at

はじめに

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 でもこの方法なら 軽快に動作します。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?