Python

【完全版】Python×Tkinterで作る「TODO+ポモドーロタイマー」アプリ入門|初心者でも生産性アプリが作れる!(App003)

tyamada

Pythonはスクリプト言語として有名ですが、実はデスクトップアプリもかなり簡単に作れます。
特に Tkinter は標準ライブラリとして最初から同梱されているため、

  • 追加インストール不要
  • 学習しやすい
  • 小規模アプリに最適

という理由から「最初に触れるGUI」として最もおすすめです。

この記事では、初心者でも実用レベルで使えるTODOリスト と ポモドーロタイマー を合わせた生産性アプリを作りながら、コードの構造を丁寧に読み解くこと を目的としています。

完成形は以下のようになります:

完成イメージ


画面は左右に分かれ、

  • 左側:ポモドーロタイマー(25分作業 / 5分休憩)
  • 右側:TODOリスト
    • 追加 / 編集 / 削除
    • 「作業中」フラグを付ける
  • 進捗バーで残り時間を可視化
  • JSONファイルへ自動保存

と、日常的に使える仕様になっています。


【完成コードを全文掲載】まずは動かす

※行ごとの詳細解説はこの後で行います。
まずは「完成形を貼り、読者が動かせる」ことが大切です。

このコードは 1ファイル(todo_pomodoro.py) として保存すれば動きます。

ソースコードを全て表示
import tkinter as tk
from tkinter import ttk, messagebox
import json
import os

TASK_FILE = "tasks.json"
WORK_MIN = 25
BREAK_MIN = 5


