学习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. 运行