ساخت تایمر پومودورو با پایتون

پروژه‌های GUI پایتون

پروژه ۸ دقیقه مطالعه
فهرست مطالب
این مطلب از سایت tool.hamidvalad.ir گرفته شده است

معرفی تایمر پومودورو و کتابخانه‌های مورد نیاز

مدیریت زمان یکی از مهم‌ترین چالش‌هایی است که افراد در دنیای آموزشی، کاری و حتی زندگی شخصی با آن روبه‌رو هستند. بسیاری از افراد با وجود تلاش‌های مداوم، باز هم نمی‌توانند تمرکز خود را برای مدت طولانی حفظ کنند یا بین کارها دچار سردرگمی می‌شوند. یکی از تکنیک ‌هایی که به‌شکل گسترده در مدیریت زمان مورد استفاده قرار می‌گیرد و توانسته اثرگذاری خود را در میان دانشجویان، برنامه‌نویسان و حتی مدیران ثابت کند، تکنیک «پومودورو» یا Pomodoro Technique است. این روش ساده اما بسیار کاربردی، زمان کار را به بخش‌های قابل مدیریت تقسیم می‌کند و بین آن‌ها استراحت‌های کوتاه قرار می‌دهد تا مغز فرصت ریکاوری داشته باشد و سطح تمرکز تثبیت شود.

مبنای کار این تکنیک ۲۵ دقیقه فعالیت متمرکز و ۵ دقیقه استراحت است. اجرای دقیق این چرخه باعث افزایش چشمگیر بهره‌وری، کاهش خستگی ذهنی و جلوگیری از فرسودگی شغلی می‌شود. یکی از بهترین راه‌ها برای تمرین پومودورو و پایبندی به آن، ساخت یک تایمر اختصاصی است؛ به‌ویژه وقتی بتوان آن را با زبان برنامه‌نویسی پایتون ساخت و همزمان تجربه‌ی عملی و پروژه‌ای ارزشمندی هم کسب کرد.

در این پروژه، از کتابخانه‌های گرافیکی و رابط کاربری در پایتون استفاده می‌کنیم تا تایمر را به‌شکلی جذاب و قابل استفاده بسازیم. طرح کلی برنامه شبیه یک گوجه‌فرنگی (نماد تکنیک پومودورو) طراحی شده و یک حلقه‌ی پیشرفت زمان نیز روی آن قرار گرفته است. برای این کار از customtkinter استفاده می‌کنیم که نسخه‌ای مدرن و زیباسازی شده از کتابخانه‌ی اصلی tkinter است.

معرفی کتابخانه‌ها

tkinter
این کتابخانه به‌صورت پیش‌فرض همراه با پایتون نصب می‌شود و برای ساخت رابط‌های گرافیکی دسکتاپ مورد استفاده قرار می‌گیرد. در این پروژه، بخش‌هایی مانند Canvas ، رسم اشکال، آرک زمان و هندسه‌ی UI با استفاده از tkinter انجام می‌شود.

customtkinter
این کتابخانه نسخه‌ای بهبود یافته و زیباتر از tkinter است که ظاهری مدرن، دکمه‌های جذاب‌تر، رنگ‌بندی حرفه‌ای و المان‌های به‌روزتر ارائه می‌دهد. برای ساخت رابط کاربری زیبا و مینیمال در این پروژه، استفاده از customtkinter بسیار مناسب است.

pip install customtkinter

time
کتابخانه‌ی time یکی از ابزارهای اساسی در کنترل وقفه‌ها، محاسبه‌ی زمان و مدیریت ثانیه‌هاست. در ساخت تایمر، نیاز داریم که هر ثانیه زمان کم شود و مقدار جدید در رابط کاربری نمایش داده شود. تابع sleep این کتابخانه وظیفه‌ی توقف موقت برنامه را بر عهده دارد.

threading
اگر تایمر را در همان Thread اصلی رابط کاربری اجرا کنیم، کل برنامه قفل می‌شود و دیگر هیچ عنصری قابل کلیک یا به‌روزرسانی نخواهد بود. برای جلوگیری از این اتفاق، یک Thread جداگانه برای تایمر تعریف می‌کنیم. این کار امکان اجرای روان و بدون هنگ‌کردن برنامه را فراهم می‌کند.

اهمیت پروژه برای یادگیری پایتون

ساخت تایمر پومودورو یک پروژه‌ی ساده اما بسیار کاربردی برای یادگیری GUI در پایتون است. برنامه‌نویسان مبتدی معمولاً به‌دنبال پروژه‌هایی هستند که هم کاربردی باشند و هم تجربه‌ی عملی مهمی به آن‌ها بدهند. این پروژه علاوه‌بر اینکه یک ابزار واقعی برای استفاده‌ی روزانه است، مهارت‌های زیر را به‌شکلی عملی تقویت می‌کند:

• نحوه‌ی رسم اشکال گرافیکی و تعامل با Canvas
• مدیریت زمان و محاسبه‌ی جریان اجرای ثانیه‌ها
• کنترل Threadها و جلوگیری از قفل‌شدن برنامه
• طراحی رابط کاربری زیبا با customtkinter
• به‌کارگیری منطق برنامه‌نویسی در یک پروژه‌ی واقعی

علاوه بر این، ظاهر گوجه‌فرنگی، حس جذابی به پروژه می‌دهد و باعث می‌شود نتیجه‌ی کار شبیه یک اپلیکیشن کاربردی واقعی باشد.

کد پروژه

"""
tomato_pomodoro_fixed.py
Pomodoro timer with a tomato-shaped UI using customtkinter and tkinter.Canvas.

Requirements:
    pip install customtkinter pillow

Run:
    python tomato_pomodoro_fixed.py
"""

import time
import tkinter as tk
from tkinter import messagebox
import customtkinter as ctk
from PIL import Image, ImageTk, ImageDraw, ImageFilter

# ---------------- CONFIG ----------------
WORK_MIN = 25
SHORT_BREAK_MIN = 5
LONG_BREAK_MIN = 15
POMODOROS_BEFORE_LONG = 4

TOMATO_RED = "#d62828"
HIGHLIGHT = "#ff7b7b"
LEAF_GREEN = "#2f9e44"
BG = "#0f1720"

def format_time(seconds):
    m = seconds // 60
    s = seconds % 60
    return f"{int(m):02d}:{int(s):02d}"

