0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-11-16

はじめに

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

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

image.png

サンプルプログラム

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

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

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

ポイント解説

1. EventManagerの役割

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

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

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

3. メモリリーク防止

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

4. スレッドセーフ性

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

まとめ

image.png

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

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

クラス図

シーケンス図

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?