ساخت برنامه دسکتاپ تبدیل تاریخ شمسی و میلادی با Python و CustomTkinter

آموزش جامع ساخت تقویم شمسی با رابط گرافیکی، مدیریت رویدادها و ذخیره‌سازی داده‌ها با توضیحات کامل هر بخش

این مطلب از سایت tool.hamidvalad.ir گرفته شده است

معرفی پروژه

در این آموزش، یک برنامه تقویم شمسی کامل با رابط گرافیکی می‌سازیم که قابلیت‌های زیر را دارد:

  • نمایش تاریخ‌های شمسی با کتابخانه jdatetime
  • رابط کاربری فارسی با tkinter
  • مدیریت و ذخیره رویدادهای شخصی
  • نمایش تعطیلات رسمی ایران
  • امکان جابجایی بین ماه‌ها و سال‌های مختلف
  • ذخیره‌سازی داده‌ها در فایل JSON

توضیحات بخش‌های کد

1. وارد کردن کتابخانه‌های مورد نیاز

این بخش شامل کتابخانه‌های اصلی مورد نیاز برای برنامه است:

import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import jdatetime
from tkinter import font as tkfont
import json
import os
  • tkinter: برای ایجاد رابط گرافیکی
  • jdatetime: برای کار با تاریخ‌های شمسی
  • json: برای ذخیره‌سازی رویدادها
  • os: برای کار با فایل‌ها و مسیرها

2. تعریف کلاس اصلی PersianCalendar

کلاس PersianCalendar هسته اصلی برنامه را تشکیل می‌دهد:

class PersianCalendar:
    def __init__(self, root):
        self.root = root
        self.root.title("تقویم شمسی")
        self.root.geometry("800x700")
        self.root.configure(bg='#f0f0f0')

در سازنده کلاس، پنجره اصلی برنامه ایجاد می‌شود و مشخصات اولیه مانند عنوان، ابعاد و رنگ پس‌زمینه تنظیم می‌شود.

3. تنظیم فونت فارسی

        try:
            self.farsi_font = tkfont.Font(family="B Nazanin", size=12)
            self.farsi_font_bold = tkfont.Font(family="B Nazanin", size=12, weight="bold")
            self.farsi_font_large = tkfont.Font(family="B Nazanin", size=14, weight="bold")
        except:
            self.farsi_font = tkfont.Font(size=12)
            self.farsi_font_bold = tkfont.Font(size=12, weight="bold")
            self.farsi_font_large = tkfont.Font(size=14, weight="bold")

این بخش سعی می‌کند از فونت فارسی "B Nazanin" استفاده کند. اگر فونت نصب نباشد، از فونت پیش‌فرض سیستم استفاده می‌شود.

4. تعریف تاریخ فعلی و متغیرهای کلاس

        self.current_date = jdatetime.date.today()
        self.current_year = self.current_date.year
        self.current_month = self.current_date.month

تاریخ امروز شمسی دریافت شده و سال و ماه جاری ذخیره می‌شوند.

5. تعریف تعطیلات رسمی ایران

        self.holidays = {
            (1, 1): "عید نوروز",
            (1, 2): "عید نوروز",
            (1, 3): "عید نوروز",
            (1, 4): "عید نوروز",
            (1, 12): "روز جمهوری اسلامی",
            (1, 13): "روز طبیعت",
            (3, 14): "رحلت امام خمینی",
            (3, 15): "قیام ۱۵ خرداد",
            (11, 22): "پیروزی انقلاب اسلامی",
            (12, 29): "روز ملی شدن صنعت نفت"
        }

یک دیکشنری برای ذخیره تعطیلات ثابت ایران تعریف شده است که هر کلید شامل (ماه, روز) و مقدار آن نام تعطیل است.

6. نام ماه‌ها و روزهای هفته

        self.persian_months = [
            "فروردین", "اردیبهشت", "خرداد", 
            "تیر", "مرداد", "شهریور", 
            "مهر", "آبان", "آذر", 
            "دی", "بهمن", "اسفند"
        ]
        
        self.persian_weekdays = ["ش", "ی", "د", "س", "چ", "پ", "ج"]

لیست‌هایی برای نام ماه‌های شمسی و حروف اختصاری روزهای هفته تعریف شده‌اند.

7. سیستم مدیریت رویدادها

        self.events = {}
        self.load_events()

یک دیکشنری برای ذخیره رویدادها و تابعی برای بارگیری رویدادهای ذخیره شده از فایل.

8. متد setup_ui - ایجاد رابط کاربری

این متد تمام اجزای رابط کاربری را ایجاد می‌کند:

  • هدر نمایش ماه و سال
  • دکمه‌های کنترل (ماه قبل، ماه بعد، امروز)
  • انتخاب‌گر سال و ماه
  • دکمه مدیریت رویدادها
  • فریم اصلی برای نمایش روزها

