python 日期选择(包含时间选择)


前言

        前期项目需求写过一个日期多选的控件(Python Tkinter实现日历多选_楠奇湖畔的博客-CSDN博客_tkinter 日历控件),后面使用的时候发现有很多可以改进的部分。因此重新写了一个日历控件,可以单选或多选,此次只放了单选的代码。


一、参考文章

Python tkinter 下拉日历控件_我的眼_001的博客-CSDN博客

        此次还是参考了上述的文章,作为一名新手,基于学习吸收的原则,自己写代码的过程中,因为不能完全吸收看懂原代码,所以很多功能实现,使用了自己能看懂且不出现bug的代码来实现。所以有不妥之处,或可优化之处,或者有不明白之处,欢迎评论或私信指出,相互学习,感激不尽!

二、期望实现功能

        日历选择

        时间选择

        控件位置随唤醒按键动态调整体

                由唤醒控件在屏幕上的相对位置实现

三、实现步骤

1.引入库

        tkinter,calendar

import calendar
import tkinter as tk
from tkinter import ttk

2.代码实现

控件位置动态调整实现

        日历控件的宽高固定(w_w, w_h),知道唤醒控件按键的绝对坐标与宽高信息(geomtry),屏幕的宽高信息,就可以判断应该将控件置放哪一个点上(LU, RU, LD, RD):

                如果高度差(△h) > 控件高度(w_h),则置于LU/RU,反之为LD/RD

                如果宽度差(△w) > 控件宽度(w_w),则置于RU/RD,反之为LU/LD

唤醒按键信息获取

# 获取按键的宽高,相对起始坐标,只用到宽高信息,相对起始坐标无用处
widget_geometry = event.widget.winfo_geometry().replace('x', '+')
widget_width = int(widget_geometry.split('+')[0])
widget_height = int(widget_geometry.split('+')[1])

# 获取按键的绝对起始坐标
point_x, point_y = event.widget.winfo_rootx(), event.widget.winfo_rooty()

屏幕宽高获取 

screen_width = root.winfo_screenwidth()  # 显示器大小
screen_height = root.winfo_screenheight()

实现效果


 更优解建议

        现有代码中,将获取唤醒按键geometry信息和唤醒日历组件并赋值分成了两个函数实现,并且需要对按键绑定两次函数,查了下资料应该是可以实现用一个函数实现的,自己也在过程中这样操作了,将传参与事件放在了一个函数中,但是运行时,会出现按键点击后一直处于按下的状态,使用时,除了这个状态不对外,其它都正常,又找不到具体是那个地方错误,所以最后还是改成了分两次实现以避免这个bug,如果有更好的建议,欢迎大家分享,谢谢。(一个代码实现如下以及bug图:)

# 二合一
def set_date(event, time_type):
    global point_x, point_y, widget_width, widget_height
    # 根据点击事件获取组件的geometry,也可以用正则获得值
    widget_geometry = event.widget.winfo_geometry().replace('x', '+')
    widget_width = int(widget_geometry.split('+')[0])
    widget_height = int(widget_geometry.split('+')[1])
    point_x, point_y = event.widget.winfo_rootx(), event.widget.winfo_rooty()

    available_x = screen_width - point_x - widget_width  # 宽度差
    position = None  # 传给日历组件的起始坐标信息
    point = None  # 传给日历组件的置放位置信息
    if available_x >= 260 and point_y >= 280:  # 日历组件默认宽高:260,280
        position = (point_x + widget_width, point_y)
        point = 'RU'
        print('RU')
    elif available_x >= 260 and point_y < 280:
        position = (point_x + widget_width, point_y + widget_height)
        point = 'RD'
        print('RD')
    elif available_x < 260 and point_y >= 280:
        position = (point_x, point_y)
        point = 'LU'
        print('LU')
    elif available_x < 260 and point_y < 280:
        position = (point_x, point_y + widget_height)
        point = 'LD'
        print('LD')

    if time_type == 'start':
        date = Calendar(position=position, point=point).date_selection()
        start_time.set(date)
    elif time_type == 'end':
        date = Calendar(position=position, point=point).date_selection()
        end_time.set(date)
    elif time_type == 'diff':
        if len(start_time.get()) < 7 or len(end_time.get()) < 7:
            messagebox.showwarning(message='请先选择日期!')
        else:
            start = datetime.strptime(start_time.get(), '%Y-%m-%d %H:%M:%S')
            end = datetime.strptime(end_time.get(), '%Y-%m-%d %H:%M:%S')
            diff = (end - start)
            if end < start:
                diff_time.set('开始时间大于结束时间!')
            else:
                diff_time.set(diff)

