PythonとTkinterで作る共有ステート管理 - カスタムEventManagerによるウィンドウ間の状態同期 -

## はじめに
Tkinterで複数のウィンドウを持つアプリケーションを作る際、ウィンドウ間でデータを共有・同期させる必要が出てきます。
よくある解決策として、グローバル変数を使用する方法がありますが、アプリケーションが大きくなるにつれて管理が難しくなります。

この記事では、イベントベースの状態管理を実装することで、この問題を解決する方法を紹介します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/d2bd3f2f-bc7c-5b86-0293-cebe4a0df11f.png)

## サンプルプログラム

商品の在庫管理を行う簡単なアプリケーションを例に説明します。
メインウィンドウで在庫数を更新すると、サブウィンドウにもリアルタイムで反映されます。

```python
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()
```

プログラムの画面の例:
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/facc9271-9298-b57b-b55a-12b6d4326528.png)

入荷、出荷のボタンを押下した在庫数の変更が在庫状況の進捗に反映される

## ポイント解説

### 1. EventManagerの役割

EventManagerは、イベントの発行（publish）と購読（subscribe）を管理します。
これにより、ウィンドウ間の直接的な参照を避け、疎結合な設計を実現しています。

### 2. イベントの発行と購読

- メインウィンドウ：在庫数が変更されたときに`stock_updated`イベントを発行
- サブウィンドウ：`stock_updated`イベントを購読して在庫表示を更新

### 3. メモリリーク防止

サブウィンドウを閉じるときは、`unsubscribe`でイベントの購読を解除しています。
これにより、不要なメモリ使用を防止します。

### 4. スレッドセーフ性

EventManagerは内部で`threading.Lock`を使用し、複数のウィンドウからの同時アクセスに対応しています。

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b94bed97-bd93-9fae-18d3-10930123ccd6.png)

EventManagerを使用することで、以下のメリットが得られました：

- ウィンドウ間の疎結合な状態管理
- クリーンな依存関係
- スレッドセーフな実装

## クラス図

```mermaid
classDiagram
    class EventManager {
        -Dict~str, List[Subscriber]~ _subscribers
        -Lock _lock
        +subscribe(event_name: str, callback: Callable, sync: bool)
        +unsubscribe(event_name: str, callback: Callable)
        +publish(event_name: str, *args, **kwargs)
    }

    class Subscriber {
        +Callable callback
        +bool sync
    }

    class MainWindow {
        +Tk root
        +EventManager event_manager
        -int stock
        -Label stock_label
        +__init__(event_manager: EventManager)
        +stock_in()
        +stock_out()
        +open_sub_window()
    }

    class SubWindow {
        +Toplevel window
        +EventManager event_manager
        -Label stock_label
        -Progressbar gauge
        +__init__(event_manager: EventManager, initial_stock: int)
        +update_stock(stock: int)
        -on_closing()
    }

    EventManager "1" -- "*" Subscriber : contains
    MainWindow "1" --> "1" EventManager : uses
    SubWindow "1" --> "1" EventManager : uses
    MainWindow ..> SubWindow : creates
```

## シーケンス図

```mermaid
sequenceDiagram
    participant User as ユーザー
    participant Main as MainWindow
    participant Event as EventManager
    participant Sub as SubWindow
    
    Note over User,Sub: 初期化フロー
    User->>Main: アプリケーション起動
    activate Main
    Main->>Event: 生成
    Main->>Main: UI初期化
    deactivate Main
    
    User->>Main: SubWindowを開く
    activate Main
    Main->>Sub: 生成(event_manager, initial_stock)
    activate Sub
    Sub->>Event: subscribe("stock_updated", update_stock)
    deactivate Sub
    deactivate Main
    
    Note over User,Sub: 在庫更新フロー
    User->>Main: 「入荷」ボタンクリック
    activate Main
    Main->>Main: stock += 10
    Main->>Main: stock_label更新
    Main->>Event: publish("stock_updated", stock)
    activate Event
    Event->>Sub: update_stock(stock)呼び出し
    activate Sub
    Sub->>Sub: stock_label更新
    Sub->>Sub: gauge更新
    deactivate Sub
    deactivate Event
    deactivate Main
    
    Note over User,Sub: SubWindow終了フロー
    User->>Sub: ウィンドウを閉じる
    activate Sub
    Sub->>Event: unsubscribe("stock_updated", update_stock)
    Sub->>Sub: ウィンドウを破棄
    deactivate Sub
```