
Kawai.Focus - приложение для фокусировки внимания (часть 3)
Данная статья посвящена:
- Продолжению реализации минимального функционала таймера Pomodoro;
- Работе с фреймворком Kivy;
- Использованию
kv
файлов для работы с интерфейсом; - Добавлению звука для таймера;
- Улучшению кода.
Дополнительные материалы

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

Вступление
Всем доброго дня! В предыдущей статье Kawai.Focus - приложение для фокусировки внимания (часть 2) я начал работу над функционалом таймера Pomodoro. Был реализован функционал старта таймера, паузы и полной отмены таймера. Также была создана блок-схема пользовательского пути для приложения.
Сегодня я продолжу писать таймер Pomodoro, улучшу его код и перенесу работу с интерфейсом приложения в kv
файл. Также подключу звук срабатывания таймера (в настоящий момент в таймере нет звука - это нужно исправить). Заварите себе чай и приготовьте вкусняшки — время продолжить разбираться с "помидором"! =)
Код
json
Чтобы избежать дублирования строк в логах ошибок и таймера, я поместил эти данные в файлы json
. Для этого я создал два файла: errors.json
и timer.json
. В errors.json
хранятся сообщения об ошибках, а в timer.json
содержится всё, что касается таймера (например, название аудиофайла).
Содержимое errors.json
:
{
"no_time": "Не указано время таймера!",
"negative_time": "Время таймера не может быть отрицательным!",
"ss_mm_big": "Секунды и минуты не могут быть больше 59!",
"hh_big": "Часы не могут быть больше 23!"
}
Содержимое timer.json
:
{
"sound_timer": "alarm-beep.mp3",
"zero_time": "00:00:00",
"custom_time": "00:00:12"
}
kawai_focus/utils/data_json.py
Для чтения json
-файлов я написал класс в файле data_json.py
для работы с этим форматом:
import json
from kawai_focus.main import Logger
class ReadJson:
"""Класс для чтения json файла"""
def __init__(self, file_name: str):
self.file_name = file_name
self.json_data = self.read_json_as_dict()
def read_json_as_dict(self) -> dict:
"""Метод для чтения json файла как dict"""
try:
file_path = f'json/{self.file_name}'
with open(file=file_path, mode='r') as file:
return json.load(file)
except (FileNotFoundError, json.decoder.JSONDecodeError) as err:
Logger.error(f'Logger: {err.__class__.__name__}: {err}')
def get_text(self, name: str) -> str:
"""Метод для возврата текста из файла json по ключу name"""
text = self.json_data.get(name)
if text:
if isinstance(text, list):
text = ''.join(text)
return text
return text
read_json_err = ReadJson(file_name='errors.json')
read_json_timer = ReadJson(file_name='timer.json')
Основная информация о классе ReadJson
:
read_json_as_dict()
читает JSON-файл какdict
:file_path = f'json/{self.file_name}'
путь до файлаjson
;with open(file=file_path, mode='r') as file:
открывает файл в режиме чтения, а после прочтения закрывает его;return json.load(file)
читает файл какdict
и возвращает его;except (FileNotFoundError, json.decoder.JSONDecodeError) as err:
отлавливает два исключения:FileNotFoundError
— файл не найден;json.decoder.JSONDecodeError
— файл не является корректным JSON.
get_text()
возвращает данные по ключу:- Если значение — список, строки объединяются в одну при помощи
''.join(text)
; - Если это строка, возвращает её в исходном виде.
- Если значение — список, строки объединяются в одну при помощи
self.json_data = self.read_json_as_dict()
хранитdict
с данными после чтенияjson
, при инициализации класса;
Экземпляры класса ReadJson
:
read_json_err = ReadJson(file_name='errors.json')
— хранит данные об ошибках для импорта;read_json_timer = ReadJson(file_name='timer.json')
— хранит информацию о таймере для импорта.
kawai_focus/utils/utils.py
Иногда бывает так, что разработчик пишет код, который нормально работает, но его нужно улучшить или модернизировать. Это может происходить по разным причинам, но чаще всего из-за неверно выбранного способа реализации или необходимости оптимизировать ресурсы.
Старый код таймера
import re
from typing import Generator
import logging
from kivy.logger import Logger
Logger.setLevel(logging.DEBUG)
def custom_timer(timer_str: str) -> Generator[str, None, None]:
"""Функция отсчитывает время, установленное для таймера в формате 'XhYmZs'."""
try:
match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)
if not match:
raise ValueError('Неверный формат времени, используйте "XhYmZs"!')
hours, minutes, seconds = match.groups()
total_seconds = (int(hours or 0) * 3600) + (int(minutes or 0) * 60) + (int(seconds or 0))
if total_seconds 0:
raise ValueError('Не указано время таймера!')
for remaining in range(total_seconds, -1, -1):
yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"
except ValueError as err:
Logger.error(f'{err.__class__.__name__}: {err}')
Этот код рабочий: генераторная функция custom_timer()
возвращает генератор, который выдаёт текущее значение времени таймера. Более подробно о работе данного таймера можно узнать в моей предыдущей статье, в разделе kawai.focus/utils.py.
Однако в коде присутствуют лишние операции со строками:
match = re.match(r'(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?', timer_str)
— сопоставление регулярного выражения со строкой изtimer_str
;raise ValueError('Неверный формат времени, используйте "XhYmZs"!')
— выбрасывание исключения, если формат времени не соответствуетXhYmZs
;hours, minutes, seconds = match.groups()
— извлечение значений часов, минут и секунд.
Эти операции можно оптимизировать, если работать с числами на входе, минуя преобразования строк.
Обновлённый код таймера
from typing import Generator
from kawai_focus.main import Logger
from kawai_focus.utils.data_json import read_json_err
def custom_timer(hh: int=0, mm: int=0, ss: int=0) -> Generator[str, None, None]:
"""Функция отсчитывает время, установленное для таймера в формате
'hh:mm:ss'. Возвращает генератор, который возвращает текущее время
в формате 'hh:mm:ss'."""
try:
if ss == 0 and mm == 0 and hh == 0:
raise ValueError(read_json_err.get_text('no_time'))
if ss < 0 or mm < 0 or hh < 0:
raise ValueError(read_json_err.get_text('negative_time'))
if ss > 59 or mm > 59:
raise ValueError(read_json_err.get_text('ss_mm_big'))
if hh > 23:
raise ValueError(read_json_err.get_text('hh_big'))
total_seconds = (int(hh or 0) * 3600) + (int(mm or 0) * 60) + (int(ss or 0))
for remaining in range(total_seconds, -1, -1):
yield f"{remaining // 3600:02d}:{(remaining % 3600) // 60:02d}:{remaining % 60:02d}"
except (TypeError, ValueError) as err:
Logger.error(f'Logger: {err.__class__.__name__}: {err}')
Изменения в коде:
from kawai_focus.main import Logger
изменился импорт логгера теперь он находится вmain.py
;from kawai_focus.utils.data_json import read_json_err
импорт словаря с ошибками;- Обработка отсутствующего времени (
ss 0 и mm 0 и hh 0
): Если все три значения равны нулю, выбрасывается ошибкаValueError
со строкойНе указано время таймера!
, полученной черезread_json_err.get_text()
; - Проверка отрицательных значений (
ss < 0 или mm < 0 или hh < 0
): Если одно из значений отрицательное, выбрасывается ошибкаValueError
с текстомВремя таймера не может быть отрицательным!
; - Проверка недопустимых значений минут и секунд (
ss > 59 или mm > 59
): Если значение секунд или минут превышает 59, выбрасывается ошибка с текстомСекунды и минуты не могут быть больше 59!
; - Ограничение на часы (
hh > 23
): Если часы превышают 23, выбрасывается ошибкаValueError
со строкойЧасы не могут быть больше 23!
; - Математическое преобразование (
total_seconds = (int(hh or 0) * 3600) + (int(mm or 0) * 60) + (int(ss or 0))
): Вычисление общего количества секунд остаётся неизменным. Это решение оптимально в данном контексте, так как математика здесь проще и быстрее, чем использованиеtimedelta
или другой функции из Python. Это связано с тем, что математические операции выполняются на низком уровне и являются крайне оптимизированными, в то время какtimedelta
— это объект с методами и дополнительной логикой, что добавляет накладные расходы.
kawai_focus/screens/timer_screen.py
Перенос кода для интерфейса
"Портянка" из кода в классе TimerScreen
, которая отвечает за интерфейс экрана:
class TimerScreen(Screen):
"""Экран таймера"""
# Другой код
self.layout = FloatLayout()
# Добавляем кнопку "Старт"
self.start_button = Button(
text='Старт',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.3,
'center_y': 0.5
}
)
self.start_button.bind(on_release=self.start_timer)
self.layout.add_widget(self.start_button)
# Добавляем кнопку "Пауза"
self.pause_button = Button(
text='Пауза',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.5,
'center_y': 0.5
}
)
self.pause_button.bind(on_release=self.pause_timer)
self.layout.add_widget(self.pause_button)
# Добавляем кнопку "Стоп"
self.stop_button = Button(
text='Стоп',
size_hint=(None, None),
height=40,
width=100,
pos_hint={
'center_x': 0.7,
'center_y': 0.5
}
)
self.stop_button.bind(on_release=self.stop_timer)
self.layout.add_widget(self.stop_button)
# Добавляем Label для отображения времени
self.time_label = Label(
text='00:00:10',
size_hint=(None, None),
pos_hint={
'center_x': 0.5,
'center_y': 0.6
}
)
self.layout.add_widget(self.time_label)
self.add_widget(self.layout)
# Другой код
Этот код отвечает за создание кнопок, меток, добавление их на экран и указание их расположения.
Теперь весь код интерфейса был перенесён в отдельный файл timer_screen.kv
:
#:kivy 2.3.1
<TimerScreen>:
FloatLayout:
# Кнопка "Старт"
Button:
text: 'Старт'
size_hint: None, None
height: 40
width: 100
pos_hint: {'center_x': 0.3, 'center_y': 0.5}
on_release: root.start_timer(self)
# Кнопка "Пауза"
Button:
text: 'Пауза'
size_hint: None, None
height: 40
width: 100
pos_hint: {'center_x': 0.5, 'center_y': 0.5}
on_release: root.pause_timer(self)
# Кнопка "Стоп"
Button:
text: 'Стоп'
size_hint: None, None
height: 40
width: 100
pos_hint: {'center_x': 0.7, 'center_y': 0.5}
on_release: root.stop_timer(self)
# Label для отображения времени
Label:
id: time_label
text: '00:00:12'
size_hint: None, None
pos_hint: {'center_x': 0.5, 'center_y': 0.6}
Объяснение содержимого timer_screen.kv
:
#:kivy 2.3.1
— указывает версию Kivy;<TimerScreen>:
— определяет экран таймера;FloatLayout:
— задаёт вид компоновки (layout
);Button:
— описание кнопки:text: 'Старт'
— текст на кнопке;size_hint: None, None
— отключение автоматического масштабирования;height: 40
— высота кнопки в пикселях;width: 100
— ширина кнопки в пикселях;pos_hint: {'center_x': 0.3, 'center_y': 0.5}
— позиция кнопки в относительных единицах;on_release: root.start_timer(self)
— событие, вызываемое при нажатии на кнопку "Старт";
Label:
— описание метки:id: time_label
— идентификатор для взаимодействия с меткой из кода;- Остальные свойства аналогичны кнопкам.
Преимущества использования .kv
файлов:
- Уменьшение количества строк кода в Python;
- Более читаемая структура описания пользовательского интерфейса;
- Лёгкость редактирования и настройки элементов.
kawai_focus/main.py
Код загрузки .kv
файла:
# Другой код
from kivy.lang import Builder
from kivy.logger import Logger
import logging
Logger.setLevel(logging.DEBUG)
class KawaiFocusApp(App):
"""Класс для создания приложения"""
title = 'Kawai.Focus'
def build(self):
# Загрузка kv файла
Builder.load_file('kv/timer_screen.kv')
# Другой код
Пояснения к коду загрузки .kv
файла:
from kivy.lang import Builder
— импорт классаBuilder
, который загружает и интерпретирует.kv
-файлы;Builder.load_file('kv/timer_screen.kv')
— загружает.kv
-файл, парсит его содержимое и создаёт соответствующие виджеты.
Подключение звука
Таймер должен издавать звук после завершения отсчёта времени. Для этого я скачал файл alarm-beep.mp3
и поместил его в папку sounds
проекта. Затем был написан код, который отвечает за загрузку, воспроизведение и остановку звука таймера.
Код для работы со звуком в timer_screen.py
:
# Другой код
from kivy.core.audio import SoundLoader
class TimerScreen(Screen):
"""Экран таймера"""
sound_timer_name = read_json_timer.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 = read_json_timer.get_text('zero_time')
self.timer_generator = None
self.paused = False
self.remaining_time = None
self.sound_stop_event = None # событие для остановки звука
# Другой код
def stop_timer(self, instance) -> None:
"""Метод для остановки таймера"""
# Остановка обновления времени
Clock.unschedule(self.update_time)
self.paused = False
# TODO: временное решение, в время будет задаваться в интерфейсе
self.remaining_time = read_json_timer.get_text('custom_time')
self.ids.time_label.text = self.remaining_time
self.sound.stop()
if self.sound_stop_event:
Clock.unschedule(self.sound_stop_event)
self.sound_stop_event = None
def play_sound(self, dt) -> None:
"""Метод для воспроизведения звука"""
self.sound.play()
# Планируем остановку звука через 20 секунд
self.sound_stop_event = Clock.schedule_once(lambda dt: self.sound.stop(), 20)
# Другой код
Описание кода для звука:
from kivy.core.audio import SoundLoader
— импорт классаSoundLoader
, который отвечает за работу со звуком в Kivy;sound = SoundLoader.load(path_file)
— загружает звуковой файл и создаёт объект для управления им;self.sound_stop_event = None
— используется для хранения события, отвечающего за автоматическую остановку звука;stop_timer()
— метод для остановки таймера и звука:self.ids.time_label.text = self.remaining_time
— обращение черезid
к тексту меткиtime_label
(определённой в.kv
-файле);self.sound.stop()
— завершает воспроизведение звука;if self.sound_stop_event:
— если запланировано событие остановки, выполняется его отмена с помощьюClock.unschedule(self.sound_stop_event)
, после чегоself.sound_stop_event
сбрасывается вNone
;
play_sound()
— метод для воспроизведения звука:self.sound.play()
— начинает воспроизведение звука;self.sound_stop_event = Clock.schedule_once(lambda dt: self.sound.stop(), 20)
— планирует автоматическую остановку звука через 20 секунд.
Код 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 custom_timer
from kawai_focus.utils.data_json import read_json_timer
class TimerScreen(Screen):
"""Экран таймера"""
sound_timer_name = read_json_timer.get_text("sound_timer")
path_file = "sounds/{sound_timer_name}"
sound = SoundLoader.load(path_file)
def __init__(self, **kwargs):
super(TimerScreen, self).__init__(**kwargs)
# Переменные для управления таймером
self.zero_time = read_json_timer.get_text("zero_time")
self.timer_generator = None
self.paused = False
self.remaining_time = None
self.sound_stop_event = None
def start_timer(self, instance) -> None:
"""Метод для запуска таймера"""
if self.paused:
self.paused = False
else:
# Инициализация генератора таймера
self.timer_generator = custom_timer(ss=12) # Установите необходимое время
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
# TODO: временное решение, время будет задаваться в интерфейсе
self.remaining_time = read_json_timer.get_text("custom_time")
self.ids.time_label.text = self.remaining_time
self.sound.stop()
if self.sound_stop_event:
Clock.unschedule(self.sound_stop_event)
self.sound_stop_event = None
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)
Обновлённая структура проекта
├── kawai-focus
| ├── screens
| └── __init__.py
| └── timer_screen.kv
| ├── utils
| └── __init__.py
| └── data_json.py
| └── utils.py
│ └── __init__.py
│ └── main.py
├── json
| └── errors.json
| └── timer.json
├── sounds
| └── alarm-beep.mp3
├── kv
| └── timer_screen.kv
├── pyproject.toml
├── poetry.lock
├── README.md
├── LICENSE
├── .gitignore
Запуск приложения
Я запускаю своё приложение с помощью Poetry в терминале:
poetry run kawai-focus
Видео-демонстрация таймера со звуком на Rutube:
Видео-демонстрация таймера со звуком на YouTube:
Анонс на следующие статьи
Следующая моя статья выйдет 17.04.25. В ней я добавлю экран конструктора таймера, где будет реализован переход с одного экрана на другой. Также появится возможность задавать длительность и другие параметры таймера. Для хранения этих данных я подключу базу данных SQLite3 и начну сохранять настройки таймера.
У меня появилась подработка на написании IT-статей. Сама статья: DIY-проект: гусеничная платформа с ИК-управлением на Arduino. Первая публикация на Habr уже состоялась! 😊
Читайте продолжение — будет интересно!
Заключение
- Написан код для работы с JSON;
- Улучшен и оптимизирован код функции
custom_timer()
; - Перенесён код интерфейса UI из
timer_screen.py
в.kv
файл; - Подключен звук для таймера;
- Сообщения об ошибках и данные таймера вынесены в JSON;
- Логгер (
Logger
) вынесен в файлmain.py
.
Ссылки к статье
- Мои статьи Arduinum628 на Код на салфетке;
- Репозиторий проекта Kawai.Focus.
Все статьи