9. متد display_month_days - نمایش روزهای ماه

این متد پیچیده‌ترین بخش برنامه است و وظایف زیر را انجام می‌دهد:

  • محاسبه اولین روز ماه و روز هفته آن
  • تعیین تعداد روزهای ماه (با توجه به ماه‌های ۳۱، ۳۰ روزه و اسفند در سال کبیسه)
  • ایجاد فریم برای هر روز با رنگ‌بندی مناسب:
    • روز جاری: آبی
    • تعطیلات: قرمز
    • جمعه: نارنجی روشن
    • روزهای دارای رویداد: سبز بسیار روشن
    • روزهای عادی: سفید
  • افزودن نشانگر رویداد (نقطه سبز) برای روزهای دارای رویداد
  • اتصال رویدادهای کلیک و دابل‌کلیک به هر روز

10. متد is_holiday - تشخیص تعطیل بودن روز

    def is_holiday(self, date_obj):
        if (date_obj.month, date_obj.day) in self.holidays:
            return True
        
        weekday = date_obj.weekday()
        if weekday == 6:
            return True
        
        return False

این تابع بررسی می‌کند که آیا یک روز خاص تعطیل است یا خیر. تعطیلات شامل:

  • تعطیلات ثابت تعریف شده در دیکشنری holidays
  • روزهای جمعه (شماره 6 در jdatetime)

11. متد add_edit_event - مدیریت رویدادها

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

  • ایجاد پنجره dialog با عنوان مناسب
  • جعبه متن برای وارد کردن توضیحات رویداد
  • دکمه‌های ذخیره، حذف و انصراف
  • ذخیره خودکار رویدادها پس از تغییر

12. متد show_events_manager - نمایش تمام رویدادها

این متد پنجره‌ای برای مدیریت همه رویدادهای ثبت شده ایجاد می‌کند:

  • نمایش لیست تمام رویدادها به ترتیب تاریخ
  • قابلیت حذف رویدادها
  • اسکرول برای دیدن رویدادهای زیاد

13. متدهای save_events و load_events - ذخیره و بازیابی

    def save_events(self):
        try:
            with open('persian_calendar_events.json', 'w', encoding='utf-8') as f:
                json.dump(self.events, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"خطا در ذخیره رویدادها: {e}")
    
    def load_events(self):
        try:
            if os.path.exists('persian_calendar_events.json'):
                with open('persian_calendar_events.json', 'r', encoding='utf-8') as f:
                    self.events = json.load(f)
        except Exception as e:
            print(f"خطا در بارگیری رویدادها: {e}")
            self.events = {}

این توابع مسئول ذخیره و بارگیری رویدادها در فایل JSON هستند.

14. متدهای ناوبری بین ماه‌ها

    def prev_month(self):
        self.current_month -= 1
        if self.current_month < 1:
            self.current_month = 12
            self.current_year -= 1
        
        self.update_calendar()
    
    def next_month(self):
        self.current_month += 1
        if self.current_month > 12:
            self.current_month = 1
            self.current_year += 1
        
        self.update_calendar()

این توابع امکان جابجایی بین ماه‌های مختلف را فراهم می‌کنند.

15. متد update_calendar - به‌روزرسانی نمایش

این متد پس از هر تغییر (تغییر ماه، افزودن رویداد و ...) فراخوانی شده و تقویم را به‌روز می‌کند.

16. تابع main و نقطه شروع برنامه

def main():
    root = tk.Tk()
    app = PersianCalendar(root)
    root.mainloop()

if __name__ == "__main__":
    main()

تابع main نقطه شروع برنامه است که پنجره اصلی tkinter را ایجاد و برنامه را اجرا می‌کند.

ساختار فایل کد

برای اجرای برنامه، کد کامل را در یک فایل با نام persian_calendar.py ذخیره کنید:

import tkinter as tk
from tkinter import ttk, messagebox, simpledialog
import jdatetime
from tkinter import font as tkfont
import json
import os

