2025, Oct 17 18:33

tkcalendar में केवल चुनिंदा तारीखें सक्षम करें: allowed_dates के साथ DateEntry कस्टमाइज़ेशन

Python Tkinter में tkcalendar DateEntry को सच्चे allowlist पर लाएँ: allowed_dates से केवल चुनिंदा तारीखें ही क्लिक हों, बाकी disable. mindate/maxdate समायोजन.

डेट पिकर को सख्त allowlist तक सीमित करना सुनने में आसान लगता है, लेकिन कई GUI स्टैक्स में डिफॉल्ट विजेट सिर्फ शुरुआत और समाप्ति की सीमा को ही संभालता है। यदि आप tk और tkcalendar इस्तेमाल कर रहे हैं, तो mindate और maxdate उपयोगी रेंज-गार्ड देते हैं, मगर वे उसी दायरे के भीतर गैर-अनुमत दिनों का चयन होने से नहीं रोकते। नीचे कैलेंडर विजेट में केवल चुनिंदा तारीखें ही चुनी जा सकें, इसके लिए असली allowlist लागू करने की एक संक्षिप्त राहनुमा दी गई है।

समस्या

आप चाहते हैं कि उपयोगकर्ता पहले से तय किए गए सेट में से ठीक एक तारीख चुने। डिफॉल्ट DateEntry एक रेंज सीमित कर सकता है, लेकिन mindate और maxdate के बीच आने वाला कोई भी मध्य का दिन फिर भी क्लिक किया जा सकता है।

import datetime as dt
import tkinter as tk
from tkinter import ttk
from tkcalendar import DateEntry
# fmt: off
allowed_str_days = [
    "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
    "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
    "2024-04-22",
    "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
    "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
    "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
]
# fmt: on
app = tk.Tk()
picked_var = tk.StringVar()
selector = DateEntry(
    app,
    textvariable=picked_var,
    date_pattern="yyyy-mm-dd",
    mindate=dt.date.fromisoformat(allowed_str_days[0]),
    maxdate=dt.date.fromisoformat(allowed_str_days[-1]),
)
selector.pack()
app.mainloop()

यह कोड सबसे शुरुआती और सबसे आखिरी चुनी जा सकने वाली तारीखों को सीमित करता है, लेकिन इन सीमाओं के अंदर के गैर-अनुमत दिनों को बाहर नहीं करता।

असल में होता क्या है

tkcalendar में Calendar और DateEntry mindate और maxdate के जरिए रेंज पाबंदियाँ देते हैं। उस रेंज के भीतर बिखरे हुए कुछ खास दिनों को ही अनुमति देने का कोई बिल्ट-इन तरीका नहीं है। असली allowlist पाने के लिए कैलेंडर ग्रिड को उस सूची में न होने वाले हर दिन को सक्रिय रूप से अक्षम करना होगा। इसके लिए कैलेंडर सेल्स कैसे रेंडर होते हैं और तारीख चयन कैसे सत्यापित होता है—दोनों में बदलाव जरूरी है।

समाधान

दृष्टिकोण यह है कि आधारभूत Calendar को allowed_dates नाम का नया विकल्प देकर बढ़ाया जाए। यह विकल्प datetime.date ऑब्जेक्ट्स की सूची लेता है और रेंडरिंग के दौरान allowed_dates[0] से allowed_dates[-1] की सीमा में उस सूची में मौजूद न होने वाली हर तारीख को निष्क्रिय कर देता है। मूल विचार वहीं लागू होता है जहां कैलेंडर दिखाया जाता है: संबंधित दिनों पर इटरैट करें और allowlist में न आने वाली कोशिकाओं को डिसेबल कर दें।