# 按键绑定部分
btn_start_time = ttk.Button(text='开始时间:',) 
btn_start_time.bind("<Button-1>", lambda event: set_date(event, time_type='start'))
btn_start_time.grid(row=0, column=0, padx=5, pady=5)

完整代码

# -*- coding = utf-8 -*- 
# Author : Abner
# @Software : PyCharm

import calendar
import tkinter as tk
from tkinter import ttk
from tkinter import messagebox

datetime = calendar.datetime.datetime
timedelta = calendar.datetime.timedelta


class Calendar:
    def __init__(self, position=None, point=None):
        self.selection_state = None  # 用存放选中的日期(为日期序号,非具体日期)
        self.date_sele = None  # 选中的具体日期
        self.toplevel = tk.Toplevel()
        self.toplevel.bind("<FocusOut>", self._exit)  # 组件绑定事件函数,焦点在组件外触发函数
        self.toplevel.overrideredirect(1)  # 无工具栏模式
        self.toplevel.attributes('-alpha', 0.9)  # 设置透明度
        self.toplevel.withdraw()
        self.root_frame = ttk.Frame(self.toplevel)
        # 提前执行函数————————————————————————————
        self.update_time()
        self.button_style()
        self.creat_frame()
        self.first_floor()
        self.second_floor()
        self.third_floor()
        self.fourth_floor()
        self.root_frame.pack(expand=1, fill='both')
        self.toplevel.update()  # 问题终结者 update
        # 根据传入唤醒按键信息,设置窗口位置及大小 ——————————————————————————
        width, height = 260, 280
        if point == 'LU':
            self.toplevel.geometry('%dx%d+%d+%d' % (width, height, position[0] - width, position[1] - height))
        elif point == 'RU':
            self.toplevel.geometry('%dx%d+%d+%d' % (width, height, position[0], position[1] - height))
        elif point == 'LD':
            self.toplevel.geometry('%dx%d+%d+%d' % (width, height, position[0] - width, position[1]))
        elif point == 'RD':
            self.toplevel.geometry('%dx%d+%d+%d' % (width, height, position[0], position[1]))
        self.toplevel.deiconify()
        self.toplevel.focus()
        self.toplevel.wait_window()

    def update_time(self):
        date = datetime.now()
        self.date_year = date.year
        self.date_month = date.month
        self.date_day = date.day
        self.date_hour = date.hour
        self.date_minute = date.minute
        self.date_second = date.second

    def button_style(self):  # 这个设置按键样式的方法,一直没搞清楚,如果大家有专栏介绍的,可以推荐下
        style = ttk.Style(self.root_frame)
        arrow_layout = lambda dir: (
            [('Button.focus', {'children': [('Button.%sarrow' % dir, None)]})]
        )
        style.layout('L.TButton', arrow_layout('left'))
        style.layout('R.TButton', arrow_layout('right'))

    def creat_frame(self):
        self.first_frame = ttk.Frame(self.root_frame)
        self.first_frame.pack(pady=5)
        self.second_frame = ttk.Frame(self.root_frame)
        self.second_frame.pack(pady=5)
        self.third_frame = ttk.Frame(self.root_frame)
        self.third_frame.pack(pady=5)
        self.fourth_frame = ttk.Frame(self.root_frame)
        self.fourth_frame.pack(pady=5)

    def first_floor(self):  # 年月选择栏
        btn_left = ttk.Button(self.first_frame, style='L.TButton', command=self._prev_month)
        btn_left.grid(in_=self.first_frame, row=0, column=0, padx=5)
        btn_right = ttk.Button(self.first_frame, style='R.TButton', command=self._next_month)
        btn_right.grid(in_=self.first_frame, row=0, column=5, padx=5)
        self.combox_year = ttk.Combobox(self.first_frame, values=[str(year) for year in
                                                                  range(self.date_year, self.date_year - 11, -1)],
                                        width=5, state='readonly')
        self.combox_year.current(0)
        self.combox_year.grid(row=0, column=1, padx=5)
        lab_year = tk.Label(self.first_frame, text='年')
        lab_year.grid(row=0, column=2)
        self.combox_month = ttk.Combobox(self.first_frame, values=[str('%02d' % x) for x in range(1, 13)], width=5,
                                         state='readonly')
        self.combox_month.current(self.date_month - 1)  # 0~11
        self.combox_month.grid(row=0, column=3, padx=5)
        lab_month = tk.Label(self.first_frame, text='月')
        lab_month.grid(row=0, column=4)
        # 函数绑定下拉框
        self.combox_year.bind('<<ComboboxSelected>>', self._update)
        self.combox_month.bind('<<ComboboxSelected>>', self._update)

    def second_floor(self):  # 日历栏
        year = int(self.combox_year.get())
        month = int(self.combox_month.get())
        self.tk_cv = tk.Canvas(self.second_frame, bg='white', width=225, height=160)
        self.tk_cv.pack()
        # 标题
        cols = ['日', '一', '二', '三', '四', '五', '六']
        for i in range(len(cols)):
            coord = 10 + i * 30, 10, 40 + i * 30, 30
            rec_x = self.tk_cv.create_rectangle(coord, fill='red', outline='white')
            self.tk_cv.create_text(25 + i * 30, 20, text=cols[i])

        def on_click(event):
            x, y = (event.x - 10) // 30, (event.y - 30) // 20
            try:
                if self.selection_state is None:
                    self.tk_cv.itemconfig(rect[y][x], fill='lightgreen')
                    self.selection_state = rect[y][x]

                elif self.selection_state != rect[y][x]:
                    self.tk_cv.itemconfig(self.selection_state, fill='white')
                    self.tk_cv.itemconfig(rect[y][x], fill='lightgreen')
                    self.selection_state = rect[y][x]

                elif self.selection_state and self.selection_state == rect[y][x]:
                    self.tk_cv.itemconfig(rect[y][x], fill='white')
                    self.selection_state = None
            except:
                pass

        def date_get():
            c = calendar.TextCalendar(calendar.SUNDAY)  # 括号内内容表示以哪天为起始
            cal = c.monthdayscalendar(year, month)  # 只由日组成的一组二维数组,每七天一组列表内嵌成一组列表
            cal_dates = c.monthdatescalendar(year, month)  # 由日期组成的二维数组
            return cal, cal_dates

        xy = [10 + i * 30 for i in range(7)]
        dates_num, dates = date_get()
        rect = [[0] * 7 for _ in range(len(dates_num))]
        dates = sum(dates, [])
        dates_states = list(map(lambda x: [0, x], dates))
        for i in range(len(dates_num)):
            for j, x in enumerate(xy):
                if dates_num[i][j] != 0:
                    rect[i][j] = self.tk_cv.create_rectangle(x, 30 + i * 20, x + 30, 50 + i * 20, tags=('imgButton1'))
                    self.tk_cv.itemconfig(rect[i][j], outline='white')  # , fill='white', )
                    self.tk_cv.create_text(x + 15, 40 + i * 20, text='%02d' % dates_num[i][j], tags=('imgButton1'))
                    if dates_num[i][j] == self.date_day:
                        self.tk_cv.itemconfig(rect[i][j], fill='lightgreen')  # 高亮显示当天日期
                        self.selection_state = rect[i][j]

        self.date_state = dict(zip(sum(rect, []), dates_states))  # {15: [0, datetime.date(2022, 8, 1)],...}
        self.tk_cv.tag_bind('imgButton1', '<Button-1>', on_click)

    def third_floor(self):  # 时间栏
        self.combox_hour = ttk.Combobox(self.third_frame, values=[str('%02d' % x) for x in range(24)], width=3,
                                        state='readonly')
        self.combox_hour.current(self.date_hour)
        self.combox_hour.grid(row=0, column=0, padx=5)

        lab_hour = tk.Label(self.third_frame, text='时')
        lab_hour.grid(row=0, column=1)

        self.combox_minu = ttk.Combobox(self.third_frame, values=[str('%02d' % x) for x in range(60)], width=3,
                                        state='readonly')
        self.combox_minu.current(self.date_minute)
        self.combox_minu.grid(row=0, column=2, padx=5)

        lab_minute = tk.Label(self.third_frame, text='分')
        lab_minute.grid(row=0, column=3)

        self.combox_sec = ttk.Combobox(self.third_frame, values=[str('%02d' % x) for x in range(60)], width=3,
                                       state='readonly')
        self.combox_sec.current(self.date_second)
        self.combox_sec.grid(row=0, column=4, padx=5)

        lab_sec = tk.Label(self.third_frame, text='秒')
        lab_sec.grid(row=0, column=5)

    def fourth_floor(self):  # 确认与取消
        btn_submit = ttk.Button(self.fourth_frame, text='提 交', command=lambda: self._exit(confirm=1))
        btn_submit.grid(row=0, column=0, padx=10)

        btn_cancel = ttk.Button(self.fourth_frame, text='取 消', command=lambda: self._exit(confirm=2))
        btn_cancel.grid(row=0, column=1, padx=10)

    def _submit(self):
        # 正常模式下的提交代码
        if self.selection_state is None:
            self.date_sele = '未选择!'
        else:
            clock = '{}:{}:{}'.format(self.combox_hour.get(), self.combox_minu.get(), self.combox_sec.get())
            self.date_sele = self.date_state[self.selection_state][1].strftime('%Y-%m-%d') + ' ' + clock

    def _update(self, *args):
        self.update_time()
        self.tk_cv.pack_forget()
        self.second_floor()
        self.combox_hour.current(self.date_hour)
        self.combox_minu.current(self.date_minute)
        self.combox_sec.current(self.date_second)

    def _prev_month(self):
        date = datetime(int(self.combox_year.get()), int(self.combox_month.get()), 1)  # 数字1代表第几天
        date = date - timedelta(days=1)
        date = datetime(date.year, date.month, 1)
        self.combox_year.set(date.year)
        self.combox_month.set(date.month)
        self._update()

    def _next_month(self):
        date = datetime(int(self.combox_year.get()), int(self.combox_month.get()), 1)  # 数字1代表第几天
        date = date + timedelta(days=calendar.monthrange(date.year, date.month)[1] + 1)
        date = datetime(date.year, date.month, 1)
        self.combox_year.set(date.year)
        self.combox_month.set(date.month)
        self._update()

    def date_selection(self, *args):
        if self.date_sele is None:
            return '未选择!'
        else:
            return self.date_sele

    def _exit(self, confirm=1):
        try:
            # 点击combox等内容时,会出现focusout导致窗口关闭,加一个if判断避免
            if 'toplevel' not in str(self.toplevel.focus_displayof()) or confirm == 2:
                self.toplevel.destroy()
            elif confirm == 1:
                self._submit()
                self.toplevel.destroy()
        except:
            pass