class PomodoroApp:
    def __init__(self, root):
        self.root = root
        self.root.title("TODO + Pomodoro Productivity App")

        # 状態変数
        self.remaining = 0
        self.is_running = False
        self.is_break = False
        self.timer_id = None
        self.tasks = []

        # レイアウト構築
        self.build_ui()
        self.load_tasks()

    # -----------------------------
    # UI構築
    # -----------------------------
    def build_ui(self):
        main_frame = ttk.Frame(self.root)
        main_frame.pack(padx=10, pady=10, fill="both", expand=True)

        # 左側:タイマー
        timer_frame = ttk.LabelFrame(main_frame, text="Timer")
        timer_frame.grid(row=0, column=0, padx=10, pady=10, sticky="ns")

        self.timer_label = ttk.Label(timer_frame, text="00:00", font=("Helvetica", 28))
        self.timer_label.pack(pady=10)

        self.progress = ttk.Progressbar(timer_frame, length=200, mode="determinate")
        self.progress.pack(pady=10)

        btn_frame = ttk.Frame(timer_frame)
        btn_frame.pack(pady=5)

        ttk.Button(btn_frame, text="Start", command=self.start_timer).grid(row=0, column=0, padx=5)
        ttk.Button(btn_frame, text="Stop", command=self.stop_timer).grid(row=0, column=1, padx=5)
        ttk.Button(btn_frame, text="Reset", command=self.reset_timer).grid(row=0, column=2, padx=5)

        # 右側:タスク
        task_frame = ttk.LabelFrame(main_frame, text="Tasks")
        task_frame.grid(row=0, column=1, padx=10, pady=10, sticky="nsew")

        self.task_list = tk.Listbox(task_frame, width=40, height=15)
        self.task_list.pack(pady=5)

        task_btn_frame = ttk.Frame(task_frame)
        task_btn_frame.pack()

        ttk.Button(task_btn_frame, text="Add", command=self.add_task).grid(row=0, column=0, padx=5)
        ttk.Button(task_btn_frame, text="Edit", command=self.edit_task).grid(row=0, column=1, padx=5)
        ttk.Button(task_btn_frame, text="Delete", command=self.delete_task).grid(row=0, column=2, padx=5)
        ttk.Button(task_btn_frame, text="Set Working", command=self.set_working_task).grid(row=0, column=3, padx=5)

        main_frame.columnconfigure(1, weight=1)

    # -----------------------------
    # タスク操作
    # -----------------------------
    def add_task(self):
        win = tk.Toplevel(self.root)
        win.title("Add Task")

        tk.Label(win, text="Task:").pack()
        entry = tk.Entry(win, width=40)
        entry.pack()

        def save():
            title = entry.get().strip()
            if title:
                self.tasks.append({"title": title, "working": False})
                self.update_task_list()
                self.save_tasks()
                win.destroy()

        ttk.Button(win, text="Save", command=save).pack()

    def edit_task(self):
        idx = self.get_selected_index()
        if idx is None:
            return

        win = tk.Toplevel(self.root)
        win.title("Edit Task")

        tk.Label(win, text="Task:").pack()
        entry = tk.Entry(win, width=40)
        entry.insert(0, self.tasks[idx]["title"])
        entry.pack()

        def save():
            title = entry.get().strip()
            if title:
                self.tasks[idx]["title"] = title
                self.update_task_list()
                self.save_tasks()
                win.destroy()

        ttk.Button(win, text="Save", command=save).pack()

    def delete_task(self):
        idx = self.get_selected_index()
        if idx is None:
            return

        del self.tasks[idx]
        self.update_task_list()
        self.save_tasks()

    def set_working_task(self):
        idx = self.get_selected_index()
        if idx is None:
            return

        for t in self.tasks:
            t["working"] = False
        self.tasks[idx]["working"] = True

        self.update_task_list()
        self.save_tasks()

    # -----------------------------
    # タイマー
    # -----------------------------
    def start_timer(self):
        if not self.is_running:
            self.is_running = True
            minutes = WORK_MIN if not self.is_break else BREAK_MIN
            self.remaining = minutes * 60
            self.progress.configure(maximum=self.remaining)
            self.countdown()

    def stop_timer(self):
        if self.is_running:
            self.is_running = False
            if self.timer_id:
                self.root.after_cancel(self.timer_id)
                self.timer_id = None

    def reset_timer(self):
        self.stop_timer()
        self.remaining = 0
        self.timer_label.config(text="00:00")
        self.progress["value"] = 0
        self.is_break = False

    def countdown(self):
        if not self.is_running:
            return

        mins, secs = divmod(self.remaining, 60)
        self.timer_label.config(text=f"{mins:02d}:{secs:02d}")
        self.progress["value"] = self.progress["maximum"] - self.remaining

        if self.remaining > 0:
            self.remaining -= 1
            self.timer_id = self.root.after(1000, self.countdown)
        else:
            # 作業 → 休憩 → 作業 の切替
            self.is_break = not self.is_break
            self.is_running = False
            self.start_timer()

    # -----------------------------
    # タスク保存
    # -----------------------------
    def update_task_list(self):
        self.task_list.delete(0, tk.END)
        for t in self.tasks:
            flag = "🟢 " if t["working"] else ""
            self.task_list.insert(tk.END, flag + t["title"])

    def save_tasks(self):
        with open(TASK_FILE, "w", encoding="utf-8") as f:
            json.dump(self.tasks, f, indent=2)

    def load_tasks(self):
        if os.path.exists(TASK_FILE):
            try:
                with open(TASK_FILE, "r", encoding="utf-8") as f:
                    self.tasks = json.load(f)
                self.update_task_list()
            except:
                messagebox.showerror("Error", "Failed to load task file.")

    # -----------------------------
    # ユーティリティ
    # -----------------------------
    def get_selected_index(self):
        try:
            return self.task_list.curselection()[0]
        except:
            return None


if __name__ == "__main__":
    root = tk.Tk()
    app = PomodoroApp(root)
    root.mainloop()

【詳細解説】

ここから、完成コードの「最初の30行ほど」を行番号つきで丁寧に解説していきます。


■ インポート部(1–4行目)

1  import tkinter as tk
2  from tkinter import ttk, messagebox
3  import json
4  import os

1行目:import tkinter as tk

Tkinterの基本機能(Frame、Label、Buttonなど)を tk.◯◯ として呼び出すためのインポート。
as tk を付けることで、冗長な tkinter.Button を書かずに済む。

2行目:from tkinter import ttk, messagebox

  • ttk:より近代的デザインのウィジェット(Button, Frame, Progressbarなど)
  • messagebox:エラーダイアログ等のポップアップ用

UIは ttk を主役にしたほうが見た目が綺麗。

3行目:import json

タスクの保存・読み込み(永続化)に JSON を使うため必須。

4行目:import os

os.path.exists() でタスクファイルの存在チェックを行うため。


■ 定数定義(6–8行目)

