PythonとTkinterで作るカンバン方式タスク管理アプリ

## はじめに

本記事では、PythonとTkinterを使用して、完全自由移動可能なカンバン方式のタスク管理アプリケーションを作成する方法を解説します。このアプリケーションは、シンプルながら柔軟性の高いタスク管理ツールとして機能し、プログラミング初心者からある程度経験のある方まで、幅広い読者に役立つ内容となっています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/f5731699-94b3-02e9-39f1-e412ae5444b0.png)

## 要件

アプリケーションの主な要件は以下の通りです：

1. カンバン方式のタスク管理（ToDo, Doing, Done の3列）
2. タスクの追加、編集、削除機能
3. タスク間の自由な移動（任意の列間で双方向に移動可能）
4. タスクの色変更機能
5. シンプルで直感的なユーザーインターフェース

```plantuml
@startuml
left to right direction
actor "ユーザー" as User

rectangle "カンバンタスク管理アプリ" {
  usecase "タスクを追加" as UC1
  usecase "タスクを移動" as UC2
  usecase "タスクを編集" as UC3
  usecase "タスクを削除" as UC4
  usecase "タスクリストを表示" as UC5
}

User --> UC1
User --> UC2
User --> UC3
User --> UC4
User --> UC5

note bottom of UC2
  ToDo, Doing, Done
  間での移動
end note

@enduml
```

## 仕様
画面全体
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/be8091c6-5840-c05b-f76b-574f13ebb997.png)

タスクの追加
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/142d52f6-5850-2d60-964f-c33e50f60d8f.png)

タスクの移動
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/79b46ab0-3655-c024-a0e1-d335d6eedd4c.png)

タスクの編集
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/20ff2f94-fae5-2c69-afe9-613d853cf335.png)

タスクの編集(色)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2648069/5ab05f20-9dd2-91fd-36cb-6400ed46a37c.png)

アプリケーションの詳細仕様は以下の通りです：

1. ユーザーインターフェース
   - ウィンドウサイズ: 800x600ピクセル
   - 3つの列（ToDo, Doing, Done）を持つカンバンボード
   - 各列の背景色を区別（ToDo: 薄い赤、Doing: 薄い緑、Done: 薄い青）
   - 新規タスク入力フィールドと追加ボタン

2. タスク管理
   - 新規タスクは常に「ToDo」列に追加
   - 各タスクカードに以下の機能を実装:
     - 左右の列への移動ボタン
     - 編集ボタン
     - 色変更ボタン
     - 削除ボタン

3. タスクの移動
   - 左右の矢印ボタンで隣接する列へ移動可能
   - ToDo ⇔ Doing ⇔ Done の任意の方向に移動可能

4. タスクの編集
   - タスクの説明文を編集可能
   - タスクカードの色をカラーピッカーで自由に変更可能

5. データ管理
   - アプリケーション実行中はメモリ上でタスクを管理
   - （オプション）タスクデータの永続化は今後の拡張機能として検討

## クラス図

```mermaid
classDiagram
    class KanbanBoard {
        -columns: List[str]
        -tasks: Dict[str, List[Tuple[Task, TaskCard]]]
        +__init__()
        +create_widgets()
        +create_header()
        +create_board()
        +create_task_entry()
        +add_task()
        +create_task_card(column: str, task: Task)
        +move_task(task: Task, from_column: str, direction: int)
        +delete_task(task: Task, column: str)
        +edit_task(task: Task, new_description: str)
        +update_all_task_cards()
    }

    class TaskCard {
        -task: Task
        -delete_callback: function
        -move_callback: function
        -edit_callback: function
        +__init__(master, task, delete_callback, move_callback, edit_callback)
        +move_left()
        +move_right()
        +delete()
        +edit_task()
        +change_color()
        +update_move_buttons()
    }

    class Task {
        +description: str
        +status: str
        +color: str
        +__init__(description: str, status: str, color: str)
        +random_color(): str
    }

    KanbanBoard "1" *-- "0..*" TaskCard : contains
    KanbanBoard "1" *-- "0..*" Task : manages
    TaskCard "1" --> "1" Task : represents
```