if __name__ == '__main__':
    # 主窗口
    root = tk.Tk()
    root.title('时间计算器')
    root_width, root_height = 400, 110
    screen_width = root.winfo_screenwidth()  # 显示器大小
    screen_height = root.winfo_screenheight()
    x, y = (screen_width - root_width) / 2, (screen_height - root_height) / 2  # 窗口默认起始坐标
    root.geometry('%dx%d+%d+%d' % (root_width, root_height, x, y))
    root.resizable(0, 0)
    point_x, point_y = 0, 0  # 唤醒按键的起始坐标
    widget_width, widget_height = 0, 0  # 唤醒按键的宽高

    # -----------------------------------
    def set_position(event):
        global point_x, point_y, widget_width, widget_height
        # 根据点击事件获取组件的geometry,也可以用正则获得值
        widget_geometry = event.widget.winfo_geometry().replace('x', '+')
        widget_width = int(widget_geometry.split('+')[0])
        widget_height = int(widget_geometry.split('+')[1])
        point_x, point_y = event.widget.winfo_rootx(), event.widget.winfo_rooty()

    def set_date(time_type):
        available_x = screen_width - point_x - widget_width  # 宽度差
        position = None  # 传给日历组件的起始坐标信息
        point = None  # 传给日历组件的置放位置信息
        if available_x >= 260 and point_y >= 280:  # 日历组件默认宽高:260,280
            position = (point_x + widget_width, point_y)
            point = 'RU'
            print('RU')
        elif available_x >= 260 and point_y < 280:
            position = (point_x + widget_width, point_y + widget_height)
            point = 'RD'
            print('RD')
        elif available_x < 260 and point_y >= 280:
            position = (point_x, point_y)
            point = 'LU'
            print('LU')
        elif available_x < 260 and point_y < 280:
            position = (point_x, point_y + widget_height)
            point = 'LD'
            print('LD')

        if time_type == 'start':
            date = Calendar(position=position, point=point).date_selection()
            start_time.set(date)
        elif time_type == 'end':
            date = Calendar(position=position, point=point).date_selection()
            end_time.set(date)
        elif time_type == 'diff':
            if len(start_time.get()) < 7 or len(end_time.get()) < 7:
                messagebox.showwarning(message='请先选择日期!')
            else:
                start = datetime.strptime(start_time.get(), '%Y-%m-%d %H:%M:%S')
                end = datetime.strptime(end_time.get(), '%Y-%m-%d %H:%M:%S')
                diff = (end - start)
                if end < start:
                    diff_time.set('开始时间大于结束时间!')
                else:
                    diff_time.set(diff)


    # 显示部分——————————————————————————————
    start_time = tk.StringVar()
    start_time.set('请选择日期!')

    end_time = tk.StringVar()
    end_time.set('请选择日期!')

    diff_time = tk.StringVar()
    diff_time.set('请选择日期!')

    lab_start_time = ttk.Label(textvariable=start_time, font=25)
    lab_start_time.grid(row=0, column=1, padx=5, pady=5)
    lab_end_time = ttk.Label(textvariable=end_time, font=25)
    lab_end_time.grid(row=1, column=1, padx=5, pady=5)
    lab_diff_time = ttk.Label(textvariable=diff_time, font=25)
    lab_diff_time.grid(row=2, column=1, padx=5, pady=5)
    # 按键部分——————————————————————————————

    # 如果有更好的方法将两个函数合并一个,绑定给按键的话,欢迎评论或私信,感激不尽
    btn_start_time = ttk.Button(text='开始时间:', command=lambda: set_date(time_type='start'))
    btn_start_time.bind("<Button-1>", lambda event: set_position(event))
    btn_start_time.grid(row=0, column=0, padx=5, pady=5)
    btn_end_time = ttk.Button(text='结束时间:', command=lambda: set_date(time_type='end'))
    btn_end_time.bind("<Button-1>", lambda event: set_position(event))
    btn_end_time.grid(row=1, column=0, padx=5, pady=5)
    btn_diff_time = ttk.Button(text='开始计算:', command=lambda: set_date(time_type='diff'))
    btn_diff_time.grid(row=2, column=0, padx=5, pady=5)

    root.mainloop()