6  TASK_FILE = "tasks.json"
7  WORK_MIN = 25
8  BREAK_MIN = 5

6行目:TASK_FILE

タスクを保存するファイル名。
後述の save_tasks()load_tasks() が参照する。

7–8行目:WORK_MIN, BREAK_MIN

ポモドーロ方式の基準値(25分作業、5分休憩)。
ここを変更するだけでタイマー仕様が変えられる柔軟な設計。

第3章:ソースコードの詳細解説(前半)

ここからは、実際の動くソースコードを引用しながら、
各部分が「何をしているのか?」を丁寧に解説していきます。

Python初心者でも読めるよう、専門用語はできるだけ避け、
プログラムの流れが自然に理解できるように構成しています。


3-1. 全体構造の理解

今回のアプリは、以下のような3つの柱で構成されています。


① クラス構造:TodoPomodoroApp

本アプリは class TodoPomodoroApp: というクラスに全ての処理をまとめています。
これには次のメリットがあります。

  • GUIアプリは「ボタン」「タイマー」「リスト」など部品が多い
  • 関数を大量に作るだけだと整理しにくい
  • 変数の管理が複雑になる
  • クラス化することで整理された構造になる

GUIアプリはクラスで書くのが定番スタイル だと覚えておくとよいでしょう。


② GUI:Tkinterによる画面生成

画面は大きく2つのフレームに分けています。

左側:ポモドーロタイマー
右側:TODOリスト(追加・編集・削除)

この2分割レイアウトは GUI アプリでは非常によく使う手法です。

Tkinter では以下のように簡単に実現できます。

self.left = ttk.Frame(self.master, padding=10)
self.left.grid(row=0, column=0, sticky="nswe")

self.right = ttk.Frame(self.master, padding=10)
self.right.grid(row=0, column=1, sticky="nswe")

grid() を使うことで、Excel のような表形式に配置できるのがポイントです。


③ ロジック:タイマー制御 & JSON保存

アプリが持つロジックは次の2つ。

  • 25分作業 → 5分休憩のポモドーロタイマー
  • タスクを JSON ファイルに保存して永続化

これらは GUI と完全に独立しており、
画面表示とは分けて「処理の仕様」を実装しています。


3-2. 必要なライブラリのインポート

まずアプリ最上部のコードを見ます。

import json
import time
import tkinter as tk
from tkinter import ttk, messagebox, simpledialog

● json

タスクの保存・読み込みに使用します。

tasks.json → アプリ終了後も残るデータ

● time

time.strftime() を使ってタイマーの残り時間を「00:00」形式に整形します。


● tkinter / ttk

Tkinter は Python の標準 GUI ライブラリ。
ttk は「新しいタイプのウィジェット」を提供し、デザインが綺麗になります。


3-3. メインクラスの初期設定

引用:

class TodoPomodoroApp:
    def __init__(self, master):
        self.master = master
        self.master.title("TODO + Pomodoro")
        self.master.geometry("780x480")
        
        # 状態変数
        self.is_running = False
        self.remaining = 0
        self.current_task = None

● ウィンドウの基本設定

  • タイトル:TODO + Pomodoro
  • サイズ:780×480の固定ウィンドウ

初心者は「画面がどこから始まるのか」を掴むため、
まずここでウィンドウを確定します。


● 状態変数について

アプリの心臓部となる変数がここでまとめられています。

変数名役割
is_runningタイマーが動いているか判定
remaining残り秒数
current_task作業中フラグの付いたタスク名

GUIアプリは状態管理がとても大切なので、
このような「初期値の宣言」は非常に重要です。


3-4. レイアウト(画面構築)

引用:

self.left = ttk.Frame(self.master, padding=10)
self.left.grid(row=0, column=0, sticky="nswe")

self.right = ttk.Frame(self.master, padding=10)
self.right.grid(row=0, column=1, sticky="nswe")

● Frameとは?

Tkinter で画面を構成するための「枠」です。

GUIでは、

  • ウィンドウ
    • その中のフレーム
      • さらにその中にボタンやラベル

という階層で画面が出来ています。


● sticky=”nswe” とは?

上下左右いっぱいに広げる設定です。

  • n: 上(north)
  • s: 下(south)
  • w: 左(west)
  • e: 右(east)

