## /ctk_extender/components/date_picker.py
import customtkinter as ctk
import datetime
import calendar
from typing import Any, Callable
from ..widget import Widget
__all__ = [
'DatePicker',
]
[документация]
class DatePicker(Widget):
"""
Интерактивный календарный виджет выбора даты.
Полностью автономен, перерисовывает дни месяца на лету.
"""
__slots__ = (
'_selected_date',
'_current_year',
'_current_month',
'_on_select_callback',
'_header_label',
'_days_frame',
'_day_buttons'
)
def __init__(
self,
parent: Any,
initial_date: datetime.date | None = None,
on_select: Callable[[datetime.date], None] | None = None,
geometry: Any = None,
**kwargs: Any
) -> None:
self._selected_date: datetime.date = initial_date or datetime.date.today()
self._current_year: int = self._selected_date.year
self._current_month: int = self._selected_date.month
self._on_select_callback: Callable[[datetime.date], None] | None = on_select
self._header_label: ctk.CTkLabel | None = None
self._days_frame: ctk.CTkFrame | None = None
self._day_buttons: list[ctk.CTkButton] = []
super().__init__(parent, geometry, **kwargs)
[документация]
def build(self) -> ctk.CTkFrame:
# Главный бокс календаря
cal_frame = ctk.CTkFrame(self.parent, border_width=1, border_color="#374151")
# 1. Шапка управления: Кнопка Левее, Месяц/Год, Кнопка Правее
nav_frame = ctk.CTkFrame(cal_frame, fg_color="transparent")
nav_frame.pack(side="top", fill="x", padx=10, pady=5)
ctk.CTkButton(nav_frame, text="<", width=28, command=lambda: self._change_month(-1)).pack(side="left")
self._header_label = ctk.CTkLabel(nav_frame, text="", font=("Arial", 12, "bold"))
self._header_label.pack(side="left", expand=True, fill="x")
ctk.CTkButton(nav_frame, text=">", width=28, command=lambda: self._change_month(1)).pack(side="right")
# 2. Строка дней недели (Пн, Вт, Ср...)
week_frame = ctk.CTkFrame(cal_frame, fg_color="transparent")
week_frame.pack(side="top", fill="x", padx=5)
days_shorts = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
for idx, d_name in enumerate(days_shorts):
lbl = ctk.CTkLabel(week_frame, text=d_name, font=("Arial", 10, "italic"), width=32)
lbl.grid(row=0, column=idx, padx=2)
# 3. Сетка дней месяца (Динамическая матрица grid)
self._days_frame = ctk.CTkFrame(cal_frame, fg_color="transparent")
self._days_frame.pack(side="top", fill="both", expand=True, padx=5, pady=(0, 5))
# Наполняем матрицу числами месяца
self._render_calendar_days()
return cal_frame
def _render_calendar_days(self) -> None:
"""Перерисовывает сетку кнопок дней для выбранного месяца и года."""
if not self._days_frame or not self._header_label: return
# Чистим старые кнопки прошлых месяцев
for btn in self._day_buttons:
btn.destroy()
self._day_buttons.clear()
# Обновляем текст шапки (например, "Июнь 2026")
months_names = ["", "Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь",
"Октябрь", "Ноябрь", "Декабрь"] #TODO: добавить локализацию
self._header_label.configure(text=f"{months_names[self._current_month]} {self._current_year}")
# Получаем матрицу дней месяца через стандартный модуль calendar
# Функция monthcalendar возвращает список недель, где дни вне месяца равны 0
cal_matrix = calendar.monthcalendar(self._current_year, self._current_month)
for row_idx, week in enumerate(cal_matrix):
for col_idx, day_num in enumerate(week):
if day_num == 0:
# Пустые дни по краям месяца забиваем невидимыми заглушками для сохранения сетки
continue
# Подсвечиваем выбранную дату другим цветом
is_selected = (
day_num == self._selected_date.day and
self._current_month == self._selected_date.month and
self._current_year == self._selected_date.year
)
bg_color = "#1D4ED8" if is_selected else ctk.ThemeManager.theme["CTkButton"]["fg_color"]
# Создаем кнопку конкретного дня
btn = ctk.CTkButton(
self._days_frame,
text=str(day_num),
width=32,
height=28,
fg_color=bg_color,
command=lambda d=day_num: self._select_day(d)
)
btn.grid(row=row_idx, column=col_idx, padx=2, pady=2)
self._day_buttons.append(btn)
def _change_month(self, direction: int) -> None:
"""Сдвигает календарную страницу вперед или назад."""
self._current_month += direction
if self._current_month > 12:
self._current_month = 1
self._current_year += 1
elif self._current_month < 1:
self._current_month = 12
self._current_year -= 1
self._render_calendar_days()
def _select_day(self, day: int) -> None:
"""Фиксирует выбор даты пользователем."""
self._selected_date = datetime.date(self._current_year, self._current_month, day)
self._render_calendar_days() # Перерисовываем для обновления подсветки кнопки
if self._on_select_callback:
try:
self._on_select_callback(self._selected_date)
except Exception:
pass