归纳

组件的关闭

        参考文章中,用root.after(self, ms, func=None, *args)方法来定时调用函数,监视实时焦点focus,如果焦点在组件外,就关闭组件。自己写的过程中,将此改成了事件触发的方式,给组件绑定了事件<FocusOut>,当组件失去焦点时,触发关闭函数。

# 绑定
self.toplevel.bind("<FocusOut>", self._exit)

# 设置焦点
self.toplevel.focus()

自定义按键风格

        这个样式设置,没搞清楚,等找到资料再补充


作为一名新手练习项目,不妥之处甚多,如有更多更好的建议,欢迎大家评论或私信指出,感谢!

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Python提供了多种方法来进行日期选择。引用中提到的一个例子是使用Python的Tkinter库实现的一个日历多选控件。这个控件可以让用户从一个日历界面中选择一个或多个日期。 另外,引用中的代码展示了使用Selenium库和WebDriver来选择日期的方法。在这个例子中,通过找到日期输入的元素,并使用send_keys方法来输入日期,即可实现日期选择。 还有一种方法是使用Python的datetime模块来处理日期。通过导入datetime模块,可以使用其中的函数和方法来创建日期对象、比较日期、格式化日期等等。这种方法适用于各种日期操作,不仅限于选择日期。 总结来说,Python提供了多种方法来进行日期选择,可以根据具体需求选择合适的方法。可以使用第三方库如Tkinter或Selenium,或者使用Python标准库中的datetime模块来实现日期选择。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [python 日期选择包含时间选择)](https://blog.csdn.net/abnerhu/article/details/126546388)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [带有源代码的 Python 简单电影预告片网站](https://download.csdn.net/download/qq_37270421/88253997)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [python 自动化 在日历中选择时间-Python中Selenium选择日期选择日历控件)的方法...](https://blog.csdn.net/weixin_39969611/article/details/109623136)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值