## シーケンス図
```mermaid
sequenceDiagram
    actor User
    participant UI as ユーザーインターフェース
    participant KanbanBoard as KanbanBoardクラス
    participant TaskCard as TaskCardクラス
    participant Task as Taskクラス

    User->>UI: アプリケーション起動
    UI->>KanbanBoard: 初期化
    KanbanBoard->>UI: ボードを表示

    Note over User,UI: タスクの追加
    User->>UI: タスク説明を入力
    User->>UI: 「タスクを追加」ボタンをクリック
    UI->>KanbanBoard: add_task()
    KanbanBoard->>Task: 新しいTaskを作成
    KanbanBoard->>TaskCard: 新しいTaskCardを作成
    KanbanBoard->>UI: タスクカードを「ToDo」列に表示

    Note over User,UI: タスクの移動
    User->>UI: タスクカードの移動ボタンをクリック
    UI->>TaskCard: move_left() or move_right()
    TaskCard->>KanbanBoard: move_task()
    KanbanBoard->>TaskCard: タスクカードを削除
    KanbanBoard->>TaskCard: 新しい列にタスクカードを作成
    KanbanBoard->>UI: 更新されたボードを表示

    Note over User,UI: タスクの編集
    User->>UI: タスクカードの「編集」ボタンをクリック
    UI->>TaskCard: edit_task()
    TaskCard->>UI: 編集ダイアログを表示
    User->>UI: 新しい説明を入力
    UI->>TaskCard: 新しい説明を設定
    TaskCard->>KanbanBoard: edit_task()
    KanbanBoard->>UI: 更新されたタスクカードを表示

    Note over User,UI: タスクの色変更
    User->>UI: タスクカードの「色」ボタンをクリック
    UI->>TaskCard: change_color()
    TaskCard->>UI: カラーピッカーを表示
    User->>UI: 新しい色を選択
    UI->>TaskCard: 新しい色を設定
    TaskCard->>UI: 更新されたタスクカードを表示

    Note over User,UI: タスクの削除
    User->>UI: タスクカードの「×」ボタンをクリック
    UI->>TaskCard: delete()
    TaskCard->>KanbanBoard: delete_task()
    KanbanBoard->>UI: タスクカードを削除
    KanbanBoard->>UI: 更新されたボードを表示
```

## 実装

それでは、上記の要件と仕様に基づいて、アプリケーションの実装を行っていきます。

### 1. 必要なモジュールのインポート

まず、必要なモジュールをインポートします。

```python
import tkinter as tk
from tkinter import ttk, messagebox, colorchooser, simpledialog
import random
```

### 2. タスククラスの定義

個々のタスクを表現するクラスを定義します。

```python
class Task:
    def __init__(self, description, status="ToDo", color=None):
        self.description = description
        self.status = status
        self.color = color or self.random_color()

    def random_color(self):
        return "#{:06x}".format(random.randint(0x808080, 0xFFFFFF))
```

### 3. タスクカードクラスの定義

タスクカードのUIと機能を実装するクラスを定義します。

```python
class TaskCard(tk.Frame):
    def __init__(self, master, task, delete_callback, move_callback, edit_callback, **kwargs):
        super().__init__(master, bg=task.color, bd=1, relief=tk.RAISED, **kwargs)
        self.task = task
        self.delete_callback = delete_callback
        self.move_callback = move_callback
        self.edit_callback = edit_callback

        self.description = tk.Label(self, text=task.description, bg=task.color, wraplength=150)
        self.description.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)

        button_frame = tk.Frame(self, bg=task.color)
        button_frame.pack(fill=tk.X, padx=2, pady=2)

        self.left_button = ttk.Button(button_frame, text="←", width=2, command=self.move_left)
        self.left_button.pack(side=tk.LEFT, padx=(0, 2))

        self.right_button = ttk.Button(button_frame, text="→", width=2, command=self.move_right)
        self.right_button.pack(side=tk.LEFT, padx=(0, 2))

        edit_button = ttk.Button(button_frame, text="編集", width=4, command=self.edit_task)
        edit_button.pack(side=tk.LEFT, padx=(0, 2))

        color_button = ttk.Button(button_frame, text="色", width=3, command=self.change_color)
        color_button.pack(side=tk.LEFT, padx=(0, 2))

        delete = ttk.Button(button_frame, text="×", width=2, command=self.delete)
        delete.pack(side=tk.RIGHT)

        self.update_move_buttons()

    def move_left(self):
        self.move_callback(self.task, self.task.status, -1)

    def move_right(self):
        self.move_callback(self.task, self.task.status, 1)

    def delete(self):
        self.delete_callback(self.task, self.task.status)

    def edit_task(self):
        new_description = simpledialog.askstring("タスクの編集", "新しい説明を入力してください:", initialvalue=self.task.description)
        if new_description:
            self.edit_callback(self.task, new_description)
            self.description.config(text=new_description)

    def change_color(self):
        new_color = colorchooser.askcolor(title="タスクの色を選択")[1]
        if new_color:
            self.task.color = new_color
            self.config(bg=new_color)
            self.description.config(bg=new_color)
            for widget in self.winfo_children():
                if isinstance(widget, tk.Frame):
                    widget.config(bg=new_color)

    def update_move_buttons(self):
        order = ["ToDo", "Doing", "Done"]
        current_index = order.index(self.task.status)
        self.left_button.config(state="normal" if current_index > 0 else "disabled")
        self.right_button.config(state="normal" if current_index < len(order) - 1 else "disabled")
```

### 4. カンバンボードクラスの定義

アプリケーションのメインウィンドウとカンバンボードの機能を実装するクラスを定義します。

