Определитель дней (Tk)

В отличие от программы, написанной с использованием PyQt, здесь приходится "эмулировать" поле даты, т.к. подобный компонент в Tkinter и его расширениях найти не удалось.

В таком случае сложность заключается в обработке того, что вводит пользователь или в ограничении возможности некорректного ввода. Причем второе предпочтительней.

Как задать "маску" для поля в Tkinter и есть ли вообще такая возможность? Скорее всего это сделать можно, но программный код получится достаточно длинным, т.к. придется обработать ввод каждого символа в конкретной позиции (см. программу "Поиск путей", где позволялся к вводу только один символ). Также, если использовать Entry, то желательно как-то автоматически подставлять точку или тире.

В программе ниже выход частично был найден в использовании компонента Spinbox, который позволяет выбирать значение из фиксированного количества значений (указывается "от" и "до" либо кортеж возможных значений). Проблема в том, что хотя при прокрутке пролистывается указанный диапазон, пользователь все равно в поле может вписать любой текст (в PyQt такой проблемы нет). Поэтому также необходимо предусмотреть обработку ввода пользователя. Например, не давать ему возможности вводить не те символы или автоматически исправлять значение, если фокус смещен из поля, а в нем значение некорректно. (Первый вариант предпочтительней). В программе данная проблема не решена.

Однако предусмотрена коррекция диапазона дня в зависимости от месяца или года. Так, если пользователь выбирает второй месяц, то поле дня позволяет вводить числа до 28 или 29 (в зависимости от указанного года). Если в поле дня стоит число больше 28 и пользователь выбирает второй месяц, то день автоматически установится на максимально возможный (28 или 29). То же самое касается месяцев из 30 дней.

Сравнивая разработку на PyQt и Tkinter можно сделать вывод, что Tkinter не является универсальной библиотекой. Его следует использовать только в программах, где требуются лишь основные компоненты, где накладывается мало ограничений на ввод пользователя. Другими словами, Tkinter подходит для простых программ с графическим интерфейсом и т.к. имеет более понятный и ясный синтаксис подходит для обучения. Следует заметить, что с использованием Tkinter были написаны достаточно крупные проекты (IDLE, графический редактор). Можно предположить, что в таком случае, программистам требовалось написать намного больше кода, чем если бы они использовали другую графическую библиотеку.

Внешний вид компонентов на "чистом" Tkinter достаточно уныл по сравнению с другими библиотеками.

Поскольку Tkinter входит в стандартную поставку пакета Python для Windows, то можно сказать, что программы, написанные с его использованием более переносимы.

Также, на наш взгляд, "менеджеры геометрии" (способы программирования расположения компонентов в окне) в Tkinter более развиты и логичны, чем в PyQt. Например, в PyQt есть сетка также как и в Tkinter. Но в PyQt компонент по умолчанию растягивается на всю ячейку, при этом указанные размеры компонента игнорируются. И похоже такое поведение отменить нельзя. Приходится использовать метод setGeometry.

Документация по Tkinter достаточно. Однако примеры в основном простые, показывающие как использовать основные компоненты.

from tkinter import *
import datetime
#########################################################
def day_control():
    """Фокус в day. Ограничение значение полей day
    в зависимости от установленного в month."""
    year = int(year1.get())
    if month1.get() == '2':
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day1.config(from_=1,to=29)
        else:
            day1.config(from_=1,to=28)
    else:        
        if month1.get() in ['4','6','9','11']:
            day1.config(from_=1,to=30)
        else:
            day1.config(from_=1,to=31)
 
def month_control():
    """Фокус в month. Автоматическое изменение полей day
    при изменении month."""
    year = int(year1.get())
    if day1.get()=='31' and (month1.get() in ['4','6','9','11']):
        day1.config(from_=1,to=30)
    elif month1.get() == '2' and (day1.get() in ['30','31','29']):
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day1.config(from_=1,to=29)
        else:
            day1.config(from_=1,to=28)
 
def year_control():
    """Фокус в year. Ограничение значение полей day
    в зависимости от года, если установлен в month находится 2."""
    year = int(year1.get())
    if month1.get() == '2':
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day1.config(from_=1,to=29)
        else:
            day1.config(from_=1,to=28)
########################################################
def day_control2():
    year = int(year2.get())
    if month2.get() == '2':
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day2.config(from_=1,to=29)
        else:
            day2.config(from_=1,to=28)
    else:        
        if month2.get() in ['4','6','9','11']:
            day2.config(from_=1,to=30)
        else:
            day2.config(from_=1,to=31)
 
def month_control2():
    year = int(year2.get())
    if day2.get()=='31' and (month2.get() in ['4','6','9','11']):
        day2.config(from_=1,to=30)
    elif month2.get() == '2' and (day2.get() in ['30','31','29']):
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day2.config(from_=1,to=29)
        else:
            day2.config(from_=1,to=28)
 