# एक कस्टम Calendar क्लास के भीतर
# --- ग्रिड रेंडर करें
def _render_grid(self):
    # ... महीने के दृश्य के लिए मौजूदा रेंडरिंग कोड ...
    allowlist = self['allowed_dates']
    if allowlist is not None:
        import datetime as dt
        day_step = dt.timedelta(days=1)
        cur_day = allowlist[0]
        last_day = allowlist[-1]
        while cur_day <= last_day:
            if cur_day not in allowlist:
                row_idx, col_idx = self._get_day_coords(cur_day)
                if row_idx is not None:
                    print(cur_day, row_idx, col_idx, '!disabled')
                    self._calendar[row_idx][col_idx].state(['disabled'])
            cur_day += day_step

यह होने पर आप allowed_dates पास कर सकते हैं और केवल वे ही सेल्स क्लिक योग्य रहेंगी। स्ट्रिंग्स की सूची को पहले datetime.date में बदलना होगा, और आप अपने कस्टम मॉड्यूल से Calendar या DateEntry—दोनों में से किसी का उपयोग कर सकते हैं। नीचे एक रन करने योग्य उदाहरण है, जो allowlist में तारीखें डायनैमिक रूप से जोड़ना और हटाना भी दिखाता है, साथ ही mindate और maxdate को नए सीमाबिंदुओं के अनुरूप रखता है।

import datetime as dt
import tkinter as tk
# from tkinter import ttk
# from tkcalendar import DateEntry
from mycalendar import Calendar
from mycalendar import DateEntry
# fmt: off
raw_days = [
    "2024-04-08", "2024-04-10", "2024-04-11", "2024-04-12",
    "2024-04-15", "2024-04-16", "2024-04-17", "2024-04-18", "2024-04-19",
    "2024-04-22",
    "2024-05-21", "2024-05-22", "2024-05-23", "2024-05-24",
    "2024-05-27", "2024-05-28", "2024-05-29", "2024-05-30", "2024-05-31",
    "2024-06-03", "2024-06-04", "2024-06-05", "2024-06-07",
    "2024-06-10", "2024-06-11", "2024-06-12", "2024-06-13", "2024-06-14",
]
# fmt: on
ui = tk.Tk()
whitelist_dt = [dt.date.fromisoformat(d) for d in raw_days]
# Calendar का उदाहरण
tk.Label(ui, text="Calendar").pack()
cal_widget = Calendar(
    ui,
    date_pattern="yyyy-mm-dd",
    mindate=whitelist_dt[0],
    maxdate=whitelist_dt[-1],
    allowed_dates=whitelist_dt,
    locale="en_GB.utf-8",
)
cal_widget.pack()
entry_var = tk.StringVar()
# DateEntry का उदाहरण
tk.Label(ui, text="DateEntry").pack()
entry_widget = DateEntry(
    ui,
    textvariable=entry_var,
    date_pattern="yyyy-mm-dd",
    mindate=whitelist_dt[0],
    maxdate=whitelist_dt[-1],
    allowed_dates=whitelist_dt,
    locale="en_GB.utf-8",
)
entry_widget.pack()
# Target Calendar या DateEntry में से कोई भी हो सकता है
# target = cal_widget
target = entry_widget
def show_whitelist():
    for d in target['allowed_dates']:
        print('allowed:', d)
btn_show = tk.Button(ui, text="Show Allowed Dates", command=show_whitelist)
btn_show.pack(fill='x')
def add_whitelisted(day_str):
    day_obj = dt.date.fromisoformat(day_str)
    if day_obj not in target['allowed_dates']:
        print('add allowed:', day_str)
        target['allowed_dates'].append(day_obj)
        target['allowed_dates'] = sorted(target['allowed_dates'])
        if target['allowed_dates'][0] < target['mindate']:
            target['mindate'] = target['allowed_dates'][0]
        if target['allowed_dates'][-1] > target['maxdate']:
            target['maxdate'] = target['allowed_dates'][-1]
        if target == cal_widget:
            target._render_grid()
for s in ('2024-06-06', '2024-06-26', '2024-07-10'):
    tk.Button(ui, text=f"Add Allowed Date: {s}", command=lambda x=s: add_whitelisted(x)).pack(fill='x')
