ポモドーロタイマー
25分作業・5分休憩サイクルで生産性を高めるポモドーロタイマー。音声通知・統計・カスタム設定付きです。
1. アプリ概要
25分作業・5分休憩サイクルで生産性を高めるポモドーロタイマー。音声通知・統計・カスタム設定付きです。
このアプリは中級カテゴリに分類される実践的なGUIアプリです。使用ライブラリは tkinter(標準ライブラリ) で、難易度は ★★☆ です。
このアプリは「ユーティリティ」カテゴリです。小回りの効くユーティリティは Python の真骨頂で、汎用的なユーザー操作を組み合わせる発想が他のアプリにもそのまま流用できます。tkinter(標準ライブラリ) を活かして実装するこの構造は、他のアプリにも応用が効きます。
コードを読む前に実行することをおすすめします。動いている挙動を先に把握しておくと、解説で出てくる関数や処理がどこに対応するかが頭に入りやすくなります。
応用のヒントは、機能を 1 つ増やす・見た目を整える・例外を一つでも丁寧に扱う、のいずれかから始めるのがおすすめです。
2. 機能一覧
- 「⚙ 設定」ボタン(
_open_settings())・「▶ スタート」ボタン(_toggle())・「⏭ スキップ」ボタン(_next_session())・「🔄 リセット」ボタン(_reset())・「保存」ボタンで操作 after(1000, ...)による1000ミリ秒間隔の_tick()呼び出し(画面の自動更新)tk.Listboxによる一覧表示- JSON ファイルの保存・読み込み(起動時に自動実行)
messagebox.showinfo()によるダイアログ通知- ウィンドウはタイトル「ポモドーロタイマー」・サイズ 480x611 で起動
3. 事前準備・環境
Python 3.10 以上 / Windows 11(実機)で起動・基本操作を確認 / Linux Mint 22.3(仮想マシン)で起動・画面表示を確認(macOS は未検証)
Windows 11(実機)で起動と基本操作を確認しています(全機能の網羅テストではありません)。Linux Mint 22.3(仮想マシン)では起動と画面表示を確認しました。macOS は標準ライブラリの範囲で動作する想定ですが、未検証です。
- Python 3.10 以上
- OS: Windows 11(実機で起動・基本操作を確認)・Linux Mint 22.3(仮想マシンで起動・画面表示を確認)
4. 完全なソースコード
右上の「コピー」ボタンをクリックするとコードをクリップボードにコピーできます。
import tkinter as tk
from tkinter import ttk, messagebox
import time
import threading
import json
import os
import platform
try:
import winsound
SOUND_AVAILABLE = platform.system() == "Windows"
except ImportError:
SOUND_AVAILABLE = False
class App25:
"""ポモドーロタイマー"""
SETTINGS_FILE = os.path.join(os.path.dirname(__file__), "pomodoro_settings.json")
DEFAULT_SETTINGS = {
"work_min": 25,
"short_break_min": 5,
"long_break_min": 15,
"long_break_interval": 4,
}
def __init__(self, root):
self.root = root
self.root.title("ポモドーロタイマー")
self.root.geometry("480x611")
self.root.minsize(480, 611)
self.root.configure(bg="#c62828")
self.settings = self._load_settings()
self.running = False
self.pomodoro_count = 0
self.session_type = "work" # work, short_break, long_break
self.remaining = self.settings["work_min"] * 60
self.total_today = 0
self._timer_id = None
self._stats = {"work": 0, "short_break": 0, "long_break": 0}
self._build_ui()
self._update_display()
def _load_settings(self):
if os.path.exists(self.SETTINGS_FILE):
try:
with open(self.SETTINGS_FILE) as f:
return {**self.DEFAULT_SETTINGS, **json.load(f)}
except Exception:
pass
return dict(self.DEFAULT_SETTINGS)
def _save_settings(self):
with open(self.SETTINGS_FILE, "w") as f:
json.dump(self.settings, f)
def _build_ui(self):
# ヘッダー
header = tk.Frame(self.root, bg="#b71c1c", pady=6)
header.pack(fill=tk.X)
tk.Label(header, text="🍅 ポモドーロタイマー",
font=("Noto Sans JP", 14, "bold"),
bg="#b71c1c", fg="white").pack(side=tk.LEFT, padx=12)
ttk.Button(header, text="⚙ 設定",
command=self._open_settings).pack(side=tk.RIGHT, padx=8)
# セッション種別バッジ
self.session_frame = tk.Frame(self.root, bg="#c62828")
self.session_frame.pack(pady=6)
self.session_label = tk.Label(self.session_frame,
text="🍅 作業中",
font=("Noto Sans JP", 13, "bold"),
bg="#c62828", fg="white")
self.session_label.pack()
# タイマー表示
timer_outer = tk.Frame(self.root, bg="#c62828")
timer_outer.pack(pady=10)
self.canvas = tk.Canvas(timer_outer, width=240, height=240,
bg="#c62828", highlightthickness=0)
self.canvas.pack()
# 円形プログレス
self.canvas.create_oval(20, 20, 220, 220, outline="#b71c1c",
width=12, tags="bg_arc")
self.arc_id = self.canvas.create_arc(20, 20, 220, 220,
start=90, extent=360,
style="arc", outline="#ff8a80",
width=12, tags="arc")
self.timer_text_id = self.canvas.create_text(
120, 120, text="25:00",
font=("Courier New", 40, "bold"),
fill="white", tags="timer_text")
# コントロール
ctrl_f = tk.Frame(self.root, bg="#c62828")
ctrl_f.pack(pady=8)
btn_s = {"bg": "#e53935", "fg": "white", "relief": tk.FLAT,
"font": ("Noto Sans JP", 12), "padx": 20, "pady": 8,
"activebackground": "#b71c1c", "activeforeground": "white", "bd": 0}
self.start_btn = tk.Button(ctrl_f, text="▶ スタート",
command=self._toggle, **btn_s)
self.start_btn.pack(side=tk.LEFT, padx=8)
tk.Button(ctrl_f, text="⏭ スキップ",
command=self._next_session, **btn_s).pack(side=tk.LEFT, padx=8)
tk.Button(ctrl_f, text="🔄 リセット",
command=self._reset, **btn_s).pack(side=tk.LEFT, padx=8)
# ポモドーロカウンター
count_f = tk.Frame(self.root, bg="#c62828")
count_f.pack(pady=4)
tk.Label(count_f, text="今日の達成:",
bg="#c62828", fg="#ffcdd2",
font=("Noto Sans JP", 11)).pack(side=tk.LEFT)
self.count_label = tk.Label(count_f, text="🍅 × 0",
bg="#c62828", fg="white",
font=("Noto Sans JP", 14, "bold"))
self.count_label.pack(side=tk.LEFT, padx=8)
# 今日の統計
stats_f = ttk.LabelFrame(self.root, text="今日の統計", padding=10)
stats_f.pack(fill=tk.X, padx=16, pady=8)
self.stats_labels = {}
inner = tk.Frame(stats_f, bg=ttk.Style().lookup("TLabelframe", "background"))
inner.pack()
for key, lbl in [("work", "🍅 作業時間"),
("short_break", "☕ 短い休憩"),
("long_break", "😴 長い休憩")]:
f2 = tk.Frame(inner, bg=inner.cget("bg"), padx=12)
f2.pack(side=tk.LEFT)
tk.Label(f2, text=lbl, bg=f2.cget("bg"),
font=("Arial", 9), fg="#555").pack()
lbl2 = tk.Label(f2, text="0分", bg=f2.cget("bg"),
font=("Arial", 12, "bold"))
lbl2.pack()
self.stats_labels[key] = lbl2
# ログ
log_f = ttk.LabelFrame(self.root, text="セッションログ", padding=4)
log_f.pack(fill=tk.BOTH, expand=True, padx=16, pady=(0, 8))
self.log_listbox = tk.Listbox(log_f, height=5,
font=("Arial", 9))
self.log_listbox.pack(fill=tk.BOTH, expand=True)
def _toggle(self):
if self.running:
self.running = False
self.start_btn.config(text="▶ スタート")
if self._timer_id:
self.root.after_cancel(self._timer_id)
else:
self.running = True
self.start_btn.config(text="⏸ 一時停止")
self._tick()
def _tick(self):
if not self.running:
return
if self.remaining > 0:
self.remaining -= 1
self._update_display()
self._timer_id = self.root.after(1000, self._tick)
else:
self._session_complete()
def _session_complete(self):
self.running = False
self.start_btn.config(text="▶ スタート")
self._beep()
dur_min = self._session_minutes()
self._stats[self.session_type] += dur_min
self._update_stats_display()
now_str = self._now_str()
if self.session_type == "work":
self.pomodoro_count += 1
self.count_label.config(text=f"🍅 × {self.pomodoro_count}")
self.log_listbox.insert(0,
f"{now_str} 🍅 作業 {dur_min}分 完了")
messagebox.showinfo("完了", f"🍅 ポモドーロ #{self.pomodoro_count} 完了!\n休憩しましょう!")
else:
label = "短い休憩" if self.session_type == "short_break" else "長い休憩"
self.log_listbox.insert(0,
f"{now_str} ☕ {label} {dur_min}分 完了")
self._next_session()
def _next_session(self):
if self.running:
self._toggle()
if self.session_type == "work":
interval = self.settings["long_break_interval"]
if self.pomodoro_count % interval == 0 and self.pomodoro_count > 0:
self.session_type = "long_break"
else:
self.session_type = "short_break"
else:
self.session_type = "work"
self.remaining = self._session_minutes() * 60
self._update_display()
def _reset(self):
if self.running:
self._toggle()
self.remaining = self._session_minutes() * 60
self._update_display()
def _session_minutes(self):
return {
"work": self.settings["work_min"],
"short_break": self.settings["short_break_min"],
"long_break": self.settings["long_break_min"],
}[self.session_type]
def _update_display(self):
mins = self.remaining // 60
secs = self.remaining % 60
self.canvas.itemconfig(self.timer_text_id,
text=f"{mins:02d}:{secs:02d}")
# アーク更新
total = self._session_minutes() * 60
ratio = self.remaining / total if total else 0
extent = 360 * ratio
self.canvas.itemconfig(self.arc_id, extent=extent)
# セッションラベル
labels = {
"work": ("🍅 作業中", "#c62828"),
"short_break": ("☕ 短い休憩", "#1565c0"),
"long_break": ("😴 長い休憩", "#1b5e20"),
}
lbl, bg = labels[self.session_type]
self.session_label.config(text=lbl)
self.root.configure(bg=bg)
self.session_label.configure(bg=bg)
def _update_stats_display(self):
for key, lbl in self.stats_labels.items():
lbl.config(text=f"{self._stats[key]}分")
def _beep(self):
if SOUND_AVAILABLE:
try:
winsound.Beep(880, 500)
winsound.Beep(1100, 500)
except Exception:
pass
def _now_str(self):
return time.strftime("%H:%M")
def _open_settings(self):
win = tk.Toplevel(self.root)
win.title("設定")
win.geometry("280x220")
win.configure(bg="#f8f9fc")
fields = [("作業時間(分)", "work_min"),
("短い休憩(分)", "short_break_min"),
("長い休憩(分)", "long_break_min"),
("長い休憩間隔(ポモドーロ数)", "long_break_interval")]
vars_ = {}
for i, (lbl, key) in enumerate(fields):
tk.Label(win, text=f"{lbl}:",
bg="#f8f9fc").grid(row=i, column=0, sticky="w",
padx=8, pady=4)
var = tk.IntVar(value=self.settings[key])
ttk.Entry(win, textvariable=var, width=8).grid(
row=i, column=1, padx=8)
vars_[key] = var
def save():
for key, var in vars_.items():
self.settings[key] = var.get()
self._save_settings()
self._reset()
win.destroy()
ttk.Button(win, text="保存", command=save).grid(
row=len(fields), column=0, columnspan=2, pady=10)
if __name__ == "__main__":
root = tk.Tk()
app = App25(root)
root.mainloop()
5. コード解説
ポモドーロタイマーのコードを、実際に書かれている実装に沿って解説します。
クラス設計とコンストラクタ
App25 クラスにアプリの全機能をまとめています(メソッド15個・全283行)。__init__ ではタイトル「ポモドーロタイマー」とウィンドウサイズ 480x611 を設定します。あわせて状態を保持する変数(self.running・self.pomodoro_count・self.session_type・self._timer_id)を初期化し、そのあと _build_ui() → _update_display() の順に呼び出して初期状態を作ります。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
ウィジェット構成
画面は tk.Frame×6(ほかにループでも生成)・tk.Label×4(ほかにループでも生成)・ttk.Button×2・tk.Canvas・tk.Button×3・ttk.LabelFrame×2・ttk.Style・tk.Listbox・tk.Toplevel・ttk.Entry(ループで生成) で構成しています。見出し付きの枠(LabelFrame)は「今日の統計」「セッションログ」のラベルでエリアを区切っています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
イベント処理とボタンの接続
「⚙ 設定」ボタン(_open_settings())・「▶ スタート」ボタン(_toggle())・「⏭ スキップ」ボタン(_next_session())・「🔄 リセット」ボタン(_reset())・「保存」ボタンを command= で接続しています。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
after() による定期処理
_tick() の末尾で after(1000, ...) を呼び、1000ミリ秒後に自分自身の再実行を予約しています。tkinter では while ループで待つと画面が固まるため、この「予約の連鎖」で定期処理を作るのが定石です。停止時は after_cancel() で予約を取り消すので、止めた後に処理が走り続けることはありません。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
JSON への保存と読み込み
データは json.dump() でファイル(pomodoro_settings.json)へ保存します。起動時は json.load() で読み戻します。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
クラス定数によるデータ定義
クラスの先頭でデータを定数として定義しています:SETTINGS_FILE = os.path.join(os.path.dirname(__file__), 'pomodoro_settings.json')。ロジックの中に直接書かず定数にまとめておくと、内容を変えたいときに1か所を書き換えるだけで済みます。
※ 該当部分のコード本体は 「4. 完全なソースコード」 をご参照ください(重複表示を避けるため再掲を省略しています)。
6. ステップバイステップガイド
このアプリをゼロから自分で作る手順を解説します。コードをコピーするだけでなく、実際に手順を追って自分で書いてみましょう。
-
1ファイルを作成する
新しいファイルを作成して app25.py と保存します。使うのは
json・os・platform・threading・time・tkinter・winsoundだけなので、追加インストールは不要です。 -
2クラスの骨格を作る
App25クラスを定義し、__init__と、末尾のroot = tk.Tk()~mainloop()の最小構成を書いて、まず空のウィンドウが出ることを確認します。 -
3ウィンドウを設定する
title("ポモドーロタイマー")・geometry("480x611")・configure(bg="#c62828")・minsize(480, 611)をコンストラクタで設定します。 -
4画面部品を並べる
_build_ui()の中で、tk.Frame×6(ほかにループでも生成)・tk.Label×4(ほかにループでも生成)・ttk.Button×2・tk.Canvas・tk.Button×3・ttk.LabelFrame×2・ttk.Style・tk.Listbox・tk.Toplevel・ttk.Entry(ループで生成) を作って配置します(掲載コードと同じ並び順で書くとレイアウトが一致します)。まず表示だけ確認しましょう。 -
5イベントを接続する
「2. 機能一覧」で挙げた各ボタンを
command=で対応するメソッド(_open_settings()・_toggle()・_next_session()・_reset()・save)につなぎます。 -
6中心になるメソッドを実装する
アプリの本体である
_tick()(9行)・_open_settings()(28行)・_next_session()(13行)など を実装します。定期処理はafter(1000, ...)で自分自身を予約し続ける形にします。 -
7動作確認する
python app25.pyで起動し、各ボタンが反応すること・表示が自動で更新されることを確認します。
7. カスタマイズアイデア
基本機能を習得したら、以下のカスタマイズに挑戦してみましょう。少しずつ機能を追加することで、Pythonのスキルが飛躍的に向上します。
💡 ダークモードを追加する
bg色・fg色を辞書で管理し、ボタン1つでダークモード・ライトモードを切り替えられるようにしましょう。
💡 更新間隔を変えてみる
after(1000, ...) の 1000 が更新間隔(ミリ秒)です。大きくすると更新頻度が下がって CPU 負荷が軽くなり、小さくすると更新が細かくなります(表示の刻み幅より細かくしても見た目は変わりません)。数値を変えて違いを確かめてみましょう。
💡 入力履歴機能
以前の入力値を覚えておいてComboboxのドロップダウンで再選択できる履歴機能を追加しましょう。
8. よくある問題と解決法
❌ 文字のフォントが崩れる・見た目が違う
原因:このコードは font=("Noto Sans JP", ...) のようにフォント名を直接指定しています。環境にこのフォントが入っていないと、OS が代わりのフォントで表示するため、見本と見た目が変わることがあります。
解決法:動作には支障ありません。気になる場合は font 引数の名前を自分の OS に入っているフォントへ書き換えるか、font 引数を省略してください。
❌ 改造したら表示の更新が二重になった・速くなった
原因:_tick() は after(1000, ...) で自分自身を予約し続ける構造です。予約を止めずにもう一度開始経路を通ると、予約の連鎖が2本になり、更新が倍速になります。
解決法:掲載コードは動作中フラグの判定と after_cancel() による予約取り消しをすでに実装しています。改造でこの2つを外さないように注意してください。
❌ ウィンドウの大きさが画面に合わない
原因:geometry("480x611") の固定サイズで起動するためです。
解決法:geometry() と minsize() の両方の数値を書き換えて起動サイズを調整してください。なお、このコードにはウィンドウサイズを固定する設定(resizable の指定)が無いため、ウィンドウの端をドラッグしたサイズ変更は既定どおり可能です。ただし minsize(480, 611) を指定しているため、起動時の大きさより小さくは縮められません(縮めると画面部品が表示されなくなるのを防ぐためです)。
9. 練習問題
アプリの理解を深めるための練習問題です。気になるものから挑戦してみてください(課題3は定型の発展課題です)。設定は同じフォルダの pomodoro_settings.json に保存されます(19行・54-56行)。セッションが終わると winsound.Beep(880, 500) と winsound.Beep(1100, 500) が鳴ります(239-245行)。winsound は Windows だけの標準モジュールなので、他の OS では SOUND_AVAILABLE が False のままになり無音で進みます(9-13行)。夜間やイヤホンでの練習は、先に音量を下げてから試してください。課題1・課題2に追加のライブラリは要りません。
-
課題1:走らせている途中で設定を保存すると、タイマーが振り出しに戻る
「⚙ 設定」の「保存」は、値を書き込んだあとに
_reset()を呼びます(273行)。_reset()は動いているタイマーを止めて、残り時間を最初へ戻します(201-205行)。何も変えずに保存しただけでも、そこまで数えた時間は消えます。期待結果:「▶ スタート」を押して数秒待ち、表示が
24:55のあたりになったのを確かめる。そのまま「⚙ 設定」を開き、値を変えずに「保存」を押す。設定の小窓が閉じると表示は25:00へ戻り、ボタンも「⏸ 一時停止」から「▶ スタート」に戻る。直したあとは、保存しても止まらず 24分台から数え続ける。合格条件
- 動いている途中で設定を保存しても、残り時間と「⏸ 一時停止」の表示がそのまま残る
- 作業時間を変えて保存したときは、次のセッションから新しい長さになる
- 止まっているときに保存したときの動きは今までどおり(表示が新しい長さに変わる)
ヒント①(どこを触るか): 触るのは1か所です。
_open_settings()の中のsave()、self._reset()の行(273行)。
ヒント②(使うもの): 動いているかどうかはself.running(35行)で分かります。if not self.running:で囲むと、走っているときだけ残り時間を守れます。止めてよいか聞くならmessagebox.askyesno()が使えます(2行で import 済み)。
つまずきやすい点:_reset()はif self.running: self._toggle()でタイマーを止めてから、self.remainingを入れ直します(202-204行)。止める行だけ消すと残り時間の書き換えは残るので、表示だけが戻ります。設定の書き込み(self._save_settings()・272行)は残してください。消すとpomodoro_settings.jsonが更新されません。 -
課題2:休憩に切り替えても、色が変わるのはウィンドウのふちだけ
_update_display()はセッションごとの色を決めて、self.rootとself.session_labelにだけ入れます(225-233行)。タイマーの円やボタンの帯はbg="#c62828"の赤で作ったままです(78行・80-81行・96行・110行)。休憩に入ると、赤と青が混ざった見た目になります。期待結果:起動した直後は画面全体が赤い。「⏭ スキップ」を1回押すと、上のラベルが
☕ 短い休憩に変わり、数字は05:00になる。ここでウィンドウのふち(余白)は青くなるが、タイマーの円のまわりとボタンの帯は赤いまま残る。直したあとは円のまわりとボタンの帯も青くなり、もう一度「⏭ スキップ」を押して作業へ戻ると赤に戻る。合格条件
- 短い休憩に切り替えると、タイマーの円のまわり(キャンバスの背景)とボタンの帯も青(
#1565c0)になる - もう一度「⏭ スキップ」で作業に戻ると、どちらも赤(
#c62828)へ戻る - 数字とラベルの文字は今までどおり白のまま読める
ヒント①(どこを触るか): 触るのは2か所です。
_build_ui()で赤いフレームを作っている行(78行・96行・110行)と、_update_display()の色を入れている行(230-233行)。
ヒント②(使うもの): 変数に入っていないフレームは、あとから色を変えられません。self._tinted = [timer_outer, ctrl_f, count_f, self.canvas]のようにリストへ入れておき、for w in self._tinted: w.configure(bg=bg)で回します。self.session_frameは 69行ですでに変数に入っています。
つまずきやすい点:tk.Labelは自分のbgを持っているので、フレームだけ変えても文字の後ろは赤いままです(112-114行)。Canvas に描いた円の色はoutlineで決まっていて(84-89行)、configure(bg=...)では変わりません。ttk.LabelFrame(121行・139行)は ttk のスタイルで色が決まるため、同じやり方は効きません。 - 短い休憩に切り替えると、タイマーの円のまわり(キャンバスの背景)とボタンの帯も青(
-
課題3:クラス定数を書き換えて動きを変える
クラス定数(
SETTINGS_FILE・DEFAULT_SETTINGS)の中身を書き換えて、画面や動きがどう変わるかを確かめてみましょう。複数の定数が対応関係にある場合は、セットで整合させる必要がある点にも注目してください。
写経中に赤いエラー文が出たら、Pythonエラー一覧&解決法(英語メッセージ逆引き)で原因と直し方をすぐ確認できます。
このレベルのアプリが作れたら、入門書の次の「作るための本」へ進む時期です。実践におすすめのPython本(当サイトの参考書ランキング総合3〜4位)で自動化とコードの書き方の2冊を、その直後の用途別専門書の節でデータ分析・Web・AIの本を比較しています。