def year_control2():
    year = int(year2.get())
    if month2.get() == '2':
        if (year % 400 == 0) or ((year % 100 != 0) and (year % 4 == 0)):
            day2.config(from_=1,to=29)
        else:
            day2.config(from_=1,to=28)
 
################################################################
def radio_change():
    if radio_var.get() == 0:
        l_days.grid_remove()
        days.grid_remove()
        l_date2.grid(row=5,column=0)
        year2.grid(row=5,column=1)
        month2.grid(row=5,column=2)
        day2.grid(row=5,column=3)
    elif radio_var.get() == 1:
        l_date2.grid_remove()
        year2.grid_remove()
        month2.grid_remove()
        day2.grid_remove()
        l_days.grid(row=5,column=0,padx=15,sticky=E)
        days.grid(row=5,column=1,columnspan=3,sticky=W)
 
###############################################################
def result():
    if radio_var.get() == 0:
        y1 = int(year1.get())
        m1 = int(month1.get())
        d1 = int(day1.get())
        date1 = datetime.date(y1,m1,d1)
        y2 = int(year2.get())
        m2 = int(month2.get())
        d2 = int(day2.get())
        date2 = datetime.date(y2,m2,d2)
        diff = date1-date2
        diff = str(diff)
        if diff[0] == '-': diff = diff[1:]
        diff = diff.split()[0]
        ent.config(text=diff)
    else:
        y1 = int(year1.get())
        m1 = int(month1.get())
        d1 = int(day1.get())
        date1 = datetime.date(y1,m1,d1)
        diff = int(days.get())
        diff = datetime.timedelta(days=diff)
        date2 = date1 + diff
        date2 = str(date2)
        ent.config(text=date2)    
##################################################################
master = Tk()
master.title("Определитель дней")
master.geometry('-450-250')
master.resizable(False,False)
 
lf = LabelFrame(bg="lightblue",width=300)
lf.grid(row=1,column=0,columnspan=5,rowspan=2)
radio_var = IntVar()
radio_var.set(0)
radio1 = Radiobutton(lf,bg="lightblue",text="Количество дней между датами",variable=radio_var,value=0,command=radio_change)
radio2 = Radiobutton(lf,bg="lightblue",text="Дата через количество дней",variable=radio_var,value=1,command=radio_change)
radio1.grid(row=1,column=0,columnspan=5,sticky=W,padx=15)
radio2.grid(row=2,column=0,columnspan=5,sticky=W,padx=15)
 
color = "#880000"
lf2 = LabelFrame()
lf2.grid(row=3,column=0,columnspan=5,rowspan=5,sticky=W+E)
Label(lf2,text='год',font='Times 6').grid(row=3,column=1)
Label(lf2,text='месяц',font='Times 6').grid(row=3,column=2)
Label(lf2,text='день',font='Times 6').grid(row=3,column=3)
 
l_date1 = Label(lf2,text='Дата 1:')
year1_var = StringVar()
year1 = Spinbox(lf2,from_=1,to=3000,width=4,textvariable=year1_var,command=year_control)
month1_var = StringVar()
month1 = Spinbox(lf2,from_=1,to=12,width=2,textvariable=month1_var,command=month_control)
day1_var = StringVar()
day1 = Spinbox(lf2,from_=1,to=31,width=2,textvariable=day1_var,command=day_control)
l_date1.grid(row=4,column=0,padx=15)
year1.grid(row=4,column=1)
month1.grid(row=4,column=2)
day1.grid(row=4,column=3)
 
l_date2 = Label(lf2,text='Дата 2:')
year2_var = StringVar()
year2 = Spinbox(lf2,from_=1,to=3000,width=4,textvariable=year2_var,command=year_control2)
month2_var = StringVar()
month2 = Spinbox(lf2,from_=1,to=12,width=2,textvariable=month2_var,command=month_control2)
day2_var = StringVar()
day2 = Spinbox(lf2,from_=1,to=31,width=2,textvariable=day2_var,command=day_control2)
l_date2.grid(row=5,column=0,padx=15)
year2.grid(row=5,column=1)
month2.grid(row=5,column=2)
day2.grid(row=5,column=3)
 
l_days = Label(lf2,text='Дни:')
days = Spinbox(lf2,from_=-1000000,to=1000000,width=7)
 
but = Button(lf2,text="Найти",command=result)
ent = Label(lf2,width=12,justify=CENTER,bg="yellow",relief=GROOVE)
but.grid(row=6,column=0,pady=5)
ent.grid(row=6,column=1,columnspan=3,sticky=W+E)
########################################################################
date = datetime.date.today()
date = str(date)
date = date.split('-')
year1_var.set(date[0])
month1_var.set(date[1])
day1_var.set(date[2])
year2_var.set(date[0])
month2_var.set(date[1])
day2_var.set(date[2])
 
mainloop()