PythonとTkinterで実現するマルチウィンドウアプリケーション - コールバックによるデータ共有

## はじめに
Tkinterを使用してマルチウィンドウアプリケーションを開発する際、課題の一つが「ウィンドウ間でのデータ共有」です。この記事では、コールバック関数を使用したデータ共有の実装方法について、具体的な実装例とともに解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c3978f80-29cf-dba6-e197-565ea7909075.png)


## 実装するアプリケーションの概要
シンプルなタスク管理アプリケーションを例に、ウィンドウ間でのデータ共有を実装します。

- メインウィンドウ：タスク一覧を表示
- サブウィンドウ：新規タスクの入力
- データ共有：入力されたタスクをメインウィンドウのリストに反映

## コード実装

```python
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox
from datetime import datetime

class MainWindow:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("タスク管理")
        self.root.geometry("400x500")
        
        # タスク一覧を表示するTreeview
        self.tree = ttk.Treeview(self.root, columns=('Task', 'Priority', 'Due Date'))
        self.tree.heading('Task', text='タスク')
        self.tree.heading('Priority', text='優先度')
        self.tree.heading('Due Date', text='期限')
        self.tree.column('#0', width=0, stretch=tk.NO)  # 非表示の先頭カラムを幅0に
        self.tree.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)
        
        # 新規タスク追加ボタン
        self.add_button = tk.Button(
            self.root,
            text="新規タスク追加",
            command=self.open_task_window
        )
        self.add_button.pack(pady=10)
        
        # タスクの合計を表示するラベル
        self.total_label = tk.Label(self.root, text="合計タスク数: 0")
        self.total_label.pack(pady=5)
    
    def open_task_window(self):
        """タスク入力ウィンドウを開く"""
        TaskWindow(self.root, self.add_task)
    
    def add_task(self, task_data):
        """
        新規タスクを追加するコールバック関数
        TaskWindowから呼び出される
        """
        # Treeviewにタスクを追加
        self.tree.insert(
            '',
            'end',
            values=(
                task_data['task'],
                task_data['priority'],
                task_data['due_date']
            )
        )
        
        # 合計タスク数を更新
        total = len(self.tree.get_children())
        self.total_label.config(text=f"合計タスク数: {total}")
    
    def run(self):
        """アプリケーションの実行"""
        self.root.mainloop()

class TaskWindow:
    def __init__(self, master, callback):
        # コールバック関数を保持
        self.callback = callback
        
        # ウィンドウの作成
        self.window = tk.Toplevel(master)
        self.window.title("新規タスク")
        self.window.geometry("300x400")
        
        # タスク名入力
        tk.Label(self.window, text="タスク名:").pack(pady=(10, 0))
        self.task_entry = tk.Entry(self.window, width=40)
        self.task_entry.pack(pady=(0, 10))
        
        # 優先度選択
        tk.Label(self.window, text="優先度:").pack(pady=(10, 0))
        self.priority_var = tk.StringVar(value="中")
        priorities = ["高", "中", "低"]
        self.priority_combo = ttk.Combobox(
            self.window,
            textvariable=self.priority_var,
            values=priorities,
            state="readonly"
        )
        self.priority_combo.pack(pady=(0, 10))
        
        # 期限日入力
        tk.Label(self.window, text="期限日 (YYYY-MM-DD):").pack(pady=(10, 0))
        self.due_date_entry = tk.Entry(self.window, width=40)
        self.due_date_entry.pack(pady=(0, 10))
        
        # 保存ボタン
        self.save_button = tk.Button(
            self.window,
            text="保存",
            command=self.save_task
        )
        self.save_button.pack(pady=20)
        
        # ウィンドウを親の中央に配置
        self.window.transient(master)
        self.window.grab_set()
    
    def save_task(self):
        """タスクを保存し、コールバック関数を呼び出す"""
        # 入力値の取得と検証
        task = self.task_entry.get().strip()
        priority = self.priority_var.get()
        due_date = self.due_date_entry.get().strip()
        
        # 入力チェック
        if not task:
            messagebox.showwarning("警告", "タスク名を入力してください")
            return
            
        if not due_date:
            messagebox.showwarning("警告", "期限日を入力してください")
            return
            
        # 日付形式のチェック
        try:
            datetime.strptime(due_date, '%Y-%m-%d')
        except ValueError:
            messagebox.showwarning(
                "警告",
                "期限日はYYYY-MM-DD形式で入力してください\n例: 2024-12-31"
            )
            return
        
        # データを辞書形式でまとめる
        task_data = {
            'task': task,
            'priority': priority,
            'due_date': due_date
        }
        
        # コールバック関数を呼び出してデータを渡す
        self.callback(task_data)
        
        # ウィンドウを閉じる
        self.window.destroy()

if __name__ == "__main__":
    app = MainWindow()
    app.run()
```

