
Kawai.Focus - приложение для фокусировки внимания (часть 7)
Данная статья посвящена:
- Работе с фреймворком Kivy в проекте Kawai.Focus;
- Созданию экрана конструктора таймера;
- Реализации перехода с одного экрана на другой;
- Доработке экрана таймера.
Дополнительные материалы

Для скачивания материалов необходимо войти или зарегистрироваться
Файлы также можно получить в Telegram-боте по коду: 931883
Реклама

Вступление
Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 6) было сделано:
- Созданы и обновлены crud функции для таймера;
- Написан декоратор для обработки ошибок
@crud_error_guard
; - Добавлена pydantic схема
TimerListModel
для списка таймеров; - Написаны тесты на
pytest
для crud функций; - Тесты из
validators_tests.py
превращены в тесты для схемschemas_tests.py
.
Было написано много нового функционала, который мне предстоит связать с новыми экранами.
Сегодня я создам экран конструктора таймера, подключу к нему CRUD-функцию new_timer()
и покажу, как реализовать переход от одного таймера к другому. Затем я запущу конструктор и проверю, как он справляется с созданием нового таймера.
Обратите внимание: в этой статье конструктор и таймер представлены в предварительной версии. Это скорее демо, чем завершённый продукт — работа над доработкой ещё продолжается.
Заваривайте чай, доставайте вкусняшки — пора “выращивать новые помидоры”! 🍅
Конструктор таймера
Конструктор таймера нужен для того что-бы создать новый таймер с уникальным именем и гибкими настройками под пользователя. У таймера должны настраиваться следующие поля:
title
— название таймера;pomodoro_time
— время/длительность одного помидора;break_time
— время/длительность перерыва между помидорами;break_long_time
— время/длительность длительного перерыва;count_pomodoro
— количество помидоров.
Пока экран таймера TimerScreen
будет поддерживает только поле pomodoro_time
. Для использования остальных полей и их функциональности в дальнейшем потребуется модернизация кода таймера и его логики. Пока же я сосредоточусь на реализации конструктора таймера.
timer_constructor_screen.kv
Сначала в папке kv
, в котором находится UI экранов я создам файл timer_constructor_screen.kv
, который будет отвечать за отрисовку элементов конструктора таймера:
#:kivy 2.3.1
<TimerConstructorScreen>:
FloatLayout:
# поле вовода "название"
TextInput:
size_hint: None, None
size: 220, 30
pos_hint: {"center_x": 0.5, "center_y": 0.9}
hint_text: "название"
# числовой счётчик "время помидора"
Label:
text: "помидор"
size_hint: None, None
pos_hint: {"center_x": 0.3, "center_y": 0.7}
Button:
text: "-"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.5, "center_y": 0.7}
on_press: time_tomato_input.decrement()
TimeTomatoInput:
id: time_tomato_input
size_hint: None, None
size: 40, 30
pos_hint: {"center_x": 0.6, "center_y": 0.7}
Button:
text: "+"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.7, "center_y": 0.7}
on_press: time_tomato_input.increment()
# числовой счётчик "время прерыва"
Label:
text: "перерыв"
size_hint: None, None
pos_hint: {"center_x": 0.3, "center_y": 0.6}
Button:
text: "-"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.5, "center_y": 0.6}
on_press: time_break_input.decrement()
TimeBreakInput:
id: time_break_input
size_hint: None, None
size: 40, 30
pos_hint: {"center_x": 0.6, "center_y": 0.6}
Button:
text: "+"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.7, "center_y": 0.6}
on_press: time_break_input.increment()
# числовой счётчик "время длительного перерыва"
Label:
text: "перерывище"
size_hint: None, None
pos_hint: {"center_x": 0.3, "center_y": 0.5}
Button:
text: "-"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.5, "center_y": 0.5}
on_press: time_long_break_input.decrement()
TimeLoongBreakInput:
id: time_long_break_input
size_hint: None, None
size: 40, 30
pos_hint: {"center_x": 0.6, "center_y": 0.5}
Button:
text: "+"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.7, "center_y": 0.5}
on_press: time_long_break_input.increment()
# числовой счётчик "колличество помидоров"
Label:
text: "помидоров"
size_hint: None, None
pos_hint: {"center_x": 0.3, "center_y": 0.4}
Button:
text: "-"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.5, "center_y": 0.4}
on_press: count_tomatos_input.decrement()
CountTomatosInput:
id: count_tomatos_input
size_hint: None, None
size: 40, 30
pos_hint: {"center_x": 0.6, "center_y": 0.4}
Button:
text: "+"
size_hint: None, None
size: 40, 40
pos_hint: {"center_x": 0.7, "center_y": 0.4}
on_press: count_tomatos_input.increment()
# Кнопка "Создать"
Button:
text: 'Создать'
size_hint: None, None
height: 40
width: 100
pos_hint: {'center_x': 0.7, 'center_y': 0.1}
on_press: root.create_timer(self)
# Кнопка "Назад"
Button:
text: 'Назад'
size_hint: None, None
height: 40
width: 100
pos_hint: {'center_x': 0.3, 'center_y': 0.1}
on_press: root.back(self)
Разбор файла разметки UI:
- TextInput "название" — поле для ввода названия таймера:
hint_text: "название"
— чем-то похож на плейсхолдер html, который нужен для отображения текста в поле (сотрётся при начале ввода).
- Label "помидор" — подпись для счётчика рабочего времени;
- TimeTomatoInput — кастомный числовой счётчик длительности одного "помидора";
- Button "+" — кнопки для увеличения значения на 1:
on_press: time_long_break_input.increment()
— по нажатию на кнопку вызовет методincrement()
, который увеличит число ввода на 1.
- Button "-" — кнопки для уменьшения значения на 1:
on_press: time_tomato_input.decrement()
— по нажатию на кнопку вызовет методdecrement()
, который уменьшит число ввода на 1;
- Label "перерыв" — подпись для счётчика короткого перерыва;
- TimeBreakInput — кастомный числовой счётчик короткого перерыва;
- Label "перерывище" — подпись для счётчика длинного перерыва;
- TimeLoongBreakInput — кастомный числовой счётчик длительного перерыва;
- Label "помидоров" — подпись для счётчика количества рабочих циклов;
- CountTomatosInput — кастомный числовой счётчик количества "помидоров";
- Button "Создать" — кнопка для запуска создания таймера;
on_press: root.create_timer(self)
— по нажатию на кнопку вызовет методcreate_timer()
, который создаст новый таймер.
- Button "Назад" — кнопка возврата на предыдущий экран:
on_press: root.back(self)
— по нажатию на кнопку вызовет методback()
, который перенесёт на предыдущий экран "Список таймеров" (пока там будет заглушка).
Теперь, когда kv-файл конструктора таймера готов, я начну писать Python-код, который будет с ним взаимодействовать.
timer_wigets.py
Теперь я создам кастомные виджеты для числовых счётчиков. По крайней мере, я не нашёл в Kivy подходящего виджета под свои нужды, поэтому решил реализовать свой собственный.:
from kivy.uix.textinput import TextInput
class BaseNumInput(TextInput):
"""Базовый класс для поля числа"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = '0'
self.halign = 'center'
def increment(self):
"""Метод для прибавки 1"""
self.text = str(int(self.text) + 1)
def decrement(self):
"""Метод для вычитания 1"""
self.text = str(int(self.text) - 1)
class TimeTomatoInput(BaseNumInput):
"""Класс для поля ввода колличества помидорово"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = '25'
class TimeBreakInput(BaseNumInput):
"""Класс для поля ввода времени перерыва"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = '5'
class TimeLoongBreakInput(BaseNumInput):
"""Класс для поля ввода времени длительного перерыва"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = '15'
class CountTomatosInput(BaseNumInput):
"""Класс для поля ввода количества помидоров"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.text = '4'
Разбор кода:
- `BaseNumInput:
- Базовый класс для виджета числового счётчика;
- Наследует от
TextInput
(класс для ввода текста в поле); - Устанавливает текст по умолчанию в
'0'
; - Центрирует ввод в
self.halign = 'center'
; - Содержит методы
increment()
иdecrement()
для изменения значения на ±1.
TimeTomatoInput
:- Класс для числового счётчика время помидора;
- Инициализируется со значением по умолчанию
'25'
минут.
TimeBreakInput
:- Устанавливает значение по умолчанию
'5'
минут; - Используется для настройки коротких перерывов между сессиями.
- Устанавливает значение по умолчанию
TimeLoongBreakInput
:- Значение по умолчанию —
'15'
минут; - Предназначен для длительных восстановительных пауз.
- Значение по умолчанию —
CountTomatosInput
:- Инициализируется числом
'4'
минуты; - Предназначен для настройки количества помидоров.
- Инициализируется числом
timer_constructor_screen.py
После того как я закончил создания UI создаю .py
файл для экрана timer_constructor_screen.py
в пакете screens
:
from kivy.uix.screenmanager import Screen
from kawai_focus.custom_widgets.timer_wigets import TimeTomatoInput
from kawai_focus.schemas import TimerModel
from kawai_focus.database.cruds import new_timer
class TimerConstructorScreen(Screen):
"""Экран конструктора таймера"""
def __init__(self, **kwargs):
super(TimerConstructorScreen, self).__init__(**kwargs)
def create_timer(self, instance):
"""Метод для создания таймера"""
timer = new_timer(
data=TimerModel(
title=self.ids.title.text,
pomodoro_time=int(self.ids.time_tomato_input.text),
break_time=int(self.ids.time_break_input.text),
break_long_time=int(self.ids.time_long_break_input.text),
count_pomodoro=int(self.ids.count_tomatos_input.text)
)
)
if timer:
screen_timer = self.manager.get_screen('timers_screen')
screen_timer.timer = timer
# Todo: временное решение: нужно сделать механизм для
# автоматического рассчёта часов, минут и секунд из
# колличества минут, введённого пользователем
screen_timer.ids.time_label.text = f'00:{
"0" + str(timer.pomodoro_time) if timer.pomodoro_time <= 9 else timer.pomodoro_time
}:00'
self.manager.current = 'timers_screen'
def back(self, instance) -> None:
"""Метод для кнопки назад - возврат в меню таймеров"""
pass
Разбор кода:
- Импортируется класс
Screen
из модуляkivy.uix.screenmanager
для создания пользовательского экрана ; - Импортируются пользовательские классы и функции:
TimeTomatoInput
(виджет ввода),TimerModel
(схема таймера),new_timer
(CRUD-функция создания таймера) ; - Создаётся класс
TimerConstructorScreen
, наследующийScreen
, представляющий экран конструктора таймера ; - В конструкторе
__init__
вызывается инициализация родительского класса черезsuper()
; - Метод
create_timer
реализует функциональность по созданию нового таймера на основе данных, введённых пользователем ; - Значения для
TimerModel
берутся из виджетов экрана черезself.ids
, все поля преобразуются вint
; - Созданный объект таймера сохраняется через функцию
new_timer
и, если успешно, передаётся экрану с таймерами (timers_screen
) ; - Поле
time_label
на экране таймера обновляется вручную, чтобы отобразить минуты в формате00:MM:00
; - После установки таймера активируется экран
timers_screen
путём изменения текущего экранаself.manager.current
(переход на экран созданного таймера после его создания); - Метод
back
определён, но не реализован — предназначен для возврата в главное меню таймеров (пока не будет создан экран "Список таймеров" тут будет заглушкаpass
).
main.py
После того как новый экран написан его нужно подключить в главном файле main.py
:
# Другой код
from kawai_focus.screens.timer_constructor_screen import TimerConstructorScreen
class KawaiFocusApp(App):
"""Класс для создания приложения"""
title = 'Kawai.Focus'
def build(self):
# Другой код
Builder.load_file('kv/timer_constructor_screen.kv')
# Другой код
screen_manager.add_widget(TimerConstructorScreen(name='timer_constructor_screen')
# Другой код
Разбор нового кода:
- Импорт
TimerConstructorScreen
конструктора класса изkawai_focus.screens.timer_constructor_screen
; Builder.load_file('kv/timer_constructor_screen.kv')
— загрузка kv файла конструктора таймера;screen_manager.add_widget(TimerConstructorScreen(name='timer_constructor_screen')
— добавление экрана конструктора таймера вscreen_manager
.
Таймер
Теперь осталось доработать таймер так, чтобы он принимал значения, передаваемые через его конструктор. Пока он работает только с минутами и секундами, но в следующей статье я планирую добавить поддержку часов — они будут вычисляться на основе минут. В текущей версии программа завершится с ошибкой, если ввести более 59 минут.
Изменения в TimerScreen
:
class TimerScreen(Screen):
"""Экран таймера"""
# Другой код
def __init__(self, **kwargs):
# Другой код
self.timer = None
def start_timer(self, instance) -> None:
"""Метод для запуска таймера"""
# Другой код
self.valid_timer_data = TimerTimeModel(mm=self.timer.pomodoro_time)
# Другой код
def stop_timer(self, instance) -> None:
"""Метод для остановки таймера"""
# Другой код
self.remaining_time = TimerTimeModel(mm=self.timer.pomodoro_time).mm
# Todo: временное решение: нужно сделать механизм для
# автоматического рассчёта часов, минут и секунд из
# колличества минут, введённого пользователем
self.ids.time_label.text = f'00:{
"0" + str(self.remaining_time) if self.remaining_time <= 9 else self.remaining_time
}:00'
# Другой код
Разбор нового кода:
self.timer = None
Инициализация атрибутаtimer
со значениемNone
. Это означает, что таймер ещё не создан или не запущен. Позже этот атрибут должен получить объект с параметрами таймера;self.valid_timer_data = TimerTimeModel(mm=self.timer.pomodoro_time)
Создание экземпляра классаTimerTimeModel
, используя значениеpomodoro_time
из текущего таймера;self.remaining_time = TimerTimeModel(mm=self.timer.pomodoro_time).mm
Повторное создание объектаTimerTimeModel
, но теперь сразу извлекается количество минут (mm
) и сохраняется в переменнуюremaining_time
. Это значение будет использоваться для обновления интерфейса;self.ids.time_label.text = f'00:{ "0" + str(self.remaining_time) if self.remaining_time <= 9 else self.remaining_time }:00'
Форматирование и отображение времени в формате00:MM:00
, гдеMM
— этоremaining_time
. Если значение меньше 10, добавляется ведущий ноль для корректного отображения времени (например,00:05:00
).
Создание нового таймера
Настало время проверить работу конструктора таймера и продемонстрировать этот процесс. Для этого запускаю приложение через терминал с помощью Poetry:
poetry run kawai-focus
Таймер успешно создан
Этот сценарий предполагает, что конструктор успешно создаёт новый таймер и переводит пользователя на экран его настройки. При запуске отображается экран конструктора:

Для числовых значений установлены значения по умолчанию — они представляют собой рабочий пример настроек. Пользователь может быстро создать таймер, просто введя название, либо изменить часть параметров по своему усмотрению.
Далее я создаю таймер, редактирую несколько полей и нажимаю кнопку "Создать":

Как видно на скриншоте, таймер успешно сохранён в базе данных, и пользователь перенаправлен на экран нового таймера. Для проверки нажимаю кнопку "Старт":

Таймер корректно запускается и начинает обратный отсчёт минут и секунд:

Таймер не удалось создать
Теперь проверим поведение приложения при некорректном вводе. Пока в коде нет валидации и механизма, предотвращающего создание ошибочного таймера. Я решил использовать это как возможность показать, как отсутствие валидации может повлиять на поведение приложения.
Для моделирования ошибки: не ввожу название таймера, уменьшаю одно из числовых значений до отрицательного значения (с помощью кнопки "−") и нажимаю кнопку "Cоздать":

Приложение не упало — запись сохранилась в базе данных, а пользователь был перенаправлен на экран таймера. Название отсутствует, а "время помидора" оказалось равным -16
. Выглядит это неожиданно: 00:0-16:00
. Я понимаю, что при запуске такого таймера приложение наверняка упадёт. Но в этом и была моя цель — протестировать на прочность. Итак, запускаю таймер:

Как и ожидалось, сработала валидация Pydantic — отрицательное значение -16
вызвало ошибку:

Выводы:
- Необходима валидация данных конструктора;
- Поля с числовыми значениями должны иметь ограничения, исключающие отрицательные числа;
- Поле «Название» должно выделяться при отсутствии введённого текста, и сохранение пустого названия нужно запретить;
- Это хороший пример того, зачем нужна валидация и обработка ошибок в интерфейсах и формах.
Анонс на следующие статьи
Следующая моя статья выйдет 10 июля 2025 года. В ней я продолжу разработку функционала таймера и его конструктора.
На текущий момент в экране таймера реализованы не все возможности и настройки. В частности, необходимо добавить механизм воспроизведения цепочки таймеров — они должны запускаться последовательно, с предусмотренными перерывами между ними.
Также требуется реализовать валидацию вводимых данных в конструкторе таймеров, чтобы избежать некорректного ввода и повысить стабильность приложения.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Поздравление канала
Поздравляю канал Код на салфетке с двухлетием! День рождения канала — 18 июня 2025 года. Желаю ему развития и процветания в наше непростое время. За время своего существования канал многое мне дал, и я с радостью продолжу поддерживать его и замечательное комьюнити.
Сейчас на канале проходит конкурс — присоединяйтесь! Подписывайтесь на Код на салфетке в Telegram — будет весело=)

Заключение
- Написана демонстрационная версия конструктора таймера;
- Реализован переход между экранами;
- Обновлён код таймера для работы с новыми данными;
- Продемонстрировано создание нового таймера в двух сценариях:
- Таймер успешно создан;
- Таймер не удалось создать;
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта Kawai.Focus.
Все статьи