これをセットにすると

そのマス目を全部使う(広がる)

という意味になります。


3-5. タイマー部分のウィジェット

引用:

self.timer_label = ttk.Label(self.left, text="25:00", font=("Arial", 36))
self.timer_label.pack(pady=20)

self.progress = ttk.Progressbar(self.left, length=250, mode="determinate")
self.progress.pack(pady=10)

● 大型の時間表示

25分タイマーは視認性が命です。
大きめのフォント(36px)で中央に表示しています。


● プログレスバー

残り時間を視覚的に見せるために Progressbar を使います。

  • 最大値:タイマー開始時に設定
  • 現在値:経過時間で更新

Tkinter の中でも学習しがいのあるウィジェットです。


3-6. タスク管理(TODOリスト)

引用:

self.task_list = tk.Listbox(self.right, height=12)
self.task_list.pack(fill="both", expand=True, pady=5)

self.entry = ttk.Entry(self.right)
self.entry.pack(fill="x", pady=5)

● Listboxとは?

Tkinter の最も基本的なリスト表示。

  • タスクの一覧
  • 選択・削除
  • ダブルクリック編集

など、文字列リストを扱う UI として使いやすい部品です。


● Entryとは?

タスク入力欄です。
単一行テキストを扱うシンプルな入力部品。

(画像:タスク入力欄のスクリーンショットを挿入)


3-7. ボタン類の表示

引用:

ttk.Button(self.right, text="追加", command=self.add_task).pack(fill="x", pady=3)
ttk.Button(self.right, text="編集", command=self.edit_task).pack(fill="x", pady=3)
ttk.Button(self.right, text="削除", command=self.delete_task).pack(fill="x", pady=3)

● command 引数とは?

ボタンが押されたときに実行される関数を指定します。

たとえば「追加」を押すと add_task() が呼ばれます。

GUI では
イベントドリブン(押されたら動く)
という仕組みが基本になります。


3-8. JSON読み込み処理

引用:

self.load_tasks()

初期化の最後に、保存しておいたタスクを読み込みます。

これによって

アプリを閉じてもタスクが消えない

という「普通のアプリとして当然の機能」を実現できます。

第4章:TODO機能の実装(add / edit / delete)

4-1. タスク追加機能(add_task)

まずはもっとも基本となる「追加」機能から見ます。

● 追加機能のコード(引用)

def add_task(self):
    task = self.entry.get().strip()
    if task:
        self.task_list.insert(tk.END, task)
        self.entry.delete(0, tk.END)
        self.save_tasks()

● ロジックの流れを解説

  1. Entryから文字を取得 task = self.entry.get().strip() strip() は前後の空白を取り除く関数です。
  2. 空欄でなければ追加 if task: self.task_list.insert(tk.END, task)
  3. Entryをクリア self.entry.delete(0, tk.END)
  4. JSONへ保存 self.save_tasks()

● GUIアプリで重要なポイント

GUIは「押されたら動く」コードなので、
処理は非常にシンプルでも毎回呼ばれます。

この考え方を押さえると、
多くのTkinterアプリが読みやすくなります。


4-2. タスク編集機能(edit_task)

Listboxで選択されたタスクを編集します。

● コード(引用)

def edit_task(self):
    try:
        index = self.task_list.curselection()[0]
    except:
        messagebox.showwarning("選択なし", "編集するタスクを選択してください")
        return

    current_text = self.task_list.get(index)
    new_text = simpledialog.askstring("編集", "新しい内容:", initialvalue=current_text)

    if new_text:
        self.task_list.delete(index)
        self.task_list.insert(index, new_text)
        self.save_tasks()

● 解説ポイント

① 選択中のインデックスを取得

index = self.task_list.curselection()[0]

Listboxでは複数選択も可能なので、
curselection() は「複数のインデックス」を返します。

ここでは先頭だけ使用。


② simpledialog で編集ウィンドウを開く

new_text = simpledialog.askstring("編集", "新しい内容:", initialvalue=current_text)

Tkinterには、確認ダイアログ以外に
シンプルな文字入力ダイアログ を提供する simpledialog があります。

これで「編集用ポップアップ」を作っています。

(画像:編集ダイアログのスクリーンショットを挿入)


③ Listboxの項目を更新