## コードの解説

### 1. データ共有の仕組み

この実装では、以下の流れでウィンドウ間のデータ共有を実現しています：

1. メインウィンドウ（`MainWindow`）がコールバック関数（`add_task`）を用意
2. サブウィンドウ（`TaskWindow`）の作成時にコールバック関数を渡す
3. サブウィンドウでデータが入力され保存ボタンが押されると、コールバック関数を通じてデータをメインウィンドウに渡す

### 2. 重要なポイント

#### コールバック関数の受け渡し
```python
def open_task_window(self):
    TaskWindow(self.root, self.add_task)  # コールバック関数を渡す
```

#### コールバック関数の保持
```python
def __init__(self, master, callback):
    self.callback = callback  # コールバック関数を保持
```

#### データの受け渡し
```python
# サブウィンドウでデータを辞書形式にまとめる
task_data = {
    'task': task,
    'priority': priority,
    'due_date': due_date
}

# コールバック関数を呼び出してデータを渡す
self.callback(task_data)
```

### 3. エラーハンドリング

入力値の検証とエラーメッセージの表示を実装しています：
- 必須項目のチェック
- 日付形式の検証
- ユーザーへのフィードバック

### 4. リソース管理

サブウィンドウの適切な管理のため、以下の実装を行っています：
- `transient(master)`による親ウィンドウとの関連付け
- `grab_set()`によるモーダルウィンドウ化
- 処理完了時の`destroy()`によるリソース解放

## 実行結果

このコードを実行すると：
1. タスク一覧を表示するメインウィンドウが表示されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b681976f-eca8-6bd4-796a-b9046ee30287.png)

2. 「新規タスク追加」ボタンをクリックすると、タスク入力用のサブウィンドウが開きます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/55f07e2d-4cd9-a7bd-bed2-491725ff0440.png)

3. サブウィンドウでタスクを入力し保存すると、メインウィンドウのリストに追加されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4ce4257c-4df8-e88c-9ead-18c4472e8b00.png)

4. タスクが追加されるたびに、合計タスク数が更新されます

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/b84bca1d-290e-3e29-0e67-23bde8c25e71.png)

## まとめ

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/90ee77b6-d013-2428-a9cb-ff5a76a20dba.png)

コールバック関数を使用したウィンドウ間のデータ共有には、以下のメリットがあります：

1. **シンプルな実装**: 複雑な状態管理が不要
2. **疎結合**: ウィンドウ間の依存関係が最小限
3. **柔軟性**: データの受け渡し方法を自由に定義可能

## クラス図

```mermaid
classDiagram
    class MainWindow {
        -root: Tk
        -tree: Treeview
        -add_button: Button
        -total_label: Label
        +__init__()
        +open_task_window()
        +add_task(task_data: dict)
        +run()
    }
    
    class TaskWindow {
        -window: Toplevel
        -callback: function
        -task_entry: Entry
        -priority_var: StringVar
        -priority_combo: Combobox
        -due_date_entry: Entry
        -save_button: Button
        +__init__(master: Tk, callback: function)
        -save_task()
    }
    
    MainWindow ..> TaskWindow : creates
    TaskWindow ..> MainWindow : calls callback
    
    note for MainWindow "メインウィンドウ\nタスク一覧の表示と\n管理を担当"
    note for TaskWindow "サブウィンドウ\n新規タスクの入力を\n担当"
```

## シーケンス図

```mermaid
sequenceDiagram
    autonumber
    actor User as ユーザー
    participant Main as MainWindow
    participant Task as TaskWindow
    
    Note over User,Task: タスク追加の流れ
    
    User->>+Main: 新規タスク追加ボタンをクリック
    Main->>+Task: create TaskWindow(self.root, self.add_task)
    Task-->>-Main: ウィンドウ生成完了
    Main-->>User: タスク入力ウィンドウを表示
    
    Note over User,Task: タスク入力フォームの操作
    User->>+Task: タスク名を入力
    User->>Task: 優先度を選択
    User->>Task: 期限日を入力
    User->>Task: 保存ボタンをクリック
    
    Note over Task,Main: 入力値の検証と保存
    Task->>Task: 入力値の検証
    alt 入力値が不正
        Task-->>User: エラーメッセージを表示
    else 入力値が正常
        Task->>Task: task_dataの作成
        Task->>+Main: add_task(task_data)を呼び出し
        Main->>Main: Treeviewにタスクを追加
        Main->>Main: 合計タスク数を更新
        Main-->>-Task: 処理完了
        Task->>Task: ウィンドウを閉じる
        Task-->>User: 入力ウィンドウを閉じる
        Main-->>User: 更新されたタスク一覧を表示
    end
```