PythonとTkinterで作るマルチウィンドウアプリケーション - Queueで解決するスレッド間通信

## はじめに
GUIアプリケーションで重い処理を行う際、メインスレッドがフリーズしないようにするには非同期処理が必要です。
本記事では、`Queue`を使って安全にデータを受け渡す方法を、実践的なCSVデータ可視化アプリケーションの実装を通じて解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/fcff53e2-834c-2f42-36f1-3e38509cc207.png)

## よくある問題点と解決策

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/e325fdfc-9a1c-a0bf-96ce-2dda307f2658.png)

### 問題1: UIのフリーズ
```python
# 悪い例
def button_click():
    result = heavy_process()  # UIがフリーズ
    show_result(result)

# 良い例
def button_click():
    thread = threading.Thread(target=process_in_background)
    thread.start()

def process_in_background():
    result = heavy_process()
    result_queue.put(result)  # Queueで安全に結果を送信
```

### 問題2: スレッドからのGUI操作
```python
# 悪い例
def background_thread():
    result = process_data()
    label.config(text=result)  # 危険！

# 良い例
def background_thread():
    result = process_data()
    queue.put(result)

def check_queue():  # メインスレッドで実行
    try:
        result = queue.get_nowait()
        label.config(text=result)  # 安全
    except Queue.Empty:
        pass
    root.after(100, check_queue)
```

## サンプルアプリケーション
CSVデータを読み込んでグラフ表示するアプリケーションを実装してみましょう。

```python
import tkinter as tk
from tkinter import ttk, filedialog
import threading
import queue
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg

class DataProcessWindow:
    def __init__(self, master, file_path, result_queue):
        self.top = tk.Toplevel(master)
        self.top.title("データ処理中")
        self.top.geometry("300x150")
        
        self.file_path = file_path
        self.result_queue = result_queue
        
        self.progress = ttk.Progressbar(
            self.top,
            mode='indeterminate',
            length=200
        )
        self.progress.pack(pady=20)
        self.progress.start()
        
        # 処理スレッドの開始
        self.thread = threading.Thread(target=self.process_data)
        self.thread.daemon = True
        self.thread.start()
    
    def create_figures(self, data):
        """メインスレッドでグラフを作成（重要）"""
        fig, ax = plt.subplots(figsize=(10, 6))
        data.plot(kind='bar', ax=ax)
        plt.tight_layout()
        return fig
    
    def process_data(self):
        try:
            # CSVファイルの読み込みと処理
            df = pd.read_csv(self.file_path)
            monthly_sum = df.groupby('month')['value'].sum()
            
            # 集計結果をキューに送信
            self.result_queue.put(('data', monthly_sum))
            
        except Exception as e:
            self.result_queue.put(('error', str(e)))
        finally:
            self.result_queue.put(('close_window', None))

class MainWindow:
    def __init__(self):
        self.root = tk.Tk()
        self.root.title("データ分析")
        self.root.geometry("400x300")
        
        self.file_button = ttk.Button(
            self.root,
            text="ファイルを選択",
            command=self.select_file
        )
        self.file_button.pack(pady=20)
        
        self.result_queue = queue.Queue()
        self.check_queue()
    
    def select_file(self):
        file_path = filedialog.askopenfilename(
            filetypes=[("CSV files", "*.csv")]
        )
        if file_path:
            self.file_button.config(state='disabled')
            self.process_window = DataProcessWindow(
                self.root, file_path, self.result_queue
            )
    
    def check_queue(self):
        try:
            while True:
                status, data = self.result_queue.get_nowait()
                
                if status == 'data':
                    # メインスレッドでグラフを作成
                    fig = self.process_window.create_figures(data)
                    self.show_result(fig)
                    self.file_button.config(state='normal')
                elif status == 'close_window':
                    if self.process_window:
                        self.process_window.top.destroy()
                
        except queue.Empty:
            pass
        finally:
            self.root.after(100, self.check_queue)
    
    def show_result(self, figure):
        window = tk.Toplevel(self.root)
        window.title("分析結果")
        window.geometry("800x600")
        
        canvas = FigureCanvasTkAgg(figure, master=window)
        canvas.draw()
        canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)
        
        # マウスイベントによるエラーを回避
        canvas.mpl_connect('motion_notify_event', lambda event: None)
    
    def run(self):
        self.root.mainloop()

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

## 動作確認用のサンプルデータ
以下の内容を`sample_data.csv`として保存してください：

```csv
month,value,category
2023-01,1250000,A
2023-02,1380000,A
2023-03,1420000,A
2023-04,1580000,A
2023-05,1680000,A
2023-06,1720000,A
```

## 動作例
ファイル選択を押下するとファイルダイアログが開く。
入力するCSVをファイルを選択すると、読み込みのプログレスバーが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/c3161919-5fc8-ed27-9473-f57802b41739.png)

読み込みが完了すると分析結果のグラフがの画面が出力される。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/4aba3c98-adbd-fb88-04e2-35c8848378f1.png)

## 実装のポイント

1. **スレッド間通信は必ずQueueを使用**
   - バックグラウンドスレッドからGUIを直接操作しない
   - データやステータスはすべてQueueを介して送受信

2. **Matplotlibの操作はメインスレッドで**
   - グラフの作成や更新は必ずメインスレッドで行う
   - バックグラウンドスレッドではデータ処理のみを実施

3. **例外処理を忘れずに**
   - バックグラウンドスレッドでの例外はキューで通知
   - メインスレッドで適切にエラーハンドリング

## まとめ
- `Queue`を使用することで、スレッド間の安全なデータ通信が実現できます
- GUIの操作は必ずメインスレッドで行うことで、予期せぬエラーを防げます
- 適切な非同期処理により、レスポンシブなGUIアプリケーションが実装できます

## クラス図
```mermaid
classDiagram
    class MainWindow {
        Queue result_queue
        +select_file()
        +check_queue()
        +show_result()
    }

    class DataProcessWindow {
        Thread worker_thread
        +create_figures()
        -process_data()
    }

    class ResultWindow {
        +show_graph()
    }

    MainWindow --> DataProcessWindow : 1. ファイル選択
    DataProcessWindow --> MainWindow : 2. 処理結果(Queue)
    MainWindow --> ResultWindow : 3. グラフ表示

    note for MainWindow "メインスレッド"
    note for DataProcessWindow "ワーカースレッド"
    note for ResultWindow "結果表示"
```

## シーケンス図
```mermaid
sequenceDiagram
    participant User
    participant Main as MainWindow
    participant Process as DataProcessWindow
    participant Worker as WorkerThread
    participant Queue
    participant Result as ResultWindow

    User->>+Main: ファイル選択
    Main->>+Process: 作成
    Process->>+Worker: スレッド開始

    Note right of Worker: CSVファイル読み込み
    Worker->>Worker: データ処理・集計
    Worker->>Queue: 処理結果を送信
    Worker->>Queue: close_windowメッセージ送信
    Worker-->>-Process: スレッド終了

    loop 定期チェック
        Main->>Queue: get_nowait()
        alt データ受信
            Queue-->>Main: 処理結果
            Main->>Main: グラフ作成
            Main->>+Result: 結果表示
            Result-->>-Main: 表示完了
        else ウィンドウ終了
            Queue-->>Main: close_window
            Main->>Process: destroy()
            Process-->>-Main: 終了
        end
    end

    Note over Main,Queue: 100ms間隔で<br/>キューをチェック

    User->>Result: グラフ確認
    User->>Result: ウィンドウを閉じる
    Result-->>Main: 終了
```