tkinter绘制组件(53)——日历选择器
- 引言
- 日历选择器类
- 控件元素布局
- 选择器
- 日期网格
- 类方法
- 效果
引言
在tkinter绘制组件(48)——日期与时间滚动选择器一文中,通过滚动选择器实现了日期的选择。对于“日期”这种有着通用特殊含义的选择内容,还有一种非常符合逻辑的选择方式——日历选择。(在WinUI3中,为CalendarDatePicker)
TinUI本身不提供日历选择,因为这个控件实际上用得很少,并且实现起来也比较复杂,因此,TinUI日历选择器在另一个拓展库tinuipicker中提供。
pip install tinuipicker项目地址:Smart-Space/TinUIPicker: TinUI高级滚动选择器。
日历选择器类
classTinUICalendarPicker:def__init__(self,tinui:BasicTinUI,pos,font=("微软雅黑",10),command=None,now=datetime.today(),anchor='nw',**kwargs):self.self=tinui self.scale_value=tinui.scale_value self.pos=pos self.font=Font(font=font)self.font_width=self.font.measure('30')self.cell_size=self.scale_value(15)+self.font_width# 每个日期单元格的大小,考虑字体宽度self.width=self.cell_size*7+self.scale_value(20)self.height=self.cell_size*8+self.scale_value(20)# 1行月份控制 + 1行星期 + 6行日期 + paddingself.command=command self.anchor=anchor self.cfg={...}self.res_year,self.res_month,self.res_day=str(now.year),str(now.month).zfill(2),str(now.day).zfill(2)# 当前展示的年月self.view_year=now.year self.view_month=now.month self._build_trigger()self._setup_picker_ui()日历选择器分为两个部分,一个是常驻控件,由_build_trigger绘制;一个是选择器部分,由_setup_picker_ui实现。
本类中所有布局和尺寸运算均使用self.cell_size,这是一个包含文本宽度和间距宽度的可变量,本质上由字体决定。这样的实现能够适应自定义字体、高DPI计算。
控件元素布局
TinUICalendarPicker的常驻控件布局就是TinUI滚动选择器的结果文本视觉元素,与tkinter绘制组件(40)——滚动选值框的文本展示实现一致,这里不赘述。
选择器
TinUICalendarPicker的选择器本体和Picker一样为弹出无边框窗口,分为四个部分:
- 左上角“年份-月份”显示
- 右上角切换月份按钮
- 中间一行星期简称表头
- 下部为可被选择的该月日期
def_build_calendar_ui(self,width):offset_x=self.scale_value(10)offset_y=self.scale_value(10)# 年月显示self.title_text=self.bar.create_text((offset_x,offset_y),text=f"{self.view_year}-{self.view_month}",# 考虑可以使用模板字符串作为参数设置具体显示格式fill=self.cfg['fg'],font=self.font,anchor="nw")# 右上角按钮 (上一月、下一月)btn_y=offset_y btn_w=self.cell_size prev_btn_x=width-btn_w*2next_btn_x=width-btn_w self.bar.add_button2((prev_btn_x,btn_y),icon='\uF090',text='',fg=self.cfg['buttonfg'],bg=self.cfg['buttonbg'],line=self.cfg['buttonbg'],activefg=self.cfg['buttonactivefg'],activebg=self.cfg['buttonactivebg'],activeline=self.cfg['buttonactivebg'],onfg=self.cfg['buttononfg'],onbg=self.cfg['buttononbg'],online=self.cfg['buttononbg'],font=self.font,command=self._prev_month)self.bar.add_button2((next_btn_x,btn_y),icon='\uF08E',text='',fg=self.cfg['buttonfg'],bg=self.cfg['buttonbg'],line=self.cfg['buttonbg'],activefg=self.cfg['buttonactivefg'],activebg=self.cfg['buttonactivebg'],activeline=self.cfg['buttonactivebg'],onfg=self.cfg['buttononfg'],onbg=self.cfg['buttononbg'],online=self.cfg['buttononbg'],font=self.font,command=self._next_month)# 星期简称栏weekdays=["Mo","Tu","We","Th","Fr","Sa","Su"]# 之后也可以考虑作为设置显示具体格式,但目前规定一周的开头是周一week_y=offset_y+self.cell_size+self.scale_value(5)fori,dayinenumerate(weekdays):self.bar.create_text((offset_x+self.cell_size*i+self.cell_size/2,week_y+self.cell_size/2),text=day,fill=self.cfg['fg'],font=self.font)# 日期网格self.grid_start_y=week_y+self.cell_size self._draw_days(offset_x)日期网格
def_draw_days(self,offset_x):# 清除旧的日期元素forelementsinself.day_elements:foriteminelements['items']:self.bar.delete(item)self.day_elements.clear()self.selected_back=None# 获取当月日历信息cal=calendar.Calendar(firstweekday=0)# 0 = Mondaymonth_days=cal.monthdayscalendar(self.view_year,self.view_month)row=0forweekinmonth_days:col=0fordayinweek:ifday!=0:self._create_day_cell(row,col,day,offset_x)col+=1row+=1self._create_day_cell创建的每个日期元素包含一个圆形背景和数字文本。
self.day_elements包含{文本元素,背景元素,文本,日期,是否选中},用于响应鼠标事件、选择事件。
def_create_day_cell(self,row,col,day,offset_x):cx=offset_x+self.cell_size*col+self.cell_size/2cy=self.grid_start_y+self.cell_size*row+self.cell_size/2r=self.cell_size/2# 圆形背景back=self.bar.create_oval(cx-r,cy-r,cx+r,cy+r,fill=self.cfg['buttonbg'],outline=self.cfg['buttonbg'],width=self.scale_value(1))# 文本text=self.bar.create_text((cx,cy),text=str(day),fill=self.cfg['buttonfg'],font=self.font)items=(back,text)# 判断是否为选中状态is_selected=(str(day)==self.res_dayandself.view_year==int(self.res_year)andself.view_month==int(self.res_month))ifis_selected:self.bar.itemconfig(back,fill=self.cfg['onbg'])self.bar.itemconfig(text,fill=self.cfg['onfg'])self.selected_back=back# 绑定事件data={'items':items,'back':back,'text':text,'day':str(day),'is_sel':is_selected}foriteminitems:self.bar.tag_bind(item,"<Enter>",lambda_,d=data:self._on_day_enter(d))self.bar.tag_bind(item,"<Leave>",lambda_,d=data:self._on_day_leave(d))self.bar.tag_bind(item,"<Button-1>",lambda_,d=data:self._on_day_click(d))self.day_elements.append(data)类方法
截止本文,TinUICalendarPicker只提供set_date方法。
defset_date(self,year:int,month:int,day:int):self.res_year=str(year)self.res_month=str(month).zfill(2)self.res_day=str(day).zfill(2)self.view_year=year self.view_month=month full_date=f"{self.res_year}-{self.res_month}-{self.res_day}"self.self.itemconfig(self.main_text,text=full_date)ifhasattr(self,'title_text'):self._update_title_and_days()效果
在Windows上,tkinter(除非是Tcl/Tk9.0及以上,不过我也没用过)画布的圆形锯齿非常明显,这里是开了高DPI感知2的窗口演示。