PythonでシンプルなToDoアプリを作った

Python の 学習の一環でシンプルな ToDo アプリを作りました。
ボタンや入力欄があると直感的で使いやすいので、GUIライブラリ「tkinter」を使ってみました。

## 開発環境

- Python: 3.11
- GUIライブラリ: tkinter（標準）
- カレンダー入力: tkcalendar（別途インストール）
- 動作確認: Windows 10


## アプリの主な機能

- タスクの登録（期日付き）
- タスクの削除
- タスクの完了/未完了切り替え
- タスクの一覧表示
- JSONファイルに保存してアプリを閉じても自動復元

## 完成画面
![キャプチャ.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4095361/0fac2b84-bffc-40b1-860c-f12d587aa5c2.png)


## 主な機能の説明
### `add_task()` : タスクの追加処理
```
# タスクを追加する
def add_task(self):
    task = self.entry.get().strip()
    due = self.due_entry.get().strip()
    if task:
        self.tasks.append({"task": task, "done": False, "due": due})
        self.entry.delete(0, tk.END)
        self.update_listbox()
```
- `self.entry.get()` で入力欄からタスク内容を取得
- `self.due_entry.get()` で選択した期日を取得
- タスク内容が空でなければ
  - 辞書形式でリストに追加
  - タスクは `done: False`（未完了）で初期化
  - 入力欄をクリアし、`update_listbox()` で表示を更新

### `delete_task()` : タスクの削除処理
```
# タスクを削除する
def delete_task(self):
    index = self.listbox.curselection()
    if index:
        del self.tasks[index[0]]
        self.update_listbox()
```
- `self.listbox.curselection()` で選択中のインデックスを取得
- 選択されていれば、そのインデックスにあるタスクを削除
- タスク一覧の表示を `update_listbox()` で更新


### `mark_done()` : 完了済み / 未完了 の切り替え処理
```
# タスクを完了済/未完了にする
def mark_done(self):
    index = self.listbox.curselection()
    # タスクを選んでいない場合はなにもしない（indexエラー対策）
    if not index:
        return
    
    # タスクを完了済みにする
    if self.tasks[index[0]]["done"] == False:
        self.tasks[index[0]]["done"] = True
        self.update_listbox()
    # タスクを未完了にする
    else:
        self.tasks[index[0]]["done"] = False
        self.update_listbox()
```
- 現在選択中のタスクがあるか確認（なければ `return` で何もしないよう処理する）
- `done` フラグを `True ⇄ False` でトグル切り替え
- 変更後にリストボックスを再描画

### タスクの一覧表示
```
def update_listbox(self):
    self.listbox.delete(0, tk.END)
    for t in self.tasks:
        mark = "✔" if t["done"] else "✖"
        due = f" (期日: {t['due']})" if t.get("due") else ""
        self.listbox.insert(tk.END, f"[{mark}] {t['task']}{due}")
```
- `self.listbox.delete` でリストボックスに表示されているタスクを一度すべて削除する(リストの初期化)
- `self.tasks` に登録されているタスクを1件ずつ処理する
  - 各タスクについて、完了済みなら「✔」、未完了なら「✖」のマークをつける
  - 期日が設定されている場合は (期日: `YYYY-MM-DD`) を表示に含める
  - 上記を組み合わせた1行の文字列をリストボックスに追加する


### JSONファイルに保存してアプリを閉じても自動復元
```
# ウインドウを閉じる
def on_close(self):
    save_tasks(self.tasks)
    self.root.destroy()

# タスクを保存する
def save_tasks(tasks):
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(tasks, f, indent=2, ensure_ascii=False)
```
- ウィンドウの「×」ボタン（閉じる）を押したときに呼び出す
- `save_tasks(self.tasks)` により、現在のタスクを保存
- 最後に `self.root.destroy()` を実行して、アプリケーションを終了

- 引数 `tasks` に渡されたタスクリストを、JSON形式でファイルに保存
- `indent=2` により、インデントを整形済みで書き出し
- `ensure_ascii=False` を指定することで、日本語も文字化けせずに保存可能

### 出力ファイル例
```
[
  {
    "task": "昼ご飯を食べる",
    "done": true,
    "due": "2025-05-29"
  },
  {
    "task": "資格勉強する",
    "done": true,
    "due": "2025-05-29"
  },
  {
    "task": "遊ぶ",
    "done": false,
    "due": "2025-05-29"
  },
  {
    "task": "夕飯を食べる",
    "done": false,
    "due": "2025-05-29"
  }
]
```

## GUI の設定
### 使用している主なウィジェット

| ウィジェット         | 用途                            |
|----------------------|---------------------------------|
| `Label`              | 説明文の表示                     |
| `Entry`              | タスクのテキスト入力欄           |
| `DateEntry`          | カレンダー形式の期日入力欄       |
| `Button`             | 各種操作（追加・削除・切替）     |
| `Listbox`            | 登録済タスクの一覧表示           |

### GUIの初期化部分（`__init__`）

```
def __init__(self, root):
    self.root = root
    self.root.title("ToDoアプリ")
    self.tasks = load_tasks()
```
- `root`：アプリケーションのウィンドウ本体
- `self.root.title(...)`：ウィンドウのタイトルを設定
- `self.tasks = load_tasks()`：保存されたタスクを読み込み

### タスク入力欄
```
tk.Label(root, text="タスク:").pack(anchor="w", padx=10)
self.entry = tk.Entry(root, width=40)
self.entry.pack(pady=5)
```
- `Label`：タスク入力欄の見出し
- `Entry`：タスクを入力するテキストボックス

### 期日入力欄
```
tk.Label(root, text="期日を入力 (例: 2025-04-30):").pack(anchor="w", padx=10)
self.due_entry = DateEntry(root, width=20, date_pattern='yyyy-mm-dd',
    year=date.today().year, month=date.today().month, day=date.today().day)
self.due_entry.pack(pady=5)
```
- `DateEntry（tkcalendar）`：カレンダー付きの入力欄
- `date_pattern='yyyy-mm-dd'`：年月日を指定フォーマットで表示

### 操作ボタン
```
self.add_button = tk.Button(root, text="タスク追加", command=self.add_task)
self.done_button = tk.Button(root, text="完了済/未完了", command=self.mark_done)
self.delete_button = tk.Button(root, text="削除", command=self.delete_task)
```
- 各ボタンは押下時に対応する関数（コールバック関数）を実行
  - `add_task()`：タスクを追加
  - `mark_done()`：完了／未完了の切替
  - `delete_task()`：タスク削除

### タスクリスト表示欄
```
self.listbox = tk.Listbox(root, width=50, selectmode=tk.SINGLE)
```
- `Listbox`：登録されたタスクを1行ずつ表示

### 閉じる処理の登録
```
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
```
- ウィンドウの「×」ボタンを押したときに `on_close()` を呼び出す



## 最終的な全体コード
```
import tkinter as tk
from tkinter import messagebox
from tkcalendar import DateEntry
from datetime import date
import json
import os

FILENAME = "tasks.json"

# 保存してあるタスクを読み込む
def load_tasks():
    if os.path.exists(FILENAME):
        with open(FILENAME, "r", encoding="utf-8") as f:
            return json.load(f)
    else:
        return []

# タスクを保存する
def save_tasks(tasks):
    with open(FILENAME, "w", encoding="utf-8") as f:
        json.dump(tasks, f, indent=2, ensure_ascii=False)


class TodoApp:
    def __init__(self, root):
        self.root = root
        self.root.title("ToDoアプリ")
        self.tasks = load_tasks()

        tk.Label(root, text="タスク:").pack(anchor="w", padx=10)
        self.entry = tk.Entry(root, width=40)
        self.entry.pack(pady=5)

        tk.Label(root, text="期日を入力 (例: 2025-04-30):").pack(anchor="w", padx=10)
        self.due_entry = DateEntry(root, width=20, date_pattern='yyyy-mm-dd', year=date.today().year, month=date.today().month, day=date.today().day)
        self.due_entry.pack(pady=5)

        self.add_button = tk.Button(root, text="タスク追加", command=self.add_task)
        self.add_button.pack(padx=5)

        self.listbox = tk.Listbox(root, width=50, selectmode=tk.SINGLE)
        self.listbox.pack(pady=5)

        self.done_button = tk.Button(root, text="完了済/未完了", command=self.mark_done)
        self.done_button.pack(pady=5)

        self.delete_button = tk.Button(root, text="削除", command=self.delete_task)
        self.delete_button.pack(pady=5)

        self.update_listbox()

        self.root.protocol("WM_DELETE_WINDOW", self.on_close)

    # タスクリストを表示させている
    def update_listbox(self):
        self.listbox.delete(0, tk.END)
        for t in self.tasks:
            mark = "✔" if t["done"] else "✖"
            due = f" (期日: {t['due']})" if t.get("due") else ""
            self.listbox.insert(tk.END, f"[{mark}] {t['task']}{due}")

    # タスクを追加する
    def add_task(self):
        task = self.entry.get().strip()
        due = self.due_entry.get().strip()
        if task:
            self.tasks.append({"task": task, "done": False, "due": due})
            self.entry.delete(0, tk.END)
            self.update_listbox()
    
    # タスクを削除する
    def delete_task(self):
        index = self.listbox.curselection()
        if index:
            del self.tasks[index[0]]
            self.update_listbox()
    
    # タスクを完了済/未完了にする
    def mark_done(self):
        index = self.listbox.curselection()
        # タスクを選んでいない場合はなにもしない（indexエラー対策）
        if not index:
            return
        
        # タスクを完了済みにする
        if self.tasks[index[0]]["done"] == False:
            self.tasks[index[0]]["done"] = True
            self.update_listbox()
        # タスクを未完了にする
        else:
            self.tasks[index[0]]["done"] = False
            self.update_listbox()

    # ウインドウを閉じる
    def on_close(self):
        save_tasks(self.tasks)
        self.root.destroy()

# アプリ起動時にmain()を実行させる記述
if __name__=="__main__":
    root = tk.Tk()
    app = TodoApp(root)
    root.mainloop()
```

## まとめ
今回の ToDo アプリ作成を通して、Python の基本であるリスト操作、関数化、JSON ファイルの入出力について実践的に学ぶことができました。また、GUI ライブラリ（tkinter）を使うことで、コマンドラインではなく、視覚的にわかりやすい画面を気軽に作れる点もとても魅力的だと感じました。