はじめに
Tkinterで複数のウィンドウを持つアプリケーションを作る際、ウィンドウ間でデータを共有・同期させる必要が出てきます。
よくある解決策として、グローバル変数を使用する方法がありますが、アプリケーションが大きくなるにつれて管理が難しくなります。
この記事では、イベントベースの状態管理を実装することで、この問題を解決する方法を紹介します。
サンプルプログラム
商品の在庫管理を行う簡単なアプリケーションを例に説明します。
メインウィンドウで在庫数を更新すると、サブウィンドウにもリアルタイムで反映されます。
import tkinter as tk
from tkinter import ttk
from dataclasses import dataclass
from typing import Callable, Dict, List
import threading
# イベント管理用のクラス
@dataclass
class Subscriber:
callback: Callable
sync: bool = True
class EventManager:
def __init__(self):
self._subscribers: Dict[str, List[Subscriber]] = {}
self._lock = threading.Lock()
def subscribe(self, event_name: str, callback: Callable, sync: bool = True) -> None:
"""イベントの購読を登録"""
with self._lock:
if event_name not in self._subscribers:
self._subscribers[event_name] = []
self._subscribers[event_name].append(Subscriber(callback, sync))
def unsubscribe(self, event_name: str, callback: Callable) -> None:
"""イベントの購読を解除"""
with self._lock:
if event_name in self._subscribers:
self._subscribers[event_name] = [
sub for sub in self._subscribers[event_name]
if sub.callback != callback
]
def publish(self, event_name: str, *args, **kwargs) -> None:
"""イベントを発行"""
with self._lock:
if event_name not in self._subscribers:
return
subscribers = self._subscribers[event_name].copy()
for subscriber in subscribers:
if subscriber.sync:
subscriber.callback(*args, **kwargs)
else:
threading.Thread(
target=subscriber.callback,
args=args,
kwargs=kwargs
).start()
# メインウィンドウ
class MainWindow:
def __init__(self, event_manager: EventManager):
self.root = tk.Tk()
self.root.title("在庫管理")
self.event_manager = event_manager
# 商品の在庫数
self.stock = 100
# UI部品の作成
frame = ttk.Frame(self.root, padding="10")
frame.grid()
ttk.Label(frame, text="商品A 在庫数:").grid(row=0, column=0)
self.stock_label = ttk.Label(frame, text=str(self.stock))
self.stock_label.grid(row=0, column=1)
ttk.Button(frame, text="入荷(+10)", command=self.stock_in).grid(row=1, column=0)
ttk.Button(frame, text="出荷(-10)", command=self.stock_out).grid(row=1, column=1)
# サブウィンドウを開くボタン
ttk.Button(
frame,
text="在庫状況ウィンドウを開く",
command=self.open_sub_window
).grid(row=2, column=0, columnspan=2)
def stock_in(self):
self.stock += 10
self.stock_label.config(text=str(self.stock))
# 在庫更新イベントを発行
self.event_manager.publish("stock_updated", self.stock)
def stock_out(self):
if self.stock >= 10:
self.stock -= 10
self.stock_label.config(text=str(self.stock))
# 在庫更新イベントを発行
self.event_manager.publish("stock_updated", self.stock)
def open_sub_window(self):
SubWindow(self.event_manager, self.stock)
# サブウィンドウ
class SubWindow:
def __init__(self, event_manager: EventManager, initial_stock: int):
self.window = tk.Toplevel()
self.window.title("在庫状況")
self.event_manager = event_manager
frame = ttk.Frame(self.window, padding="10")
frame.grid()
ttk.Label(frame, text="現在の在庫状況").grid(row=0, column=0, columnspan=2)
self.gauge = ttk.Progressbar(
frame,
length=200,
mode='determinate',
maximum=200
)
self.gauge.grid(row=1, column=0, columnspan=2)
self.stock_label = ttk.Label(frame, text=f"{initial_stock}個")
self.stock_label.grid(row=2, column=0, columnspan=2)
# 在庫更新イベントの購読を開始
self.event_manager.subscribe("stock_updated", self.update_stock)
# ウィンドウが閉じられたときのイベント購読解除
self.window.protocol("WM_DELETE_WINDOW", self.on_closing)
# 初期在庫を表示
self.update_stock(initial_stock)
def update_stock(self, stock: int):
self.stock_label.config(text=f"{stock}個")
self.gauge['value'] = stock
def on_closing(self):
# イベントの購読を解除してからウィンドウを閉じる
self.event_manager.unsubscribe("stock_updated", self.update_stock)
self.window.destroy()
def main():
event_manager = EventManager()
main_window = MainWindow(event_manager)
main_window.root.mainloop()
if __name__ == "__main__":
main()
入荷、出荷のボタンを押下した在庫数の変更が在庫状況の進捗に反映される
ポイント解説
1. EventManagerの役割
EventManagerは、イベントの発行(publish)と購読(subscribe)を管理します。
これにより、ウィンドウ間の直接的な参照を避け、疎結合な設計を実現しています。
2. イベントの発行と購読
- メインウィンドウ:在庫数が変更されたときに
stock_updated
イベントを発行 - サブウィンドウ:
stock_updated
イベントを購読して在庫表示を更新
3. メモリリーク防止
サブウィンドウを閉じるときは、unsubscribe
でイベントの購読を解除しています。
これにより、不要なメモリ使用を防止します。
4. スレッドセーフ性
EventManagerは内部でthreading.Lock
を使用し、複数のウィンドウからの同時アクセスに対応しています。
まとめ
EventManagerを使用することで、以下のメリットが得られました:
- ウィンドウ間の疎結合な状態管理
- クリーンな依存関係
- スレッドセーフな実装