Вступление
Всем доброго дня! В предыдущей статье 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.
Комментарии
Оставить комментарийВойдите, чтобы оставить комментарий.
Комментариев пока нет.