class PersianCalendar:
    def __init__(self, root):
        self.root = root
        self.root.title("تقویم شمسی")
        self.root.geometry("800x700")
        self.root.configure(bg='#f0f0f0')
        
        # تنظیم فونت فارسی
        try:
            self.farsi_font = tkfont.Font(family="B Nazanin", size=12)
            self.farsi_font_bold = tkfont.Font(family="B Nazanin", size=12, weight="bold")
            self.farsi_font_large = tkfont.Font(family="B Nazanin", size=14, weight="bold")
        except:
            # اگر فونت فارسی نصب نبود از فونت پیش‌فرض استفاده می‌شود
            self.farsi_font = tkfont.Font(size=12)
            self.farsi_font_bold = tkfont.Font(size=12, weight="bold")
            self.farsi_font_large = tkfont.Font(size=14, weight="bold")
        
        # تاریخ فعلی
        self.current_date = jdatetime.date.today()
        self.current_year = self.current_date.year
        self.current_month = self.current_date.month
        
        # لیست تعطیلات رسمی ایران (ثابت - به صورت ماه/روز)
        self.holidays = {
            (1, 1): "عید نوروز",
            (1, 2): "عید نوروز",
            (1, 3): "عید نوروز",
            (1, 4): "عید نوروز",
            (1, 12): "روز جمهوری اسلامی",
            (1, 13): "روز طبیعت",
            (3, 14): "رحلت امام خمینی",
            (3, 15): "قیام ۱۵ خرداد",
            (11, 22): "پیروزی انقلاب اسلامی",
            (12, 29): "روز ملی شدن صنعت نفت"
        }
        
        # نام ماه‌های شمسی
        self.persian_months = [
            "فروردین", "اردیبهشت", "خرداد", 
            "تیر", "مرداد", "شهریور", 
            "مهر", "آبان", "آذر", 
            "دی", "بهمن", "اسفند"
        ]
        
        # نام روزهای هفته
        self.persian_weekdays = ["ش", "ی", "د", "س", "چ", "پ", "ج"]
        
        # دیکشنری برای ذخیره رویدادها
        self.events = {}
        
        # بارگیری رویدادهای ذخیره شده
        self.load_events()
        
        self.setup_ui()
        self.update_calendar()
    
    def setup_ui(self):
        # هدر تقویم
        header_frame = tk.Frame(self.root, bg='#2c3e50', height=80)
        header_frame.pack(fill=tk.X, padx=10, pady=(10, 5))
        
        # دکمه‌های کنترل
        control_frame = tk.Frame(self.root, bg='#f0f0f0')
        control_frame.pack(fill=tk.X, padx=10, pady=5)
        
        # دکمه ماه قبل
        self.prev_month_btn = tk.Button(
            control_frame, text="< ماه قبل", 
            command=self.prev_month,
            bg='#3498db', fg='white', font=self.farsi_font_bold,
            padx=15, pady=5
        )
        self.prev_month_btn.pack(side=tk.RIGHT, padx=5)
        
        # دکمه ماه بعد
        self.next_month_btn = tk.Button(
            control_frame, text="ماه بعد >", 
            command=self.next_month,
            bg='#3498db', fg='white', font=self.farsi_font_bold,
            padx=15, pady=5
        )
        self.next_month_btn.pack(side=tk.RIGHT, padx=5)
        
        # نمایش ماه و سال
        self.month_year_label = tk.Label(
            header_frame, 
            text="", 
            font=self.farsi_font_large,
            bg='#2c3e50', fg='white'
        )
        self.month_year_label.pack(expand=True)
        
        # دکمه برگشت به امروز
        self.today_btn = tk.Button(
            control_frame, text="امروز", 
            command=self.go_to_today,
            bg='#2ecc71', fg='white', font=self.farsi_font_bold,
            padx=20, pady=5
        )
        self.today_btn.pack(side=tk.RIGHT, padx=5)
        
        # انتخاب‌گر سال و ماه
        selector_frame = tk.Frame(control_frame, bg='#f0f0f0')
        selector_frame.pack(side=tk.LEFT, padx=10)
        
        tk.Label(selector_frame, text="سال:", font=self.farsi_font, bg='#f0f0f0').pack(side=tk.LEFT, padx=(0, 5))
        
        self.year_var = tk.StringVar()
        self.year_spinbox = tk.Spinbox(
            selector_frame, 
            from_=1300, 
            to=1500, 
            textvariable=self.year_var,
            width=8,
            font=self.farsi_font,
            justify='center'
        )
        self.year_spinbox.pack(side=tk.LEFT, padx=5)
        
        tk.Label(selector_frame, text="ماه:", font=self.farsi_font, bg='#f0f0f0').pack(side=tk.LEFT, padx=(10, 5))
        
        self.month_var = tk.StringVar()
        self.month_combobox = ttk.Combobox(
            selector_frame, 
            textvariable=self.month_var,
            values=self.persian_months,
            state="readonly",
            width=10,
            font=self.farsi_font
        )
        self.month_combobox.pack(side=tk.LEFT, padx=5)
        
        # دکمه اعمال تغییرات
        self.apply_btn = tk.Button(
            selector_frame, text="اعمال", 
            command=self.apply_date_change,
            bg='#e67e22', fg='white', font=self.farsi_font,
            padx=15, pady=2
        )
        self.apply_btn.pack(side=tk.LEFT, padx=10)
        
        # دکمه مدیریت رویدادها
        self.events_btn = tk.Button(
            control_frame, text="مدیریت رویدادها", 
            command=self.show_events_manager,
            bg='#9b59b6', fg='white', font=self.farsi_font_bold,
            padx=15, pady=5
        )
        self.events_btn.pack(side=tk.LEFT, padx=10)
        
        # فریم تقویم
        self.calendar_frame = tk.Frame(self.root, bg='#ecf0f1')
        self.calendar_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=(5, 10))
        
        # ایجاد هدر روزهای هفته
        self.create_weekday_headers()
    
    def create_weekday_headers(self):
        # پاک کردن ویجت‌های قدیمی
        for widget in self.calendar_frame.winfo_children():
            widget.destroy()
        
        # ایجاد هدر روزهای هفته
        for i, day_name in enumerate(self.persian_weekdays):
            label = tk.Label(
                self.calendar_frame, 
                text=day_name, 
                font=self.farsi_font_bold,
                bg='#34495e', 
                fg='white',
                width=12, height=2,
                relief=tk.RAISED
            )
            label.grid(row=0, column=i, sticky="nsew", padx=1, pady=1)
    
    def update_calendar(self):
        # به‌روزرسانی نمایش ماه و سال
        month_name = self.persian_months[self.current_month - 1]
        self.month_year_label.config(text=f"{month_name} {self.current_year}")
        
        # به‌روزرسانی انتخاب‌گرها
        self.year_var.set(self.current_year)
        self.month_combobox.set(month_name)
        
        # محاسبه روزهای ماه
        self.display_month_days()
    
    def display_month_days(self):
        # پاک کردن روزهای قبلی (به جز هدر)
        for widget in self.calendar_frame.winfo_children()[7:]:
            widget.destroy()
        
        # محاسبه اولین روز ماه
        first_day_of_month = jdatetime.date(self.current_year, self.current_month, 1)
        
        # پیدا کردن روز هفته برای اولین روز ماه (شنبه=0)
        first_weekday = first_day_of_month.weekday()
        
        # محاسبه تعداد روزهای ماه
        if self.current_month <= 6:
            days_in_month = 31
        elif self.current_month <= 11:
            days_in_month = 30
        else:  # اسفند
            # بررسی سال کبیسه
            if jdatetime.date(self.current_year, 12, 30).month == 12:
                days_in_month = 30
            else:
                days_in_month = 29
        
        # نمایش روزهای ماه
        row, col = 1, first_weekday
        today = jdatetime.date.today()
        
        for day in range(1, days_in_month + 1):
            # ایجاد برچسب برای هر روز
            date_obj = jdatetime.date(self.current_year, self.current_month, day)
            weekday = date_obj.weekday()
            
            # بررسی تعطیل بودن روز
            is_holiday = self.is_holiday(date_obj)
            is_today = (date_obj == today)
            
            # بررسی وجود رویداد برای این روز
            event_key = f"{self.current_year}-{self.current_month}-{day}"
            has_event = event_key in self.events
            
            # تعیین رنگ و استایل
            if is_today:
                bg_color = '#3498db'
                fg_color = 'white'
                border = tk.SOLID
                border_width = 2
            elif is_holiday:
                bg_color = '#ffebee'
                fg_color = '#e74c3c'
                border = tk.SOLID
                border_width = 1
            elif weekday == 6:  # جمعه
                bg_color = '#f5f5f5'
                fg_color = '#e74c3c'
                border = tk.SOLID
                border_width = 1
            elif has_event:
                bg_color = '#e8f5e8'  # سبز بسیار روشن برای روزهای دارای رویداد
                fg_color = 'black'
                border = tk.SOLID
                border_width = 1
            else:
                bg_color = 'white'
                fg_color = 'black'
                border = tk.SOLID
                border_width = 1
            
            # ایجاد فریم برای هر روز (برای قرار دادن چند ویجت)
            day_frame = tk.Frame(
                self.calendar_frame,
                bg=bg_color,
                relief=border,
                borderwidth=border_width
            )
            day_frame.grid(row=row, column=col, sticky="nsew", padx=1, pady=1)
            
            # برچسب شماره روز
            day_label = tk.Label(
                day_frame,
                text=str(day),
                font=self.farsi_font_bold,
                bg=bg_color,
                fg=fg_color,
                width=4,
                height=2
            )
            day_label.pack(side=tk.TOP, anchor=tk.NW, padx=2, pady=2)
            
            # نشانگر رویداد (اگر روز رویداد داشته باشد)
            if has_event:
                event_indicator = tk.Label(
                    day_frame,
                    text="●",
                    font=("Arial", 10, "bold"),
                    bg=bg_color,
                    fg='#27ae60'  # سبز تیره
                )
                event_indicator.place(relx=0.8, rely=0.1, anchor=tk.NE)
            
            # اضافه کردن رویداد برای نمایش جزئیات (کلیک چپ)
            day_frame.bind("<Button-1>", lambda e, d=day: self.show_day_details(d))
            day_label.bind("<Button-1>", lambda e, d=day: self.show_day_details(d))
            
            # اضافه کردن رویداد برای افزودن/ویرایش رویداد (کلیک راست یا دابل کلیک)
            day_frame.bind("<Double-Button-1>", lambda e, d=day: self.add_edit_event(d))
            day_frame.bind("<Button-3>", lambda e, d=day: self.add_edit_event(d))
            
            # افکت hover
            day_frame.bind("<Enter>", lambda e, f=day_frame, bg=bg_color: f.config(bg='#e3f2fd'))
            day_frame.bind("<Leave>", lambda e, f=day_frame, bg=bg_color: f.config(bg=bg_color))
            
            # به‌روزرسانی موقعیت
            col += 1
            if col > 6:
                col = 0
                row += 1
        
        # تنظیم وزن سطرها و ستون‌ها برای گسترش
        for i in range(row + 1):
            self.calendar_frame.rowconfigure(i, weight=1)
        for i in range(7):
            self.calendar_frame.columnconfigure(i, weight=1)
    
    def is_holiday(self, date_obj):
        # بررسی تعطیلات ثابت
        if (date_obj.month, date_obj.day) in self.holidays:
            return True
        
        # فقط جمعه تعطیل است (پنجشنبه حذف شده)
        weekday = date_obj.weekday()
        if weekday == 6:  # جمعه
            return True
        
        return False
    
    def get_holiday_name(self, date_obj):
        # دریافت نام تعطیلی
        return self.holidays.get((date_obj.month, date_obj.day), "")
    
    def show_day_details(self, day):
        # نمایش جزئیات روز
        date_obj = jdatetime.date(self.current_year, self.current_month, day)
        
        # تبدیل به میلادی برای نمایش
        gregorian_date = date_obj.togregorian()
        
        # نام روز هفته
        weekdays_fa = ["شنبه", "یکشنبه", "دوشنبه", "سه‌شنبه", "چهارشنبه", "پنجشنبه", "جمعه"]
        weekday_name = weekdays_fa[date_obj.weekday()]
        
        # نام ماه
        month_name = self.persian_months[date_obj.month - 1]
        
        # بررسی تعطیل بودن
        is_holiday = self.is_holiday(date_obj)
        holiday_info = ""
        if is_holiday:
            holiday_name = self.get_holiday_name(date_obj)
            if holiday_name:
                holiday_info = f"\nتعطیل رسمی: {holiday_name}"
            else:
                holiday_info = "\nتعطیل (جمعه)"
        
        # بررسی وجود رویداد
        event_key = f"{self.current_year}-{self.current_month}-{day}"
        event_info = ""
        if event_key in self.events:
            event_info = f"\nرویداد: {self.events[event_key]}"
        
        # ایجاد پیام
        message = f"{weekday_name}، {day} {month_name} {self.current_year}\n"
        message += f"میلادی: {gregorian_date.strftime('%Y/%m/%d')}"
        message += holiday_info
        message += event_info
        
        # نمایش پیام
        messagebox.showinfo("جزئیات روز", message)
    
    def add_edit_event(self, day):
        # افزودن یا ویرایش رویداد برای روز مشخص
        event_key = f"{self.current_year}-{self.current_month}-{day}"
        current_event = self.events.get(event_key, "")
        
        # پنجره برای ورود رویداد
        event_dialog = tk.Toplevel(self.root)
        event_dialog.title(f"رویداد برای {day} {self.persian_months[self.current_month-1]} {self.current_year}")
        event_dialog.geometry("500x300")
        event_dialog.configure(bg='#f0f0f0')
        event_dialog.resizable(False, False)
        
        # مرکز پنجره
        event_dialog.transient(self.root)
        event_dialog.grab_set()
        
        # عنوان
        title_label = tk.Label(
            event_dialog,
            text=f"رویداد برای {day} {self.persian_months[self.current_month-1]} {self.current_year}",
            font=self.farsi_font_bold,
            bg='#f0f0f0',
            fg='#2c3e50'
        )
        title_label.pack(pady=15)
        
        # جعبه متن برای ورود رویداد
        event_text = tk.Text(
            event_dialog,
            height=8,
            width=50,
            font=self.farsi_font,
            wrap=tk.WORD
        )
        event_text.pack(padx=20, pady=10, fill=tk.BOTH, expand=True)
        event_text.insert("1.0", current_event)
        
        # فریم برای دکمه‌ها
        button_frame = tk.Frame(event_dialog, bg='#f0f0f0')
        button_frame.pack(pady=10)
        
        # تابع ذخیره رویداد
        def save_event():
            new_event = event_text.get("1.0", "end-1c").strip()
            if new_event:
                self.events[event_key] = new_event
            else:
                # اگر رویداد خالی باشد، آن را حذف می‌کنیم
                if event_key in self.events:
                    del self.events[event_key]
            
            self.save_events()
            self.update_calendar()
            event_dialog.destroy()
            messagebox.showinfo("موفق", "رویداد با موفقیت ذخیره شد.")
        
        # تابع حذف رویداد
        def delete_event():
            if event_key in self.events:
                del self.events[event_key]
                self.save_events()
                self.update_calendar()
                event_dialog.destroy()
                messagebox.showinfo("موفق", "رویداد با موفقیت حذف شد.")
            else:
                messagebox.showinfo("اطلاع", "رویدادی برای حذف وجود ندارد.")
        
        # دکمه ذخیره
        save_button = tk.Button(
            button_frame,
            text="ذخیره رویداد",
            command=save_event,
            bg='#27ae60',
            fg='white',
            font=self.farsi_font_bold,
            padx=15,
            pady=5
        )
        save_button.pack(side=tk.LEFT, padx=10)
        
        # دکمه حذف
        delete_button = tk.Button(
            button_frame,
            text="حذف رویداد",
            command=delete_event,
            bg='#e74c3c',
            fg='white',
            font=self.farsi_font_bold,
            padx=15,
            pady=5
        )
        delete_button.pack(side=tk.LEFT, padx=10)
        
        # دکمه انصراف
        cancel_button = tk.Button(
            button_frame,
            text="انصراف",
            command=event_dialog.destroy,
            bg='#95a5a6',
            fg='white',
            font=self.farsi_font_bold,
            padx=15,
            pady=5
        )
        cancel_button.pack(side=tk.LEFT, padx=10)
    
    def show_events_manager(self):
        # نمایش مدیریت رویدادها
        events_window = tk.Toplevel(self.root)
        events_window.title("مدیریت رویدادها")
        events_window.geometry("600x500")
        events_window.configure(bg='#f0f0f0')
        
        # عنوان
        title_label = tk.Label(
            events_window,
            text="رویدادهای ثبت شده",
            font=self.farsi_font_large,
            bg='#f0f0f0',
            fg='#2c3e50'
        )
        title_label.pack(pady=15)
        
        # فریم برای لیست رویدادها
        list_frame = tk.Frame(events_window, bg='#f0f0f0')
        list_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10)
        
        # لیست رویدادها
        if not self.events:
            empty_label = tk.Label(
                list_frame,
                text="هیچ رویدادی ثبت نشده است.",
                font=self.farsi_font,
                bg='#f0f0f0',
                fg='#7f8c8d'
            )
            empty_label.pack(pady=50)
        else:
            # ایجاد کانوس برای اسکرول
            canvas = tk.Canvas(list_frame, bg='white')
            scrollbar = tk.Scrollbar(list_frame, orient="vertical", command=canvas.yview)
            scrollable_frame = tk.Frame(canvas, bg='white')
            
            scrollable_frame.bind(
                "<Configure>",
                lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
            )
            
            canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
            canvas.configure(yscrollcommand=scrollbar.set)
            
            # نمایش رویدادها
            for i, (event_key, event_text) in enumerate(sorted(self.events.items())):
                # تجزیه کلید رویداد
                year, month, day = map(int, event_key.split('-'))
                date_str = f"{day} {self.persian_months[month-1]} {year}"
                
                # فریم برای هر رویداد
                event_frame = tk.Frame(scrollable_frame, bg='white', relief=tk.RAISED, borderwidth=1)
                event_frame.pack(fill=tk.X, padx=5, pady=5, ipady=5)
                
                # تاریخ رویداد
                date_label = tk.Label(
                    event_frame,
                    text=date_str,
                    font=self.farsi_font_bold,
                    bg='white',
                    fg='#2c3e50',
                    width=20
                )
                date_label.pack(side=tk.LEFT, padx=10)
                
                # متن رویداد
                event_label = tk.Label(
                    event_frame,
                    text=event_text[:50] + ("..." if len(event_text) > 50 else ""),
                    font=self.farsi_font,
                    bg='white',
                    fg='#34495e',
                    wraplength=300,
                    justify=tk.RIGHT
                )
                event_label.pack(side=tk.LEFT, padx=10, fill=tk.X, expand=True)
                
                # دکمه حذف
                delete_btn = tk.Button(
                    event_frame,
                    text="حذف",
                    command=lambda k=event_key: self.delete_event_from_manager(k, events_window),
                    bg='#e74c3c',
                    fg='white',
                    font=self.farsi_font,
                    padx=5
                )
                delete_btn.pack(side=tk.RIGHT, padx=5)
            
            canvas.pack(side="left", fill="both", expand=True)
            scrollbar.pack(side="right", fill="y")
        
        # دکمه بستن
        close_button = tk.Button(
            events_window,
            text="بستن",
            command=events_window.destroy,
            bg='#95a5a6',
            fg='white',
            font=self.farsi_font_bold,
            padx=20,
            pady=5
        )
        close_button.pack(pady=15)
    
    def delete_event_from_manager(self, event_key, events_window):
        # حذف رویداد از مدیر رویدادها
        if event_key in self.events:
            del self.events[event_key]
            self.save_events()
            self.update_calendar()
            events_window.destroy()
            self.show_events_manager()
            messagebox.showinfo("موفق", "رویداد با موفقیت حذف شد.")
    
    def save_events(self):
        # ذخیره رویدادها در فایل JSON
        try:
            with open('persian_calendar_events.json', 'w', encoding='utf-8') as f:
                json.dump(self.events, f, ensure_ascii=False, indent=2)
        except Exception as e:
            print(f"خطا در ذخیره رویدادها: {e}")
    
    def load_events(self):
        # بارگیری رویدادها از فایل JSON
        try:
            if os.path.exists('persian_calendar_events.json'):
                with open('persian_calendar_events.json', 'r', encoding='utf-8') as f:
                    self.events = json.load(f)
        except Exception as e:
            print(f"خطا در بارگیری رویدادها: {e}")
            self.events = {}
    
    def prev_month(self):
        # رفتن به ماه قبل
        self.current_month -= 1
        if self.current_month < 1:
            self.current_month = 12
            self.current_year -= 1
        
        self.update_calendar()
    
    def next_month(self):
        # رفتن به ماه بعد
        self.current_month += 1
        if self.current_month > 12:
            self.current_month = 1
            self.current_year += 1
        
        self.update_calendar()
    
    def go_to_today(self):
        # بازگشت به تاریخ امروز
        today = jdatetime.date.today()
        self.current_year = today.year
        self.current_month = today.month
        
        self.update_calendar()
    
    def apply_date_change(self):
        # اعمال تغییرات سال و ماه
        try:
            new_year = int(self.year_var.get())
            new_month = self.persian_months.index(self.month_var.get()) + 1
            
            if 1300 <= new_year <= 1500 and 1 <= new_month <= 12:
                self.current_year = new_year
                self.current_month = new_month
                self.update_calendar()
            else:
                messagebox.showerror("خطا", "لطفاً مقادیر معتبر وارد کنید")
        except ValueError:
            messagebox.showerror("خطا", "لطفاً مقادیر معتبر وارد کنید")