Listboxには「直接の上書き」がありません。
そのため

  1. 元の項目を削除
  2. 同じ位置に再度追加

という処理を行います。


4-3. タスク削除機能(delete_task)

引用:

def delete_task(self):
    try:
        index = self.task_list.curselection()[0]
    except:
        messagebox.showwarning("選択なし", "削除するタスクを選択してください")
        return

    self.task_list.delete(index)
    self.save_tasks()

● 解説

編集機能とほぼ同じ構造ですが、実装はさらにシンプル。

  • 選択されていなければエラーを表示
  • 選択されていれば削除
  • 保存する

これだけです。

GUIアプリでは、
「何を選択しているか?」を扱うコードが非常に多くなります。


4-4. 作業中フラグの切り替え(set_current_task)

タイマーをスタートした瞬間、
「どのタスクを今やっているか?」 を記録します。

引用:

def select_task(self):
    try:
        index = self.task_list.curselection()[0]
        self.current_task = self.task_list.get(index)
        messagebox.showinfo("作業中", f"『{self.current_task}』を作業中に設定しました。")
    except:
        messagebox.showwarning("選択なし", "作業中にするタスクを選んでください。")

● シンプルなフラグ管理なのに利便性が高い

ポモドーロアプリは、
「どのタスクに時間を使ったか?」を記録すると一気に便利になります。

特に、

  • 勉強
  • ブログ執筆
  • プログラミング

の時間管理に役立つため、
本アプリの満足度が上がります。


4-5. JSON保存(save_tasks と load_tasks)

いよいよ永続化処理の解説に入ります。


● save_tasks(保存)

引用:

def save_tasks(self):
    tasks = self.task_list.get(0, tk.END)
    with open("tasks.json", "w", encoding="utf-8") as f:
        json.dump(list(tasks), f, ensure_ascii=False, indent=4)

● 解説

  • Listbox の全要素を取得(タプルで返る)
  • list() でリスト化
  • JSONに保存

かなりシンプルで読みやすい構造です。

ensure_ascii=False
日本語をエスケープせず保存する設定です。


● load_tasks(読み込み)

引用:

def load_tasks(self):
    try:
        with open("tasks.json", "r", encoding="utf-8") as f:
            tasks = json.load(f)
            for t in tasks:
                self.task_list.insert(tk.END, t)
    except FileNotFoundError:
        pass

● 解説

  1. ファイルが存在しなければ無視(エラーを出さない)
  2. 読み込んだリストを Listbox に追加

JSON の読み書きは Python 初心者が
「自分でデータ永続化できる」喜びを得られる重要ポイントです。

ここまでで TODO機能の全ての仕組み が理解できました。

第5章:ポモドーロタイマーの実装(詳細コード解説)

この章では、今回のアプリの中核機能である ポモドーロタイマー の仕組みを徹底解説します。

Tkinter の GUI アプリでは
after() を使った「疑似スレッド」のような処理が重要ポイントになります。


5-1. ポモドーロタイマーの全体構造

まず、コード全体の流れを頭に入れましょう。

ポモドーロは通常:

  • 25分作業(ワークセッション)
  • 5分休憩(ブレイク)

という流れを繰り返します。

アプリは以下のように構成されています:

  1. 「開始」ボタンでタイマー開始
  2. after(1000ms) で1秒ごとに残り時間を減らす
  3. 0になったら作業 → 休憩の自動切り替え
  4. 進捗バーも連動
  5. 表示を更新し続ける

このような一連の動きが
1つの関数では完結せず、複数の関数で役割分担 されています。


5-2. スタートボタンの処理

引用:

def start_timer(self, minutes=25):
    if self.is_running:
        return

    self.is_running = True
    self.remaining = minutes * 60
    self.progress["maximum"] = self.remaining
    self.progress["value"] = self.remaining

    self.update_timer()

● 詳細解説

① 2重起動防止

if self.is_running:
    return

GUIアプリは、
ユーザーが何度もボタンを連打する可能性があります。

これがないと、
タイマーが大量に多重起動してバグる ので非常に大切です。


② 残り時間(秒)に変換

self.remaining = minutes * 60

タイマーは秒単位で計算する方が便利なので、
25分なら1500秒に変換します。


③ Progressbar の最大値を設定

