tkinter绘制组件53——日历选择器引言日历选择器类控件元素布局选择器日期网格类方法效果引言在tkinter绘制组件48——日期与时间滚动选择器一文中通过滚动选择器实现了日期的选择。对于“日期”这种有着通用特殊含义的选择内容还有一种非常符合逻辑的选择方式——日历选择。在WinUI3中为CalendarDatePickerTinUI本身不提供日历选择因为这个控件实际上用得很少并且实现起来也比较复杂因此TinUI日历选择器在另一个拓展库tinuipicker中提供。pip install tinuipicker项目地址Smart-Space/TinUIPicker: TinUI高级滚动选择器。日历选择器类classTinUICalendarPicker:def__init__(self,tinui:BasicTinUI,pos,font(微软雅黑,10),commandNone,nowdatetime.today(),anchornw,**kwargs):self.selftinui self.scale_valuetinui.scale_value self.pospos self.fontFont(fontfont)self.font_widthself.font.measure(30)self.cell_sizeself.scale_value(15)self.font_width# 每个日期单元格的大小考虑字体宽度self.widthself.cell_size*7self.scale_value(20)self.heightself.cell_size*8self.scale_value(20)# 1行月份控制 1行星期 6行日期 paddingself.commandcommand self.anchoranchor self.cfg{...}self.res_year,self.res_month,self.res_daystr(now.year),str(now.month).zfill(2),str(now.day).zfill(2)# 当前展示的年月self.view_yearnow.year self.view_monthnow.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_xself.scale_value(10)offset_yself.scale_value(10)# 年月显示self.title_textself.bar.create_text((offset_x,offset_y),textf{self.view_year}-{self.view_month},# 考虑可以使用模板字符串作为参数设置具体显示格式fillself.cfg[fg],fontself.font,anchornw)# 右上角按钮 (上一月、下一月)btn_yoffset_y btn_wself.cell_size prev_btn_xwidth-btn_w*2next_btn_xwidth-btn_w self.bar.add_button2((prev_btn_x,btn_y),icon\uF090,text,fgself.cfg[buttonfg],bgself.cfg[buttonbg],lineself.cfg[buttonbg],activefgself.cfg[buttonactivefg],activebgself.cfg[buttonactivebg],activelineself.cfg[buttonactivebg],onfgself.cfg[buttononfg],onbgself.cfg[buttononbg],onlineself.cfg[buttononbg],fontself.font,commandself._prev_month)self.bar.add_button2((next_btn_x,btn_y),icon\uF08E,text,fgself.cfg[buttonfg],bgself.cfg[buttonbg],lineself.cfg[buttonbg],activefgself.cfg[buttonactivefg],activebgself.cfg[buttonactivebg],activelineself.cfg[buttonactivebg],onfgself.cfg[buttononfg],onbgself.cfg[buttononbg],onlineself.cfg[buttononbg],fontself.font,commandself._next_month)# 星期简称栏weekdays[Mo,Tu,We,Th,Fr,Sa,Su]# 之后也可以考虑作为设置显示具体格式但目前规定一周的开头是周一week_yoffset_yself.cell_sizeself.scale_value(5)fori,dayinenumerate(weekdays):self.bar.create_text((offset_xself.cell_size*iself.cell_size/2,week_yself.cell_size/2),textday,fillself.cfg[fg],fontself.font)# 日期网格self.grid_start_yweek_yself.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_backNone# 获取当月日历信息calcalendar.Calendar(firstweekday0)# 0 Mondaymonth_dayscal.monthdayscalendar(self.view_year,self.view_month)row0forweekinmonth_days:col0fordayinweek:ifday!0:self._create_day_cell(row,col,day,offset_x)col1row1self._create_day_cell创建的每个日期元素包含一个圆形背景和数字文本。self.day_elements包含{文本元素,背景元素,文本,日期,是否选中}用于响应鼠标事件、选择事件。def_create_day_cell(self,row,col,day,offset_x):cxoffset_xself.cell_size*colself.cell_size/2cyself.grid_start_yself.cell_size*rowself.cell_size/2rself.cell_size/2# 圆形背景backself.bar.create_oval(cx-r,cy-r,cxr,cyr,fillself.cfg[buttonbg],outlineself.cfg[buttonbg],widthself.scale_value(1))# 文本textself.bar.create_text((cx,cy),textstr(day),fillself.cfg[buttonfg],fontself.font)items(back,text)# 判断是否为选中状态is_selected(str(day)self.res_dayandself.view_yearint(self.res_year)andself.view_monthint(self.res_month))ifis_selected:self.bar.itemconfig(back,fillself.cfg[onbg])self.bar.itemconfig(text,fillself.cfg[onfg])self.selected_backback# 绑定事件data{items:items,back:back,text:text,day:str(day),is_sel:is_selected}foriteminitems:self.bar.tag_bind(item,Enter,lambda_,ddata:self._on_day_enter(d))self.bar.tag_bind(item,Leave,lambda_,ddata:self._on_day_leave(d))self.bar.tag_bind(item,Button-1,lambda_,ddata:self._on_day_click(d))self.day_elements.append(data)类方法截止本文TinUICalendarPicker只提供set_date方法。defset_date(self,year:int,month:int,day:int):self.res_yearstr(year)self.res_monthstr(month).zfill(2)self.res_daystr(day).zfill(2)self.view_yearyear self.view_monthmonth full_datef{self.res_year}-{self.res_month}-{self.res_day}self.self.itemconfig(self.main_text,textfull_date)ifhasattr(self,title_text):self._update_title_and_days()效果在Windows上tkinter除非是Tcl/Tk9.0及以上不过我也没用过画布的圆形锯齿非常明显这里是开了高DPI感知2的窗口演示。