def main():
    root = tk.Tk()
    app = PersianCalendar(root)
    root.mainloop()

if __name__ == "__main__":
    main()

نحوه کارکرد برنامه

روند اجرای برنامه:

  1. برنامه با ایجاد پنجره اصلی tkinter شروع می‌شود
  2. کلاس PersianCalendar مقداردهی اولیه می‌شود
  3. رویدادهای ذخیره شده از فایل JSON بارگیری می‌شوند
  4. رابط کاربری شامل دکمه‌ها و کنترل‌ها ایجاد می‌شود
  5. تقویم ماه جاری با روزهای رنگی‌شده نمایش داده می‌شود
  6. کاربر می‌تواند با کلیک روی هر روز جزئیات آن را ببیند
  7. با دابل کلیک یا کلیک راست روی هر روز می‌توان رویداد اضافه کرد
  8. با دکمه‌های کنترل می‌توان بین ماه‌ها و سال‌ها حرکت کرد
  9. رویدادها به صورت خودکار در فایل ذخیره می‌شوند

سیستم رنگ‌بندی روزها:

  • آبی: روز جاری
  • قرمز: روزهای تعطیل
  • نارنجی: روزهای جمعه
  • سبز: روزهای دارای رویداد
  • سیاه: روزهای عادی

نحوه اجرای برنامه

برای اجرای برنامه مراحل زیر را دنبال کنید:

1. نصب پیش‌نیازها

pip install jdatetime

2. ذخیره کد

کد کامل را در فایلی با نام persian_calendar.py ذخیره کنید.

3. اجرای برنامه

python persian_calendar.py

4. استفاده از برنامه

  • برنامه به طور خودکار تاریخ امروز را نمایش می‌دهد
  • روی هر روز کلیک کنید تا جزئیات آن روز شامل تاریخ میلادی و تعطیلات نمایش داده شود
  • برای افزودن رویداد، روی روز مورد نظر دابل کلیک کنید یا کلیک راست کنید
  • از دکمه‌های "ماه قبل" و "ماه بعد" برای پیمایش استفاده کنید
  • دکمه "امروز" شما را به تاریخ فعلی بازمی‌گرداند
  • برای مدیریت همه رویدادها، دکمه "مدیریت رویدادها" را بزنید

ویژگی‌های پیشرفته پیاده‌سازی شده

1. سیستم ذخیره‌سازی JSON

رویدادها در فایل persian_calendar_events.json ذخیره می‌شوند که ساختار زیر را دارد:

{
  "1403-1-15": "جلسه کاری",
  "1403-2-10": "تولد دوست",
  "1403-3-1": "پرداخت قبض"
}