self.progress["maximum"] = self.remaining
self.progress["value"] = self.remaining

これは非常に重要です。

Progressbar は数値を

  • 0(完了)
  • maximum(開始時点)

として扱います。

残り時間が減るほどバーが左へ縮む設計になっています。


④ タイマー更新処理の開始

self.update_timer()

ここから「1秒ごとに動く処理」が始まります。


5-3. update_timer() の実装

ポモドーロの核心部分です。

引用:

def update_timer(self):
    if self.remaining <= 0:
        self.finish_session()
        return

    mins = self.remaining // 60
    secs = self.remaining % 60
    self.timer_label.config(text=f"{mins:02}:{secs:02}")

    self.progress["value"] = self.remaining

    self.remaining -= 1
    self.master.after(1000, self.update_timer)

● この関数の役割は 1 つだけ


✔「1秒ごとに画面を更新し、秒数を減らし続ける」

これです。


行ごとに解説します


① タイムアップ判定

if self.remaining <= 0:
    self.finish_session()
    return

残り時間が0になったら
「作業セッション終了 → 休憩へ」
もしくは
「休憩終了 → 次の作業へ」
という遷移を行います。


② 表示用の時間整形

mins = self.remaining // 60
secs = self.remaining % 60
self.timer_label.config(text=f"{mins:02}:{secs:02}")
  • // :整数の割り算(分)
  • % :余り(秒)

これで「00:00」形式に整えます。


③ 進捗バーの更新

self.progress["value"] = self.remaining

Progressbar の値を残り秒数と同期させます。

0に向かって減るため、
終了が近づくほどバーが短くなります。


④ 残り時間を1秒だけ減らす

self.remaining -= 1

⑤ after() による「1秒後の再実行」

self.master.after(1000, self.update_timer)

Tkinter の after() は、
「1000ms(1秒)後に関数を実行する」という命令です。

これにより GUI がフリーズせず、
スムーズに1秒ごとの更新が可能になります。


after() がどれだけ大事か?

Tkinter では、
スレッドを使わずに「待ち時間」を実現できるのは after() だけです。

もし time.sleep() を使うと…


✔ アプリ全体が固まる(フリーズする)


そのため Tkinter でタイマーやアニメーションを作りたいときは、
100% after() を使います。


5-4. セッション終了処理(finish_session)

引用:

def finish_session(self):
    self.is_running = False

    if self.current_task:
        messagebox.showinfo("完了", f"『{self.current_task}』の作業が完了しました!")
    else:
        messagebox.showinfo("完了", "作業が完了しました!")

    self.start_break()

● 解説

  1. タイマー停止フラグをOFFにする
  2. メッセージを表示(作業中タスクを含める)
  3. 自動的に休憩(break)へ移行

ここで休憩が始まります。

(画像:ポモドーロ完了ダイアログのスクリーンショットを挿入)


5-5. 休憩タイマーの開始

引用:

def start_break(self):
    self.timer_label.config(text="休憩タイム")
    self.master.after(2000, lambda: self.start_timer(5))

● 解説

休憩開始 → 2秒間メッセージ表示 → 自動で5分カウント開始

という自然な流れです。

休憩前に「ちょっと余韻を残す」感じの UI に仕上げています。


5-6. ストップボタンによる停止

引用:

def stop_timer(self):
    self.is_running = False
    self.remaining = 0
    self.timer_label.config(text="停止中")

● 重要ポイント

stop を押すと
「強制的に update_timer の実行チェーンを途切れさせる」
という効果があります。

after() は
「次の関数呼び出しを予約」するだけなので、

is_running が False なら update_timer は何もせず止まる

という仕組みです。


第5章:アプリの起動・レイアウト構築の解説(続き)

5-4. タスク管理UIの詳細解説(右側パネル)

右側エリアは「タスクの追加・編集・削除・選択」を扱う、アプリの操作中心となる部分です。
ここでは Listbox / Entry / Button の組み合わせを理解することができます。


(コード引用)タスク管理UI

# ==== タスク管理UI(右側) ====
tk.Label(right_frame, text="タスク一覧", font=("Arial", 12, "bold")).pack()

task_listbox = tk.Listbox(right_frame, width=25, height=15)
task_listbox.pack(pady=5)

