Вступление
Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 9) было сделано:
- Реализован функционал запуска таймеров друг за другом по цепочке;
- На экран таймера выведены его название и тип;
- Добавлен механизм скрытия и показа кнопки «Стоп».
Сегодня я реализую экран «Таймеры», который позволит выбрать любой из созданных таймеров и использовать его. Также добавлю кнопку «Назад», чтобы пользователь мог вернуться из любого таймера цепочки на экран «Таймеры».
Кроме того, планирую сделать кнопку «Удалить». После этого функционал для MVP1 будет практически завершён, и можно будет пользоваться базовой версией таймера Pomodoro.
Заваривайте чай, доставайте вкусняшки — пора “собирать урожай помидоров с грядки”! 🍅
Экран «Таймеры»
Экран «Таймеры» нужен для удобной навигации по созданным пользователем таймерам. Из него можно будет нажать кнопку «Новый таймер» или выбрать один из существующих для использования.
Для удобства в кнопках таймеров будет кратко отображаться следующая информация:
- название;
- время одного «помидора»;
- количество «помидоров».
timers_screen.kv
Создаю файл timers_screen.kv
в папке kv
для экрана со списком таймеров:
#:kivy 2.3.1
<TimersScreen>:
FloatLayout:
RecycleView:
id: timers_view
size_hint: 1, 1
pos_hint: {"x": 0, "y": 0}
viewclass: "TimerButton"
RecycleBoxLayout:
default_size: None, dp(50)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: "vertical"
spacing: dp(5)
Button:
id: new_timer
text: 'Новый таймер'
size_hint: None, None
height: 40
width: 120
pos_hint: {"center_x": 0.3, "center_y": 0.1}
on_release: root.switch_new_timer(self)
<TimerButton@Button>:
timer_id: None
text: ""
on_release: app.root.get_screen("timers_screen").switch_timer(self)
size_hint: None, None
height: dp(50)
background_normal: "" # отключаем стандартный фон
background_color: 0, 0, 0, 0 # прозрачный
canvas.before:
Color:
rgba: 1, 1, 1, 1 # белый контур
Line:
width: 1.5
rectangle: (self.x, self.y, self.width, self.height)
Color:
rgba: (0, 0.5, 1, 0.5) if self.state "down" else (0, 0, 0, 0)
Rectangle:
pos: self.pos
size: self.size
Разбор содержимого kv файла:
- Корневой виджет —
<TimersScreen>
содержитFloatLayout
, который управляет расположением всех дочерних элементов; - RecycleView — отображает список таймеров:
- id:timers_view
;
- size_hint:1, 1
— занимает всё пространство;
- pos_hint:{"x": 0, "y": 0}
— прикреплён к нижнему левому углу;
- viewclass:TimerButton
— каждый элемент списка будет кнопкой с кастомным оформлением.
- RecycleBoxLayout внутри RecycleView:
- default_size:None, dp(50)
— каждая кнопка по высоте 50 dp;
- default_size_hint:1, None
— ширина растягивается на весь контейнер, высота фиксированная;
- size_hint_y:None
— высота RecycleBoxLayout определяется суммой дочерних элементов;
- height:self.minimum_height
— минимальная высота учитывает все элементы;
- orientation:vertical
— элементы располагаются вертикально;
- spacing:dp(5)
— расстояние между кнопками. - Кнопка "Новый таймер" — добавляет возможность создать новый таймер:
- id:new_timer
;
- text:"Новый таймер"
;
- size:height: 40
,width: 120
;
- pos_hint:{"center_x": 0.3, "center_y": 0.1}
— около нижней части экрана слева;
- on_release: вызывает методswitch_new_timer(self)
у корневого виджета. - Класс кнопки таймера —
<TimerButton@Button>
представляет элемент списка RecycleView:
- timer_id: свойство для хранения идентификатора таймера;
- text: пустой по умолчанию;
- on_release: вызывает методswitch_timer(self)
у экранаtimers_screen
;
- size:height: dp(50)
,size_hint: None, None
;
- background_normal:""
— отключает стандартный фон кнопки;
- background_color:0, 0, 0, 0
— делает фон прозрачным;
- canvas.before:
- Рисуется белый контур с шириной 1.5 пикселя по границам кнопки;
- При нажатии кнопка подсвечивается полупрозрачным синим цветом(0, 0.5, 1, 0.5)
;
- Иначе фон прозрачный(0, 0, 0, 0)
.
cruds.py
Мне нужно изменить функцию list_timers()
, чтобы она выводила список таймеров в формате, подходящем для работы с RecycleView
. Этот виджет использует список словарей (data
) для отображения данных.
Кроме того, валидировать данные после получения из БД нет необходимости, так как в локальной базе всегда будут храниться корректные значения.
Старый код list_timers()
:
@crud_error_guard
def list_timers() -> list[TimerListModel]:
"""Функция для получения списка таймеров"""
with db.get_session() as session:
query = select(Timer.id, Timer.title)
result = session.execute(query)
timers = result.mappings().fetchall()
return [TimerListModel.model_validate(obj=accept, from_attributes=True) for accept in timers]
Обновлённый код list_timers()
:
@crud_error_guard
def list_timers() -> list[dict[str, int | str]]:
"""Функция для получения списка таймеров"""
with db.get_session() as session:
query = select(
Timer.id,
Timer.title,
Timer.pomodoro_time,
Timer.count_pomodoro
)
result = session.execute(query)
timers = result.mappings().fetchall()
return [
{
'timer_id': timer.id,
'text': (
f'{timer.title}\n'
f'Помидоров: {timer.count_pomodoro}, '
f'размер помидора: {timer.pomodoro_time}'
)
}
for timer in timers
]
Разбор обновлённого кода:
Timer.pomodoro_time
— количество минут, установленное для одного "помидора" в таймере;Timer.count_pomodoro
— количество помидоров (циклов работы) в данном таймере;список словарей
— структура данных, которая возвращается функцией; каждый словарь содержит:'timer_id': timer.id
— уникальный идентификатор таймера;'text'
— строка для отображения на кнопке/экране, включающая:f'{timer.title}\n'
— название таймера с переносом строки;f'Помидоров: {timer.count_pomodoro}, '
— количество циклов работы;f'размер помидора: {timer.pomodoro_time}'
— длительность одного помидора.
Теперь функция готова к внедрению в функционал экрана «Таймеры».
timers_screen.py
Я хочу чтобы экран «Таймеры» обладал следующим функционалом:
- Отображал кнопки с данными таймеров;
- Мог переключить на экран выбранного таймера;
- Переключал на экран «Конструктор» для создания нового таймера.
Импорты:
from kivy.uix.screenmanager import Screen
from kawai_focus.utils.utils import calculate_time, gen_types_timers
from kawai_focus.database.cruds import get_timer
Разбор импортов:
from kivy.uix.screenmanager import Screen
— импорт классаScreen
для создания экранов в Kivy;from kawai_focus.utils.utils import calculate_time, gen_types_timers
— импорт утилит:calculate_time
— функция для форматирования времени;gen_types_timers
— генератор или функция для перебора типов таймеров.
from kawai_focus.database.cruds import get_timer
— импорт функции для получения таймера из базы данных.
Класс TimersScreen
:
class TimersScreen(Screen):
"""Экран таймеры"""
def __init__(self, **kwargs):
super(TimersScreen, self).__init__(**kwargs)
def switch_new_timer(self, instance) -> None:
"""Метод для переключения на экран Конструктор таймера"""
self.manager.current = 'timer_constructor_screen'
def switch_timer(self, instance) -> None:
"""Метод для кнопки переключения на выбранный таймер"""
timer = get_timer(timer_id=instance.timer_id)
screen_timer = self.manager.get_screen('timer_screen')
screen_timer.timer = timer
time_culc = calculate_time(mm_user=timer.pomodoro_time)
screen_timer.timer_start_time = time_culc
screen_timer.ids.time_label.text = time_culc
screen_timer.ids.title_label.text = timer.title
screen_timer.source_timer_names = gen_types_timers(count_pomodoro=timer.count_pomodoro)
self.manager.state_machine = screen_timer.source_timer_names.copy()
self.manager.current = 'timer_screen'
Разбор кода:
class TimersScreen(Screen)
— экран для отображения списка таймеров;__init__
— стандартный конструктор, вызывает конструктор родителяScreen
;switch_new_timer(self, instance)
— переключение на экран конструктора нового таймера (timer_constructor_screen
);switch_timer(self, instance)
— обработка выбора существующего таймера:- получает таймер из базы (
get_timer
) поtimer_id
; - сохраняет таймер в экран
timer_screen
; - вычисляет и отображает стартовое время таймера (
calculate_time
); - обновляет заголовок экрана и список типов таймеров (
gen_types_timers
); - копирует список таймеров в
state_machine
; - переключает текущий экран на
timer_screen
.
- получает таймер из базы (
main.py
Теперь осталось подключить новый экран в main.py
для его использования.
Импорты:
from kawai_focus.screens.timers_screen import TimersScreen
from kawai_focus.database.cruds import list_timers
Разбор импортов:
TimersScreen
— класс для экрана «Таймеры»;list_timers
— crud функция для вывода списка таймеров.
Обновлённый код класса KawaiFocusApp
:
class KawaiFocusApp(App):
"""Класс для создания приложения"""
title = 'Kawai.Focus'
def build(self):
# Загрузка kv файла
Builder.load_file('kv/timer_screen.kv')
Builder.load_file('kv/timer_constructor_screen.kv')
Builder.load_file('kv/timers_screen.kv')
screen_manager = ScreenManager()
timers = list_timers()
timers_screen = TimersScreen(name='timers_screen')
timers_screen.ids.timers_view.data = timers
screen_manager.add_widget(timers_screen)
screen_manager.add_widget(TimerConstructorScreen(name='timer_constructor_screen'))
screen_manager.add_widget(TimerScreen(name='timer_screen'))
return screen_manager
Разбор обновлённого кода:
Builder.load_file('kv/timers_screen.kv')
— загружает KV-файл с разметкой экрана таймеров;timers = list_timers()
— получает список таймеров из базы данных в виде списка словарей;timers_screen = TimersScreen(name='timers_screen')
— создаёт экземпляр экрана таймеров с указанным именем;timers_screen.ids.timers_view.data = timers
— передаёт данные таймеров в виджетRecycleView
для отображения;screen_manager.add_widget(timers_screen)
— добавляет экран таймеров в менеджер экранов для навигации.
На этом код для экрана «Таймеры» завершён и готов к использованию.
Экран «Таймер»
Перехожу к обновлению кода экрана «Таймер» под новый функционал.
Мне нужно подключить функционал к следующим кнопкам:
- «Назад» — возврат на экран «Таймеры»;
- «Удалить» — удаляет текущий таймер.
timer_screen.kv
В конец timer_screen.kv
добавляю следующие строки:
Label:
id: title_warning
text: ""
color: (1, 0.3, 0.3, 1)
font_size: 14
size_hint_y: None
height: 20
pos_hint: {"center_x": 0.5, "center_y": 0.4}
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)
Button:
id: del_timer
text: "Удалить"
size_hint: None, None
height: 40
width: 120
pos_hint: {"center_x": 0.7, "center_y": 0.1}
on_press: root.delete_timer(self)
Разбор новых строк из kv файла:
Label
— лейбл с пустой строкой для отображения предупреждения об удалении;Button
— кнопки «Назад» и «Удалить»:on_press: root.back(self)
— при нажатии использует метод классаTimerScreen
для выхода на экран «Таймеры»;on_press: root.delete_timer(self)
— при нажатии использует метод классаTimerScreen
для удаления таймера.
timer_screen.py
Пора реализовать методы back()
и delete_timer()
для подключения к кнопкам.
Импортирую crud функции для удаления таймера и вывода списка таймеров:
from kawai_focus.database.cruds import del_timer, list_timers
Код метода back()
:
def back(self, instance) -> None:
"""Метод для кнопки назад - возврат в меню таймеров"""
if self.ids.title_warning.text:
self.ids.title_warning.text = ''
self.ids.del_timer.color = (1, 1, 1, 1)
self.manager.current = 'timers_screen'
Разбор кода:
self.ids.title_warning.text = ''
— очищает предупреждение, если оно отображается (title_warning.text
);self.ids.del_timer.color = (1, 1, 1, 1)
— сбрасывает цвет кнопки удаления на стандартный белый;self.manager.current = 'timers_screen'
— переключает экран на список таймеров (timers_screen
).
Код метода delete_timer()
:
def delete_timer(self, instance) -> None:
"""Метод для удаления таймера"""
if not self.ids.title_warning.text:
self.ids.title_warning.text = (
'Вы действительно хотите удалить таймер?\n'
'Если да, то нажмите кнопку "удалить" ещё раз.'
)
self.ids.del_timer.color = (1, 0.3, 0.3, 1)
else:
del_timer(timer_id=self.timer.id)
timers = list_timers()
timers_screen = self.manager.get_screen('timers_screen')
timers_screen.ids.timers_view.data = timers
self.manager.current = 'timers_screen'
Разбор кода:
if not self.ids.title_warning.text:
— проверяет, показано ли предупреждение; если нет, то нужно его отобразить:self.ids.title_warning.text = 'Вы действительно хотите удалить таймер?\nЕсли да, то нажмите кнопку "удалить" ещё раз.'
— выводит текст предупреждения для подтверждения удаления;self.ids.del_timer.color = (1, 0.3, 0.3, 1)
— меняет цвет кнопки на красный, чтобы подчеркнуть действие удаления.
else:
— если предупреждение уже показано, выполняется удаление таймера:del_timer(timer_id=self.timer.id)
— удаляет таймер из базы данных по его ID;timers = list_timers()
— получает обновлённый список таймеров после удаления;timers_screen = self.manager.get_screen('timers_screen')
— получает экран со списком таймеров;timers_screen.ids.timers_view.data = timers
— обновляет данныеRecycleView
новым списком таймеров;self.manager.current = 'timers_screen'
— возвращает пользователя на экран со списком таймеров.
Весь код timer_screen.py
from kivy.uix.screenmanager import Screen
from kivy.clock import Clock
from kivy.core.audio import SoundLoader
from kawai_focus.utils.utils import data_json, custom_timer, calculate_time
from kawai_focus.database.cruds import del_timer, list_timers
class TimerScreen(Screen):
"""Экран таймера"""
sound_timer_name = data_json.get_text('sound_timer')
path_file = f'sounds/{sound_timer_name}'
sound = SoundLoader.load(path_file)
def __init__(self, **kwargs):
super(TimerScreen, self).__init__(**kwargs)
# Переменные для управления таймером
self.zero_time = data_json.get_text('zero_time')
self.timer_generator = None
self.paused = False
self.remaining_time = None
self.sound_stop_event = None
self.timer = None
self.timer_start_time = None
self.source_timer_names = None
def choice_timer(self) -> None:
"""Метод для выбора таймера"""
current_timer_name = self.manager.state_machine.pop(0)
if current_timer_name 'pomodoro':
self.timer_generator = custom_timer(mm_user=self.timer.pomodoro_time)
self.ids.time_label.text = calculate_time(mm_user=self.timer.pomodoro_time)
self.ids.type_timer_label.text = 'Помидор'
elif current_timer_name 'break':
self.timer_generator = custom_timer(mm_user=self.timer.break_time)
self.ids.time_label.text = calculate_time(mm_user=self.timer.break_time)
self.ids.type_timer_label.text = 'Перерыв'
else:
self.timer_generator = custom_timer(mm_user=self.timer.break_long_time)
self.ids.time_label.text = calculate_time(mm_user=self.timer.break_long_time)
self.ids.type_timer_label.text = 'Перерывище'
self.ids.stop_button.opacity = 0
self.ids.stop_button.disabled = True
def start_timer(self, instance) -> None:
"""Метод для запуска таймера"""
if self.paused:
self.paused = False
else:
# Инициализация генератора таймера
if len(self.manager.state_machine) and self.timer_generator is None:
self.choice_timer()
self.ids.stop_button.opacity = 1
self.ids.stop_button.disabled = False
self.remaining_time = next(self.timer_generator, self.zero_time)
# Запуск обновления времени каждую секунду
Clock.schedule_interval(self.update_time, 1)
def pause_timer(self, instance) -> None:
"""Метод для паузы таймера"""
if not self.paused:
self.paused = True
# Остановка обновления времени
Clock.unschedule(self.update_time)
def stop_timer(self, instance) -> None:
"""Метод для остановки таймера"""
# Остановка обновления времени
Clock.unschedule(self.update_time)
self.paused = False
self.remaining_time = next(self.timer_generator, self.zero_time)
self.ids.time_label.text = self.timer_start_time
self.sound.stop()
if self.sound_stop_event:
Clock.unschedule(self.sound_stop_event)
self.sound_stop_event = None
if not len(self.manager.state_machine):
self.manager.state_machine = self.source_timer_names.copy()
self.choice_timer()
def back(self, instance) -> None:
"""Метод для кнопки назад - возврат в меню таймеров"""
if self.ids.title_warning.text:
self.ids.title_warning.text = ''
self.ids.del_timer.color = (1, 1, 1, 1)
self.manager.current = 'timers_screen'
def delete_timer(self, instance) -> None:
"""Метод для удаления таймера"""
if not self.ids.title_warning.text:
self.ids.title_warning.text = ('Вы действительно хотите удалить таймер?\n'
'Если да, то нажмите кнопку "удалить" ещё раз.')
self.ids.del_timer.color = (1, 0.3, 0.3, 1)
else:
if del_timer(timer_id=self.timer.id):
timers = list_timers()
timers_screen = self.manager.get_screen('timers_screen')
timers_screen.ids.timers_view.data = timers
self.manager.current = 'timers_screen'
def play_sound(self, dt) -> None:
"""Метод для воспроизведения звука"""
self.sound.play()
# Планируем остановку звука через 20 секунд
self.sound_stop_event = Clock.schedule_once(lambda dt: self.sound.stop(), 20)
def update_time(self, dt) -> None:
"""Метод для обновления времени на экране"""
if self.paused:
return
# Получение следующего значения времени из генератора
self.remaining_time = next(self.timer_generator, self.zero_time)
self.ids.time_label.text = self.remaining_time
if self.remaining_time self.zero_time:
# Остановка обновления времени
Clock.unschedule(self.update_time)
Clock.schedule_once(self.play_sound)
Проверка работы экрана «Таймеры»
Настало время посмотреть, как выглядит новый экран «Таймеры» и проверить корректность работы обновлённой логики приложения.
Запускаю приложение в terminal:
poetry run kawai-focus
Удаление таймера
После запуска приложения пользователь видит список таймеров, представленных в виде кнопок с их описанием. Этот список можно прокручивать колёсиком мыши или с помощью правой кнопки. Для трёх записей прокрутка не нужна, но при большом количестве таймеров эта возможность пригодится.
Выбираю «Таймер 2» и нажимаю на него:

Открылся «Таймер 2», как и ожидалось. Нажимаю кнопку «Удалить»:

При попытке удаления появляется предупреждение, а сама кнопка «Удалить» меняет цвет на красный. Это сделано для предотвращения случайного удаления.
Для подтверждения удаления таймера нажимаю «Удалить» ещё раз:

После подтверждения происходит возврат на экран «Таймеры». В списке видно, что осталось два таймера, и в базе данных также осталось две записи. Логика удаления отработала корректно.
Теперь нажимаю кнопку «Новый таймер»:

Проверка работы кнопки «Назад»
На экране «Конструктор таймеров» нажимаю кнопку «Назад» для проверки её работы:

Приложение возвращает меня на экран «Таймеры». Теперь хочу проверить, что будет, если нажать «Удалить», а затем передумать.
Выбираю «Таймер мини»:

Нажимаю на кнопку «Удалить»:

Так как я передумал удалить таймер, нажимаю кнопку «Назад»:

Вернувшись на экран «Таймеры», вижу, что «Таймер мини» остался на месте.
Снова открываю «Таймер мини», чтобы убедиться, что предупреждение об удалении и красный цвет кнопки исчезли:

Экран «Таймер мини» вернулся к исходному виду:

Экран «Таймеры» и функционал новых кнопок успешно протестированы вручную.
Анонс на следующие статьи
Я реализовал почти весь основной функционал прототипа MVP1. На текущий момент осталось создать два простых экрана — «Инструкция» и «О программе», которые будут нести информационную составляющую для моего приложения. Ещё не реализована кнопка «Редактировать».
Однако в следующей статье, которая выйдет 11.09.2025, я решил заняться куда более важным и интересным для любого приложения аспектом — дизайном и внешним видом.
На текущий момент внешний вид приложения подходит только для учебного прототипа, который едва ли мог бы конкурировать с современными красивыми десктоп-приложениями. Это относится как к дизайну, так и к расположению элементов на экране.
Что можно использовать, чтобы вывести внешний вид моего приложения на качественно новый уровень? Верстать самому, используя скудные возможности Kivy, не вариант. Существует библиотека KivyMD, которая предлагает набор виджетов, совместимых с Material Design, для использования с Kivy. Material Design (MD) — это концепция дизайна, разработанная Google, которая задаёт единый визуальный язык для приложений на разных платформах.
На первый взгляд KivyMD выглядит замечательно, но библиотека не обновлялась несколько лет. Использование устаревших библиотек чревато риском несовместимости в будущем. Однако я быстро нашёл решение: существует форк KivyMD 2.0.0, который активно поддерживается.
Красивый и гармоничный дизайн станет важным шагом в превращении приложения из прототипа в полноценный и удобный продукт для пользователя.
Если у вас есть мысли о том, как можно улучшить проект, пишите в комментариях — с удовольствием ознакомлюсь с вашими предложениями!
Читайте продолжение — не пропустите!
Заключение
- Реализован экран «Таймеры»;
- Созданы кнопки: «Назад», «Удалить», «Новый таймер» и подключенен функционал к ним.
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта Kawai.Focus.
Комментарии
Оставить комментарийВойдите, чтобы оставить комментарий.
Комментариев пока нет.