```python
class KanbanBoard(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title("タスク管理アプリ")
        self.geometry("800x600")
        self.configure(bg="#f0f0f0")

        self.columns = ["ToDo", "Doing", "Done"]
        self.tasks = {column: [] for column in self.columns}

        self.create_widgets()

    def create_widgets(self):
        self.create_header()
        self.create_board()
        self.create_task_entry()

    def create_header(self):
        header = tk.Frame(self, bg="#4a4a4a")
        header.pack(fill=tk.X, padx=10, pady=10)

        title = tk.Label(header, text="タスク管理アプリ", font=("Helvetica", 18, "bold"), bg="#4a4a4a", fg="white")
        title.pack(side=tk.LEFT, padx=10)

    def create_board(self):
        board = tk.Frame(self)
        board.pack(expand=True, fill=tk.BOTH, padx=10, pady=10)

        colors = {"ToDo": "#ffcccc", "Doing": "#ccffcc", "Done": "#ccccff"}

        for i, column in enumerate(self.columns):
            column_frame = tk.Frame(board, bg=colors[column], bd=2, relief=tk.RAISED)
            column_frame.grid(row=0, column=i, padx=5, pady=5, sticky="nsew")

            column_label = tk.Label(column_frame, text=column, font=("Helvetica", 14, "bold"), bg=colors[column])
            column_label.pack(pady=10)

            task_frame = tk.Frame(column_frame, bg=colors[column])
            task_frame.pack(expand=True, fill=tk.BOTH, padx=5, pady=5)

            self.tasks[column] = []
            
            board.columnconfigure(i, weight=1)
        board.rowconfigure(0, weight=1)

    def create_task_entry(self):
        entry_frame = tk.Frame(self, bg="#f0f0f0")
        entry_frame.pack(fill=tk.X, padx=10, pady=10)

        self.task_entry = ttk.Entry(entry_frame, font=("Helvetica", 12), width=40)
        self.task_entry.pack(side=tk.LEFT, padx=(0, 10))

        add_button = ttk.Button(entry_frame, text="タスクを追加", command=self.add_task)
        add_button.pack(side=tk.LEFT)

    def add_task(self):
        description = self.task_entry.get().strip()
        if description:
            task = Task(description)
            self.create_task_card("ToDo", task)
            self.task_entry.delete(0, tk.END)
        else:
            messagebox.showwarning("警告", "タスクの説明を入力してください。")

    def create_task_card(self, column, task):
        column_frame = self.winfo_children()[1].grid_slaves(row=0, column=self.columns.index(column))[0]
        task_frame = column_frame.winfo_children()[1]

        card = TaskCard(task_frame, task, delete_callback=self.delete_task, 
                        move_callback=self.move_task, edit_callback=self.edit_task)
        card.pack(fill=tk.X, padx=5, pady=5)

        task.status = column
        self.tasks[column].append((task, card))

    def move_task(self, task, from_column, direction):
        current_index = self.columns.index(from_column)
        new_index = current_index + direction
        if 0 <= new_index < len(self.columns):
            to_column = self.columns[new_index]
            self.delete_task(task, from_column)
            self.create_task_card(to_column, task)
            task.status = to_column  # タスクのステータスを更新
            self.update_all_task_cards()  # すべてのタスクカードを更新

    def delete_task(self, task, column):
        for t, card in self.tasks[column]:
            if t == task:
                card.destroy()
                self.tasks[column].remove((t, card))
                break

    def edit_task(self, task, new_description):
        task.description = new_description
        # タスクカードの表示を更新
        for column in self.tasks:
            for t, card in self.tasks[column]:
                if t == task:
                    card.description.config(text=new_description)
                    break

    def update_all_task_cards(self):
        for column in self.tasks:
            for task, card in self.tasks[column]:
                card.update_move_buttons()

if __name__ == "__main__":
    app = KanbanBoard()
    app.mainloop()
```

## 実行方法

上記のコードを `kanban_task_manager.py` として保存し、以下のコマンドで実行します：

```
python kanban_task_manager.py
```

## 機能説明

1. タスクの追加: 下部の入力フィールドにタスクの説明を入力し、「タスクを追加」ボタンをクリックします。
2. タスクの移動: タスクカード上の「←」「→」ボタンで左右の列に移動できます。
3. タスクの編集: 「編集」ボタンをクリックし、新しい説明を入力します。
4. タスクの色変更: 「色」ボタンをクリックし、カラーピッカーから新しい色を選択します。
5. タスクの削除: 「×」ボタンをクリックしてタスクを削除します。

## まとめ

このアプリケーションでは、PythonとTkinterを使用して、カンバン方式のタスク管理ツールを実装しました。完全自由移動可能なタスク、カラフルなタスクカード、シンプルで直感的なUIにより、効率的なタスク管理が可能です。

## 発展的な機能追加のアイデア

1. データの永続化（ファイルやデータベースへの保存）
2. タスクの優先度設定
3. 締め切り日の追加
4. 複数のプロジェクトやボードの管理
5. ドラッグ＆ドロップによるタスク移動

これらの機能を追加することで、より高度なタスク管理アプリケーションを作成できます。

ぜひ、このコードを基に自分なりのカスタマイズを加えて、理想のタスク管理ツールを作ってみてください！