# ---------------- APP ----------------
class TomatoPomodoroApp(ctk.CTk):
    def __init__(self):
        super().__init__()

        self.title("Tomato Pomodoro 🍅")
        self.geometry("420x520")
        self.resizable(False, False)
        ctk.set_appearance_mode("dark")
        ctk.set_default_color_theme("dark-blue")

        self._running = False
        self._paused = False
        self._mode = "WORK"
        self._remaining = WORK_MIN * 60
        self._pomodoro_count = 0
        self._after_id = None

        # Canvas
        self.canvas = tk.Canvas(self, width=420, height=420, bg=BG, highlightthickness=0)
        self.canvas.pack(pady=10)

        # Draw tomato
        self._draw_tomato_base()

        # Timer
        self.timer_text = self.canvas.create_text(
            210, 200, text=format_time(self._remaining),
            font=("Montserrat", 36, "bold"), fill="white"
        )

        self.mode_text = self.canvas.create_text(
            210, 240, text="WORK", font=("Montserrat", 12), fill="#ffecec"
        )

        # Progress ring (NO capstyle!)
        self.ring_radius = 150
        self.ring_width = 18

        self.progress_arc = self.canvas.create_arc(
            210 - self.ring_radius,
            210 - self.ring_radius,
            210 + self.ring_radius,
            210 + self.ring_radius,
            start=90,
            extent=0,
            style="arc",
            width=self.ring_width,
            outline=HIGHLIGHT
        )

        # Controls
        controls = ctk.CTkFrame(self, fg_color="transparent")
        controls.pack(pady=8)

        self.start_btn = ctk.CTkButton(controls, text="Start", width=80, command=self.start)
        self.start_btn.grid(row=0, column=0, padx=5)

        self.pause_btn = ctk.CTkButton(controls, text="Pause", width=80, command=self.pause, state="disabled")
        self.pause_btn.grid(row=0, column=1, padx=5)

        self.reset_btn = ctk.CTkButton(controls, text="Reset", width=80, command=self.reset)
        self.reset_btn.grid(row=0, column=2, padx=5)

        # Presets
        preset = ctk.CTkSegmentedButton(self, values=["25/5", "15/5", "50/10"], command=self._preset)
        preset.pack(pady=6)
        preset.set("25/5")

        # Status
        self.status_label = ctk.CTkLabel(self, text="Pomodoros: 0")
        self.status_label.pack()

        self._draw_leafs()

    # ---------- UI drawing ----------
    def _draw_tomato_base(self):
        size = (420, 420)
        img = Image.new("RGBA", size, (0, 0, 0, 0))
        draw = ImageDraw.Draw(img)

        center = (210, 210)
        radius = 160

        # Gradient tomato body
        for i in range(radius, 0, -1):
            t = i / radius
            r = int(180 * t)
            g = int(30 * t)
            b = int(30 * t)
            draw.ellipse([center[0]-i, center[1]-i, center[0]+i, center[1]+i],
                         fill=(r, g, b, 255))

        # highlight
        highlight = Image.new("RGBA", size, (0, 0, 0, 0))
        hd = ImageDraw.Draw(highlight)
        hd.ellipse((110, 110, 300, 210), fill=(255, 180, 180, 70))
        highlight = highlight.filter(ImageFilter.GaussianBlur(8))
        img = Image.alpha_composite(img, highlight)

        # shadow
        shadow = Image.new("RGBA", size, (0, 0, 0, 0))
        sd = ImageDraw.Draw(shadow)
        sd.ellipse((130, 310, 290, 350), fill=(0, 0, 0, 120))
        shadow = shadow.filter(ImageFilter.GaussianBlur(10))
        img = Image.alpha_composite(shadow, img)

        self.tk_tomato_img = ImageTk.PhotoImage(img)
        self.canvas.create_image(0, 0, anchor="nw", image=self.tk_tomato_img)

    def _draw_leafs(self):
        self.canvas.create_polygon(
            (210, 80, 165, 40, 185, 90),
            fill=LEAF_GREEN, outline="#1f7a2a", smooth=True
        )
        self.canvas.create_polygon(
            (210, 75, 255, 40, 235, 90),
            fill="#3fbf66", outline="#1f7a2a", smooth=True
        )
        self.canvas.create_rectangle(200, 95, 220, 105, fill="#3b3b2b")

    # ---------- Timer logic ----------
    def start(self):
        if not self._running:
            self._running = True
            self._paused = False
            self.start_btn.configure(state="disabled")
            self.pause_btn.configure(state="normal")
            self._tick()
        elif self._paused:
            self._paused = False
            self.start_btn.configure(state="disabled")
            self.pause_btn.configure(state="normal")
            self._tick()

    def pause(self):
        self._paused = True
        self.start_btn.configure(state="normal")
        self.pause_btn.configure(state="disabled")
        if self._after_id:
            self.after_cancel(self._after_id)

    def reset(self):
        if self._after_id:
            self.after_cancel(self._after_id)

        self._running = False
        self._paused = False
        self._mode = "WORK"
        self._remaining = WORK_MIN * 60
        self._pomodoro_count = 0

        self._update_ui()
        self.start_btn.configure(state="normal")
        self.pause_btn.configure(state="disabled")

    # ---------- presets ----------
    def _preset(self, value):
        global WORK_MIN, SHORT_BREAK_MIN
        w, b = value.split("/")
        WORK_MIN = int(w)
        SHORT_BREAK_MIN = int(b)

        if not self._running:
            self._remaining = WORK_MIN * 60
            self._update_ui()

    # ---------- switching ----------
    def _switch_mode(self):
        if self._mode == "WORK":
            self._pomodoro_count += 1
            if self._pomodoro_count % POMODOROS_BEFORE_LONG == 0:
                self._mode = "LONG_BREAK"
                self._remaining = LONG_BREAK_MIN * 60
            else:
                self._mode = "SHORT_BREAK"
                self._remaining = SHORT_BREAK_MIN * 60
        else:
            self._mode = "WORK"
            self._remaining = WORK_MIN * 60

    # ---------- ticking ----------
    def _tick(self):
        if self._paused:
            return
        if self._remaining <= 0:
            messagebox.showinfo("Pomodoro", f"{self._mode} finished!")
            self._switch_mode()
            self._update_ui()
            self._remaining -= 1
            self._after_id = self.after(1000, self._tick)
            return

        self._update_ui()
        self._remaining -= 1
        self._after_id = self.after(1000, self._tick)

    # ---------- update UI ----------
    def _update_ui(self):
        self.canvas.itemconfigure(self.timer_text, text=format_time(self._remaining))
        mode_label = {
            "WORK": "WORK",
            "SHORT_BREAK": "BREAK",
            "LONG_BREAK": "LONG BREAK"
        }[self._mode]
        self.canvas.itemconfigure(self.mode_text, text=mode_label)

        self.status_label.configure(text=f"Pomodoros: {self._pomodoro_count}")

        # update progress arc
        total = {
            "WORK": WORK_MIN * 60,
            "SHORT_BREAK": SHORT_BREAK_MIN * 60,
            "LONG_BREAK": LONG_BREAK_MIN * 60
        }[self._mode]

        progress = (total - self._remaining) / total
        extent = progress * 360

        self.canvas.itemconfigure(self.progress_arc, extent=-extent)

        # color
        if self._mode == "WORK":
            self.canvas.itemconfigure(self.progress_arc, outline=HIGHLIGHT)
        else:
            self.canvas.itemconfigure(self.progress_arc, outline="#8be58b")

# ---------------- RUN ----------------
if __name__ == "__main__":
    app = TomatoPomodoroApp()
    app.mainloop()

نکات مهم کد