2. پشتیبانی از سال کبیسه

برنامه به طور خودکار سال‌های کبیسه را تشخیص داده و اسفند را 30 روزه نمایش می‌دهد.

3. رابط کاربری فارسی و RTL

تمام متن‌ها به فارسی و تراز به راست نمایش داده می‌شوند.

4. افکت‌های تعاملی

  • هاور (Hover) روی روزها
  • رنگ‌بندی پویا بر اساس وضعیت روز
  • نشانگر رویداد برای روزهای دارای رویداد

راهنمای رفع مشکلات

مشکل 1: خطای "jdatetime not found"

راه حل: کتابخانه jdatetime را نصب کنید:

pip install jdatetime

مشکل 2: فونت فارسی نمایش داده نمی‌شود

راه حل: فونت "B Nazanin" را روی سیستم نصب کنید یا کد را برای استفاده از فونت پیش‌فرض تغییر دهید.

مشکل 3: برنامه اجرا می‌شود اما پنجره نمایش داده نمی‌شود

راه حل: مطمئن شوید که tkinter روی سیستم نصب است. برای سیستم‌عامل‌های مختلف:

  • Ubuntu/Debian: sudo apt-get install python3-tk
  • Windows: معمولاً همراه پایتون نصب است
  • macOS: brew install python-tk

