حالت آفلاین — محتوا از حافظهٔ کش نمایش داده می‌شود
آموزش پایتون پروژه‌محور؛ قسمت سوم: شیءگرایی تا ساخت برنامه

پروژه مدیریت کارها با پایتون، Tkinter و JSON

پروژه مدیریت کارها با پایتون و ساخت فایل اجرایی · 0٪

پروژه روی کامپیوتر خودت اجرا می‌شه و باید دستی بررسیش کنی؛ این کد رو در مرورگر اجرا نکن. کد زیر رو در فایل tasks_app.py ذخیره کن و با پایتون محیط مجازی اجراش کن. هر بخش یک کار مشخص داره:

  • Task عنوان رو بررسی می‌کنه و اطلاعات یک کار رو نگه می‌داره.
  • TaskManager کارها رو می‌خونه، تغییر می‌ده و ذخیره می‌کنه.
  • تابع‌های دکمه‌ها یا callbackها، ورودی پنجره رو به این بخش‌ها می‌دن و نمایش رو به‌روز می‌کنن.

داده در codetamrin_tasks.json در پوشهٔ خانگی کاربر ذخیره می‌شه. برنامهٔ اولیه هم در حالت فایل .py و هم در بستهٔ اجرایی، مسیر قابل نوشتن و ثابتی برای داده داره. در اولین اجرا نبود فایل طبیعیه؛ اما فایل خراب یا ساختار نامعتبر باعث پیام خطا و پایان برنامه می‌شه تا دادهٔ قبلی با لیست خالی جایگزین نشه.

import json
from pathlib import Path
import tkinter as tk
from tkinter import messagebox


class Task:
    def __init__(self, title, done=False):
        self.title = title
        self.done = done

    @property
    def title(self):
        return self._title

    @title.setter
    def title(self, value):
        if not isinstance(value, str) or not value.strip():
            raise ValueError("عنوان نامعتبر است")
        self._title = value.strip()

    def record(self):
        return {"title": self.title, "done": self.done}


class TaskManager:
    def __init__(self, path):
        self.path = Path(path)
        self.tasks = []

    def load(self):
        try:
            with self.path.open("r", encoding="utf-8") as file:
                rows = json.load(file)
        except FileNotFoundError:
            return  # اولین اجرا؛ هنوز فایلی نداریم.
        if not isinstance(rows, list):
            raise ValueError("ساختار فایل باید فهرست باشد")
        loaded = []
        for row in rows:
            if (not isinstance(row, dict)
                    or not isinstance(row.get("title"), str)
                    or type(row.get("done")) is not bool):
                raise ValueError("ساختار یکی از کارها نامعتبر است")
            loaded.append(Task(row["title"], row["done"]))
        self.tasks = loaded

    def save(self):
        # اول فایل موقت را کامل می‌نویسیم، بعد جایگزین می‌کنیم.
        temporary = self.path.with_suffix(self.path.suffix + ".tmp")
        with temporary.open("w", encoding="utf-8") as file:
            json.dump([task.record() for task in self.tasks], file,
                      ensure_ascii=False, indent=2)
        temporary.replace(self.path)

    def add(self, title):
        task = Task(title)
        self.tasks.append(task)
        try:
            self.save()
        except OSError:
            self.tasks.pop()
            raise

    def complete(self, index):
        old = self.tasks[index].done
        self.tasks[index].done = True
        try:
            self.save()
        except OSError:
            self.tasks[index].done = old
            raise


def main():
    root = tk.Tk()
    root.title("کارهای من")
    # داده کنار مفسر یا فایل exe ذخیره نمی‌شود؛ پوشهٔ خانگی کاربر است.
    manager = TaskManager(Path.home() / "codetamrin_tasks.json")
    try:
        manager.load()
    except (OSError, UnicodeError, ValueError) as error:
        messagebox.showerror("خواندن فایل", str(error), parent=root)
        root.destroy()
        return  # فایل خراب را با دادهٔ خالی بازنویسی نمی‌کنیم.

    entry = tk.Entry(root, width=40)
    entry.pack(padx=12, pady=8)
    items = tk.Listbox(root, width=50)
    items.pack(padx=12, pady=8)

    def refresh():
        items.delete(0, tk.END)
        for task in manager.tasks:
            mark = "✓" if task.done else "○"
            items.insert(tk.END, f"{mark} {task.title}")

    def add_task():
        try:
            manager.add(entry.get())
        except (OSError, ValueError) as error:
            messagebox.showerror("افزودن انجام نشد", str(error), parent=root)
            return
        entry.delete(0, tk.END)
        refresh()

    def complete_task():
        selected = items.curselection()
        if not selected:
            messagebox.showinfo("انتخاب کار", "ابتدا یک کار را انتخاب کن", parent=root)
            return
        try:
            manager.complete(selected[0])
        except OSError as error:
            messagebox.showerror("ذخیره انجام نشد", str(error), parent=root)
            return
        refresh()

    tk.Button(root, text="افزودن", command=add_task).pack(pady=3)
    tk.Button(root, text="انجام شد", command=complete_task).pack(pady=3)
    refresh()
    root.mainloop()


if __name__ == "__main__":
    main()

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

آزمون دستی رو به ترتیب انجام بده: ۱) یک عنوان فارسی اضافه کن و برنامه رو ببند و باز کن؛ ۲) اون رو انجام‌شده کن و دوباره باز کن؛ ۳) عنوان خالی رو امتحان کن؛ ۴) بدون انتخاب کار روی «انجام شد» بزن؛ ۵) بعد از تهیهٔ کپی پشتیبان از JSON، با فایل آزمایشی خراب و ساختار اشتباه رفتار خطا رو ببین. برای این آزمون‌ها دادهٔ مهم شخصی استفاده نکن. در این نسخه می‌تونی کار اضافه کنی و انجام‌شدنش رو ثبت کنی. برای تمرین بیشتر، بعداً حذف، ویرایش و جست‌وجو رو هم اضافه کن.

فعالیت شما

اجزای آموخته‌شده رو در یک برنامهٔ محلی کامل و قابل اجرای دوباره کنار هم قرار بده.

مطالعهٔ این گام برای همه آزاد است

برای پاسخ‌دادن، بررسی پاسخ، ثبت پیشرفت و استفاده از دستیار باید شرایط شرکت در فعالیت‌ها را داشته باشید.

برای انجام فعالیت وارد شوید

فعالیت‌های این بخش به اشتراک فعال نیاز دارند. تهیهٔ اشتراک

برای انجام فعالیت‌ها ابتدا این دوره‌های پیش‌نیاز را تکمیل کنید: