python 自定义日历控件开发

       学习python期间,发现 tkinter没有自带的日期选择控件。决定自己的写一个日期控件,费尽周转,终于写了一个自己满意的日期控件。本着人人为我,我为人人的原则,欢迎大家转发,评论,及提出宝贵的建议和意见。严重反感复制别人作品来获取下载积分和关注等行为。

1.创建一个自定义DatePicker类,实现下拉日期选择

import tkinter
from tkinter import ttk
import calendar
import datetime


class DatePicker(ttk.Entry):

    def __init__(self, master=None, **kw):
        self._separator = kw.pop('separator', '-')
        if self._separator not in ['-','/']:
            self._separator = '-'

        super().__init__(master, **kw)

        self.set_state(kw.get('state', ''))

        self.style = ttk.Style(self)
        self._setup_style()
        self.configure(style='DatePicker')

        self._year = tkinter.IntVar()
        self._year.set(datetime.date.today().year)
        self._month = tkinter.IntVar()
        self._month.set(datetime.date.today().month)
        self._day = tkinter.IntVar()
        self._day.set(datetime.date.today().day)
        self._set_text()

        self._frame = tkinter.Toplevel(self, background='gray', borderwidth=1)
        self._frame.withdraw()                       #   隐藏窗口
        self._frame.overrideredirect(True)           #   显示下拉时,不带标题栏 (Toplevel是一个窗口)
        self._set_frame()

        self.bind('<Motion>', self._on_motion)
        self.bind('<Leave>', lambda e: self.state(['!active']))
        self.bind('<Button-1>', self._show_down_frame)
        self._top_frame.bind('<FocusOut>', self._on_focus_out)


    def _setup_style(self):
        """
        设置样式(复制TCombobox样式)
        :return:
        """
        self.style.layout('DatePicker', self.style.layout('TCombobox'))
        conf = self.style.configure('TCombobox')
        if conf:
            self.style.configure('DatePicker', **conf)
        maps = self.style.map('TCombobox')
        if maps:
            self.style.map('DatePicker', **maps)


    def _on_motion(self, event):
        x, y = event.x, event.y
        if 'disabled' not in self.state():
            if self.identify(x, y) == 'Combobox.rightdownarrow':
                self.state(['active'])
                self.configure(cursor='arrow')
            else:
                self.state(['!active'])
                self.configure(cursor='xterm')


    def _show_down_frame(self, event):
        """
        显示下拉
        :param event:
        :return:
        """
        if ('disabled' in self.state()) or (self.identify(event.x, event.y) != 'Combobox.rightdownarrow'):
            return

        if self._sp_year.winfo_ismapped():     #   _btn被绘制(下拉打开状态)
            self._frame.withdraw()             #   隐藏下拉窗口(关闭下拉)
        else:
            x = self.winfo_rootx()
            y = self.winfo_rooty()+ self.winfo_height()

            self._frame.geometry('+%i+%i' % (x,y))
            self._frame.deiconify()                     #   打开下拉(显示窗口)
            self._sp_year.focus_set()


    def _on_focus_out(self, event):
        if self.focus_get() == None and 'active' in  self.state():   #下拉打开再次点击下拉
            pass
        else:
            self._frame.withdraw()


    def _set_frame(self):
        """
        设置下拉界面
        :return:
        """
        self._top_frame = ttk.Frame(self._frame)
        self._top_frame.pack(fill='x')

        self._sp_year = ttk.Spinbox(self._top_frame, from_=1900, to=5000, width=6, textvariable=self._year)
        self._sp_year.grid(row=0, column=0, padx=2, pady=1)
        ttk.Label(self._top_frame, text='年').grid(row=0, column=1, padx=2)

        self._sp_month = ttk.Spinbox(self._top_frame, from_=1, to=12, width=4, textvariable=self._month)
        self._sp_month.grid(row=0, column=2, padx=3, pady=1)
        ttk.Label(self._top_frame, text='月').grid(row=0, column=3, padx=2)


        _middle_frame = ttk.Frame(self._frame)
        _middle_frame.pack(fill='x')
        self._initial_labels(_middle_frame)

        _bottom_frame = ttk.Frame(self._frame)
        _bottom_frame.pack(fill='x')
        _label_today = ttk.Label(_bottom_frame, text='今天: '+ str(datetime.date.today()))
        _label_today.pack(anchor='e', ipadx=8)
        _label_today.bind('<Motion>', self._on_label_today_motion)
        _label_today.bind('<Leave>', self._on_label_today_leave)
        _label_today.bind('<Button-1>', self._on_label_today_click)

        self._sp_year.configure(command=self._on_change)
        self._sp_year.bind('<KeyRelease>', self._on_spinbox_press)
        self._sp_year.bind('<Return>', self._on_year_return)

        self._sp_month.configure(command=self._on_change)
        self._sp_month.bind('<KeyRelease>', self._on_spinbox_press)
        self._sp_month.bind('<Return>', self._on_month_return)


    def _on_label_today_motion(self,event):
            event.widget.configure(foreground='DeepSkyBlue')


    def _on_label_today_leave(self, event):
            event.widget.configure(foreground=self.cget('background'))


    def _on_label_today_click(self, event):
        self._year.set(datetime.date.today().year)
        self._month.set(datetime.date.today().month)
        self._day.set(datetime.date.today().day)

        self._on_change()      #  通过_on_change()重新设置self._day_list
        self._set_text()
        self._frame.withdraw()


    def _on_year_return(self,event):
        if self._year.get() > int(event.widget.cget('to')):
            self._year.set(event.widget.cget('to'))
        if self._year.get() < int(event.widget.cget('from')):
            self._year.set(event.widget.cget('from'))

        self._sp_month.focus()
        self._on_change()


    def _on_month_return(self,event):
        if self._month.get() > int(event.widget.cget('to')):
            self._month.set(event.widget.cget('to'))
        if self._month.get() < int(event.widget.cget('from')):
            self._month.set(event.widget.cget('from'))

        self._sp_year.focus()
        self._on_change()


    def _on_spinbox_press(self, event):
        txt = event.widget.get()
        if event.keysym:
            if not txt.isdigit():
                event.widget.set(''.join(i for i in txt if i.isdigit()))


    def _initial_labels(self, parent):
        """
        初始化日期标签
        :param parent:
        :return:
        """
        week = ('一', '二', '三', '四', '五', '六', '日')
        for i, item in enumerate(week):
            ttk.Label(parent, text=item.center(3)).grid(row=0, column=i)

        self._day_list = []
        self._label_list = []
        self._set_day_list()
        for i, item in enumerate(self._day_list):
            label = ttk.Label(parent, text=str(item[2]).rjust(2))
            label.grid(row= i // 7 + 1, column=item[3], padx=1)
            label.hint = item                #添加一个属性
            label['foreground'] ='Black'
            if item[1] != self._month.get():
                label['foreground'] = 'Gray'
            label['background'] = self.cget('background')
            if item[2] == self._day.get() and  item[1] == self._month.get():
                label['background'] = 'DeepSkyBlue'

            label.bind('<Button-1>', self._on_label_click)
            label.bind('<Motion>', self._on_label_motion)
            label.bind('<Leave>', self._on_label_leave)
            self._label_list.append(label)


    def _set_day_list(self):
        """
        设置日期列表
        :return:
        """
        year = self._year.get()
        month = self._month.get()
        for day in calendar.Calendar().itermonthdays4(year, month):
            self._day_list.append(day)


        if len(self._day_list) < 42:
            month = month + 1
            if month == 13:
                year = year +1
                month = 1

            for day2 in calendar.Calendar().itermonthdays4(year, month):
                if day2 in self._day_list:
                    continue

                self._day_list.append(day2)
                if len(self._day_list) == 42:
                    break

        ########################错误代码(修改于2023-10-09)######################
        # if len(self._day_list) == 28:     #本月只有28天且一号为星期一
        #     for day in calendar.Calendar().itermonthdays4(self._year.get(), self._month.get() + 1):
        #         self._day_list.append(day)
        #         if len(self._day_list) == 35:
        #             break


    def _on_change(self):
        self._day_list.clear()
        self._set_day_list()

        for i, label in enumerate(self._label_list):
            label.hint = self._day_list[i]
            label['text'] = self._day_list[i][2]

            label['foreground'] ='Black'
            if self._day_list[i][1] != self._month.get():
                label['foreground'] = 'Gray'

            label['background'] = self.cget('background')
            if self._day_list[i][1] == self._month.get() and self._day_list[i][2] == self._day.get():
                label['background'] = 'DeepSkyBlue'


    def _set_text(self):
        """
        设置日期至文本框
        :return:
        """
        readonly = False
        if 'readonly' in self.state():
            readonly = True
            self.state(['!readonly'])

        txt = self._separator.join([str(self._year.get()), str(self._month.get()), str(self._day.get())])
        self.delete(0, 'end')
        self.insert(0, txt)
        if readonly:
            self.state(['readonly'])


    def _on_label_click(self, event):
        if event.widget.hint is not None:
            for label in self._label_list:
                if label != event.widget:     # 刷新背景色(修改默认日期的背景色)
                    label.configure(background=self.cget('background'))

            self._year.set(event.widget.hint[0])
            self._month.set(event.widget.hint[1])
            self._day.set(event.widget.hint[2])

            self._set_text()
            self._frame.withdraw()


    def _on_label_motion(self, event):
        if event.widget.hint is not None:
            event.widget.configure(background= 'SkyBlue')
            if event.widget.hint[1] == self._month.get() and event.widget.hint[2] == self._day.get():
                event.widget.configure(background='DeepSkyBlue')


    def _on_label_leave(self, event):
        if event.widget.hint is not None:
            event.widget.configure(background= self.cget('background'))
            if event.widget.hint[1] == self._month.get() and event.widget.hint[2] == self._day.get():
                event.widget.configure(background='DeepSkyBlue')


    def _set_date(self, text):
        """
        设置日期
        :param text:  被设置的日期文本
        :return:
        """
        try:
            formatstr = self._separator.join(['%Y','%m','%d'])
            cur_date = datetime.datetime.strptime(text, formatstr)
            self._year.set(cur_date.date().year)
            self._month.set(cur_date.date().month)
            self._day.set(cur_date.date().day)
            self._set_text()
        except Exception:
                raise ValueError("%s不是一个合法的日期" % text)

    def _get_date(self):
        return self.get()


    def set_state(self, *args):
        """
        设置状态
        :param args:
        :return:
        """
        if args:
            if ('disabled' in args) or ('readonly' in args):
                self.configure(cursor='arrow')
            elif ('!disabled' in args) or ('!readonly' in args):
                self.configure(cursor='xterm')
            self.state(args)

    date = property(_get_date, _set_date)

2. 测试主界面

import tkinter as tk
import datepicker


class GUI:

    def __init__(self):
        self.root = tk.Tk()
        self.root.title('演示')
        self.root.geometry("300x230+300+150")
        self.interface()

    def interface(self):
        """"界面编写位置"""
        self.Label0 = tk.Label(self.root, text="日  期")
        self.Label0.grid(row=0, column=0, padx=2)

        self.date = datepicker.DatePicker(self.root, width='10')
        self.date.grid(row=0, column=1, padx=2)

        self.Button = tk.Button(self.root, text="获取日期", width=7, command=self.show)
        self.Button.grid(row=0, column=2,  padx=2)

        self.text = tk.Text(self.root, width=30, height=10)
        self.text.grid(row=1, column=0, columnspan=3)


    def show(self):
        # 获取日期
        self.text.delete(0.0,'end')
        self.text.insert(1.0, f"日期1:{self.date.date}\n")
        self.text.insert(1.0, f"日期2:{self.date.date.replace('-', '/')}\n")


if __name__ == '__main__':
    a = GUI()
    a.root.mainloop()

3. 运行

  • 1
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值