def remove_whitelisted(day_str):
    day_obj = dt.date.fromisoformat(day_str)
    if day_obj in target['allowed_dates']:
        print('remove allowed:', day_str)
        target['allowed_dates'].remove(day_obj)
        if target['mindate'] < target['allowed_dates'][0]:
            target['mindate'] = target['allowed_dates'][0]
        if target['maxdate'] > target['allowed_dates'][-1]:
            target['maxdate'] = target['allowed_dates'][-1]
        if target == cal_widget:
            target._render_grid()
for s in ('2024-06-06', '2024-06-26', '2024-07-10'):
    tk.Button(ui, text=f"Remove Allowed Date: {s}", command=lambda x=s: remove_whitelisted(x)).pack(fill='x')
ui.mainloop()

इस तरीके का उपयोग करते समय कुछ बातें ध्यान देने लायक हैं। Calendar में 2024-06-06 जोड़ने पर वह DateEntry में भी परिलक्षित हुआ, लेकिन इसे हटाने का प्रभाव उसी तरह प्रसारित नहीं हुआ। एक और देखा गया व्यवहार यह था कि यदि कोई तारीख मौजूदा mindate या maxdate के बाहर थी, तो DateEntry ने उसे जोड़ा ही नहीं। इसलिए पूर्वानुमेय इंटरैक्शन के लिए allowlist में किए गए संपादनों का रेंज सीमाओं के साथ सुसंगत रहना आवश्यक है।

यह क्यों मायने रखता है

वास्तविक शेड्यूलिंग में अक्सर चुनिंदा, विरल तारीखों का सेट चाहिए होता है। रेंज के भीतर मनचाहे दिनों को अक्षम करना डिफॉल्ट विजेट्स की बिल्ट-इन सुविधा नहीं है, इसलिए Calendar को allowlist मानने के लिए बढ़ाना एक व्यावहारिक तरीका है—और इसके लिए परिचित DateEntry वर्कफ़्लो छोड़ना भी नहीं पड़ता। यह समझना भी महत्वपूर्ण है कि कई फ़ंक्शंस मिलकर तारीखों को वैलिडेट और रेंडर करते हैं। ग्रिड पेंट करने वाली डिस्प्ले रूटीन के अलावा, _check_sel_date जैसी चयन जाँचें और DateEntry इंटीग्रेशन में भी बदलाव चाहिए हो सकते हैं ताकि allowed_dates पूरे इंटरैक्शन चक्र में पूरी तरह लागू रहे।

मुख्य बातें

यदि आपको सच्ची allowlist चाहिए, तो allowed_dates को datetime.date ऑब्जेक्ट्स के रूप में पास करें और कैलेंडर की रेंडरिंग के दौरान हर गैर-अनुमत सेल को डिसेबल करें। अपनी स्ट्रिंग इनपुट्स को date.fromisoformat से बदलें, allowlist को सॉर्टेड रखें, और जब भी आइटम जोड़ें या हटाएँ, mindate और maxdate समायोजित करें ताकि रेंज और allowlist एक-दूसरे के अनुरूप रहें। अगर DateEntry के साथ एकीकरण कर रहे हैं, तो मानकर चलें कि कुछ अतिरिक्त फ़ंक्शंस को भी allowlist नियम हर जगह दर्शाने के लिए संरेखित करना पड़ सकता है, और संबंधित हुक्स खोजने के लिए tkcalendar.calendar_ तथा tkcalendar.dateentry के स्रोत देखें। यदि कैलेंडर में संशोधन आपकी समयसीमा में संभव नहीं, तो उसी allowlist से जुड़े ड्रॉपडाउन से चयन कराना एक व्यवहार्य विकल्प है।

यह लेख StackOverflow पर प्रश्न (लेखक: Joooeey) और furas के उत्तर पर आधारित है।