یکی از مهم‌ترین قسمت‌های این پروژه، ساخت رابط کاربری با استفاده از ترکیب tkinter و customtkinter است. هرکدام از این کتابخانه‌ها مزایای مخصوص خود را دارند. tkinter برای رسم اشکال و کارهای سطح پایین Canvas مناسب است، در حالی که customtkinter در طراحی دکمه‌ها و نوشته‌های گرافیکی زیباتر عمل می‌کند. این ترکیب در بسیاری از پروژه‌های GUI قابل استفاده است.

در ابتدای کد، رنگ‌ها به‌صورت مقادیر HEX تعریف شده‌اند. این کار باعث می‌شود ساختار کد تمیزتر شود و در صورت نیاز به تغییر رنگ‌ها یا ایجاد تم تیره و روشن، تنها بخش کوچکی از کد اصلاح شود. مدیریت رنگ‌ها یکی از نکات حرفه‌ای در ساخت رابط کاربری است که باید از همان ابتدا رعایت شود.

یکی از چالش‌های اصلی ساخت تایمر در برنامه‌های گرافیکی، جلوگیری از قفل شدن رابط کاربری است. اگر کاهش ثانیه‌ها را در همان Thread اصلی اجرا کنیم، پنجره دیگر پاسخ‌گو نخواهد بود، دکمه‌ها کار نمی‌کنند و حتی اگر پنجره را ببندیم سیستم منتظر پایان حلقه زمان می‌ماند. با استفاده از Threading، تایمر در یک مسیر جداگانه اجرا می‌شود و UI همیشه روان می‌ماند. این موضوع در همه پروژه‌های زمان‌محور مثل کرنومتر، شمارش معکوس، یا بازی‌ها اهمیت دارد.

در بخش رسم گوجه‌فرنگی، از ترکیب دایره، بیضی و مستطیل استفاده شده است. این طراحی کاملاً قابل تغییر و توسعه است. می‌توانید سایه‌های بیشتری اضافه کنید، برق سطح گوجه را با اشکال سفید پیاده‌سازی کنید یا حتی برگ‌های اضافه داشته باشید. قدرت Canvas در tkinter این امکان را به شما می‌دهد که ظاهر برنامه را کاملاً اختصاصی طراحی کنید.

حلقه‌ی نمایش مقدار زمان با استفاده از دستور create_arc ساخته شده است. از آنجا که نسخه‌های جدید tkinter از capstyle پشتیبانی نمی‌کنند، arc با لبه‌های ساده رسم شده است. در صورت نیاز می‌توانید با ترفندهایی مثل رسم دو دایره کوچک روی ابتدا و انتهای arc، شکل آن را طبیعی‌تر کنید. کاهش یا افزایش مقدار extent باعث می‌شود حلقه از صفر به ۳۶۰ درجه برسد و میزان پیشرفت زمان را نشان دهد.

در بخش تایمر، محاسبات بسیار ساده هستند اما کاملاً دقیق اجرا شده‌اند. ثانیه به ثانیه مقدار زمان کم می‌شود و متن برچسب زمان نیز هر بار با مقدار جدید مقداردهی می‌شود. همچنین میزان پیشرفت بر اساس نسبت زمان باقی‌مانده به زمان اولیه محاسبه می‌شود. این بخش می‌تواند برای توسعه‌های بعدی پایه قرار بگیرد؛ مثلاً اضافه کردن حالت‌های مختلف پومودورو (تایمر کوتاه، تایمر بلند، تایمر جلسه کاری و…).

نتیجه‌گیری

پروژه ساخت تایمر پومودورو با پایتون و customtkinter یک نمونه کاربردی، آموزشی و ساده از پیاده‌سازی رابط کاربری در پایتون است. این پروژه با ترکیب طراحی زیبا، منطق برنامه‌نویسی و مفاهیم مدیریت زمان، به‌عنوان یک تمرین ارزشمند برای علاقه‌مندان به حوزه‌ی توسعه نرم‌افزار شناخته می‌شود. ساخت چنین پروژه‌ای به شما کمک می‌کند که مهارت‌های کار با Canvas، مدیریت Threadها، استفاده از کتابخانه‌های گرافیکی و بهینه‌سازی UI را در عمل تجربه کنید. از طرف دیگر، خروجی این پروژه یک ابزار مفید برای بهبود بهره‌وری شخصی است؛ تایمری که می‌توانید واقعاً هنگام مطالعه یا کار از آن استفاده کنید.

در نهایت، این پروژه می‌تواند نقطه آغاز خوبی برای ورود به ساخت نرم‌افزارهای دسکتاپ با پایتون باشد. شما می‌توانید ویژگی‌هایی مانند اعلان پایان زمان، حالت تاریک (Dark Mode)، ذخیره‌سازی تعداد جلسات پومودورو، صداهای هشدار، و حتی گزارش عملکرد روزانه را به برنامه اضافه کنید. با این توسعه‌ها، پروژه از یک تایمر ساده به یک ابزار حرفه‌ای مدیریت زمان تبدیل می‌شود.