مشکل 4: رویدادها ذخیره نمی‌شوند

راه حل: اطمینان حاصل کنید که برنامه دسترسی نوشتن در دایرکتوری جاری را دارد.

پیشنهادات برای توسعه و بهبود

  • سیستم یادآوری: افزودن قابلیت یادآوری برای رویدادها با نوتیفیکیشن
  • چند تقویمی: پشتیبانی همزمان از تقویم میلادی، شمسی و قمری
  • چاپ تقویم: امکان چاپ تقویم ماه جاری
  • مناسبت‌های مذهبی: افزودن مناسبت‌های مذهبی متغیر (با محاسبه تاریخ قمری)
  • همگام‌سازی: قابلیت همگام‌سازی با Google Calendar یا Outlook
  • تم‌های رنگی: امکان انتخاب تم‌های رنگی مختلف توسط کاربر
  • جستجوی رویدادها: افزودن قابلیت جستجو در رویدادهای ثبت شده
  • امکانات اشتراک‌گذاری: امکان اشتراک‌گذاری رویدادها با دیگران
  • نسخه تحت وب: تبدیل برنامه به وب اپلیکیشن با Flask یا Django
  • برنامه موبایل: ایجاد نسخه اندروید با Kivy یا BeeWare

مهارت‌های آموخته شده از این پروژه

  • برنامه‌نویسی GUI: کار عملی با tkinter برای ایجاد رابط کاربری
  • کار با تاریخ و زمان: استفاده از کتابخانه jdatetime برای تاریخ شمسی
  • ذخیره‌سازی داده‌ها: کار با فایل‌های JSON برای ذخیره اطلاعات
  • برنامه‌نویسی شی‌گرا: طراحی و پیاده‌سازی کلاس‌های پیچیده
  • مدیریت رویدادها: پیاده‌سازی سیستم مدیریت رویدادها
  • رنگ‌بندی و استایل: ایجاد رابط کاربری جذاب با رنگ‌بندی پویا
  • مدیریت حالت برنامه: پیاده‌سازی حالت‌های مختلف نمایش
  • خطایابی: پیاده‌سازی سیستم مدیریت خطا
  • برنامه‌نویسی فارسی: ایجاد برنامه‌های فارسی زبان با پشتیبانی RTL

جمع‌بندی

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

  • طراحی و پیاده‌سازی رابط کاربری با tkinter
  • کار با تاریخ‌های شمسی و تبدیل آن‌ها
  • پیاده‌سازی سیستم ذخیره‌سازی داده‌ها با JSON
  • مدیریت رویدادها و تعاملات کاربر
  • ایجاد برنامه‌های فارسی زبان با پشتیبانی از راست‌به‌چپ

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