task_entry = tk.Entry(right_frame)
task_entry.pack(pady=5)

add_button = tk.Button(right_frame, text="追加", command=add_task)
add_button.pack(fill="x")

edit_button = tk.Button(right_frame, text="編集", command=edit_task)
edit_button.pack(fill="x")

delete_button = tk.Button(right_frame, text="削除", command=delete_task)
delete_button.pack(fill="x")

start_button = tk.Button(right_frame, text="作業中に設定", command=mark_working)
start_button.pack(fill="x")

5-5. Listboxの動作と特徴

Listbox は tkinter の中でも非常に軽量なウィジェットで、タスクリストのような縦に並ぶ要素を表示するのに最適です。

Listbox のよく使う関数

関数説明
insert(index, text)指定位置に項目を追加
delete(index)指定項目を削除
curselection()現在選択しているインデックスを取得
get(index)指定インデックスの文字を取得

このアプリでは add / edit / delete / mark_working の各関数で、この Listbox にアクセスしています。



第6章:機能別コード解説(最重要)

ここからがこの記事のメインです。
なぜこの書き方なのか?
どこが初心者にとって成長ポイントなのか?

を、1行ずつ丁寧に解説していきます。


6-1. タスク追加機能 ― add_task()

まずは最も基本的な機能「タスク追加」です。

(コード引用)

def add_task():
    task = task_entry.get()
    if task:
        tasks.append({"name": task, "working": False})
        task_listbox.insert(tk.END, task)
        task_entry.delete(0, tk.END)
        save_tasks()

6-1-1. task_entry.get() でユーザー入力を取得

Entry は1行入力用のテキストボックス。
get() を呼べば、ユーザーが入力した文字列が取れます。


6-1-2. tasks(内部リスト)に辞書形式で保存

tasks.append({"name": task, "working": False})

ここが重要ポイント。

なぜ辞書で保存しているのか?

タスクごとに状態(作業中かどうか)を持たせる必要があるためです。

初心者はここで「Listboxに入れたものがすべてじゃないの?」と思いがちですが、
GUI の表示状態と内部データは必ず分けるべきです。


6-1-3. Listbox にも表示

task_listbox.insert(tk.END, task)

Listbox は「見た目」だけなので、内部データとは別で更新します。


6-1-4. 入力欄をクリア

task_entry.delete(0, tk.END)

この操作があるだけで UX が段違いに良くなります。


6-1-5. save_tasks() の呼び出し(JSON保存)

ここで JSON に保存されるため、アプリを終了してもタスクが残ります。



6-2. タスク編集機能 ― edit_task()

次に重要な「編集」。

(コード引用)

def edit_task():
    idx = task_listbox.curselection()
    if idx:
        new_text = task_entry.get()
        if new_text:
            tasks[idx[0]]["name"] = new_text
            task_listbox.delete(idx[0])
            task_listbox.insert(idx[0], new_text)
            save_tasks()

6-2-1. curselection() で選択項目を取得

初心者がつまずきポイント:

curselection() はタプルで返る

例:

(3,)

なので idx[0] として取り出す必要があります。


6-2-2. 内部データ(tasks)と Listbox を両方更新

編集は

  • 内部データの name を書き換える
  • Listbox の該当行を削除 → 新しいテキストで再追加

という2ステップが必要。



6-3. タスク削除機能 ― delete_task()

(コード引用)

def delete_task():
    idx = task_listbox.curselection()
    if idx:
        task_listbox.delete(idx[0])
        tasks.pop(idx[0])
        save_tasks()

削除のポイント

pop() と Listbox.delete() は順番が重要

  • Listbox.delete → 見た目を先に消す
  • tasks.pop → 内部データも消す

もし逆にすると UI の削除がズレる可能性があります。



6-4. 作業中フラグを立てる ― mark_working()

(コード引用)

def mark_working():
    idx = task_listbox.curselection()
    if idx:
        for t in tasks:
            t["working"] = False
        tasks[idx[0]]["working"] = True
        save_tasks()

ポイントは「全タスクの working をリセット」

ポモドーロは1つのタスクに集中する前提のため、
複数タスクを並列処理する想定はしない設計になっています。



6-5. JSON保存/読み込みの仕組み

このアプリの基盤部分です。

(コード引用)

def save_tasks():
    with open("tasks.json", "w", encoding="utf-8") as f:
        json.dump(tasks, f, ensure_ascii=False, indent=2)

def load_tasks():
    global tasks
    try:
        with open("tasks.json", "r", encoding="utf-8") as f:
            tasks = json.load(f)
            for t in tasks:
                task_listbox.insert(tk.END, t["name"])
    except FileNotFoundError:
        tasks = []

初心者が学べるポイント

● JSON は Python の辞書・リストと相性がよい

  • Python の list → JSON配列
  • Python の dict → JSONオブジェクト

そのまま保存・復元できます。

● FileNotFoundError の丁寧な処理

初回起動時は tasks.json が無いため、
エラーではなく「空リスト」から開始するのが自然な UX。



6-6. タイマーの仕組み(after の本領)

ポモドーロタイマーの心臓部です。

(コード引用)

def update_timer():
    global remaining_seconds, running
    if running and remaining_seconds > 0:
        remaining_seconds -= 1
        timer_label.config(text=format_time(remaining_seconds))
        progress["value"] = (work_duration - remaining_seconds) / work_duration * 100
        root.after(1000, update_timer)

ここが「GUI プログラミングの壁」を越える超重要ポイントです。


6-6-1. after の動き

root.after(1000, update_timer)

  • 1000ミリ秒後に update_timer をもう一度呼ぶ
  • 無限ループではないため GUI が固まらない

初心者が陥る失敗

while True:
    時間更新…

これは GUI をフリーズさせるため絶対禁止。


6-6-2. プログレスバー計算式

(work_duration - remaining_seconds) / work_duration * 100

進捗率(%)を算出し、Progressbar に反映します。



6-7. タイマー開始/停止

(コード引用)

def start_timer():
    global running, remaining_seconds
    running = True
    remaining_seconds = work_duration
    update_timer()

ロジックのポイント

  • running = True にして update_timer に処理を進めさせる
  • update_timer 内で after が繰り返される


6-8. 休憩タイマー(5分)の処理

(コード引用)

def start_break():
    global running, remaining_seconds
    running = True
    remaining_seconds = break_duration
    update_timer()

これを実装することでポモドーロが完成します。

25分 → 5分
の切り替えを user が自由に行えます。



第7章:UI をもっと良くする改善案

画像:完成UIのスクリーンショットを挿入

作ったアプリはそのままでも動きますが、
以下を追加すると “製品レベル” に近づきます。


7-1. タスクの「作業中」を色表示

Listbox の文字色を変えることで、
今どのタスクを進めているか直感的にわかるようになります。


7-2. 通知音の追加

作業終了時に「ピッ」という音を鳴らすと UX が向上。


7-3. ポモドーロ統計(作業時間の累計)

  • 今日の作業時間
  • 月間合計
  • タスクごとの集中回数

このあたりを JSON に追記すれば簡単に実装できます。


第8章:この記事のまとめ

このアプリは、
Python(tkinter)で実践的なGUIとタスク管理・タイマー処理が学べる最高の教材です。

  • GUI 操作(Listbox, Entry, Button)
  • JSON 保存
  • after() によるタイマー制御
  • Progressbar の処理

など、初心者が中級者にステップアップするための要素が詰まっています。

  • TODO管理アプリ
  • ポモドーロタイマー
  • 作業中のタスク切替
  • データ保存

これら全部を体験できます。

「簡単に書けるけど本格的」
そんなアプリだからこそ学習に最適です。

そして Python GUI を学ぶと、
あなたの PC が仕事道具として何倍も活躍できるようになります。


最後に ― これから本気で学ぶあなたへ

Python で GUI を作り始めると、
どうしても同時に必要になるのが 快適にプログラムを動かせるPC です。

  • マルチタスク
  • Python実行環境
  • 画像編集
  • YouTube編集(学習記録の発信)

これらをストレスなく進められる PC があると、
学習効率が驚くほど変わります。

この記事の読者の多くが、
「Python を本格的にやってみたい」
という段階に進むはずです。

そんな時、あなたの進歩を邪魔しない “良い相棒(PC)” を手元に置いておくと、
モチベーションも成功確率も大きく変わります。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

ABOUT ME
tyamada
tyamada
普通の会社員(平)
記事URLをコピーしました