Эти забавные зверушки
Не так давно передо мной встала задача в сжатые
Ну… почти весь. Под катом расскажу что не так и как это побороть.
Ёж Киви — птица гордая
Фреймворк имеет полезный класс kivy.uix.popup.Popup для реализации всплывающих окон. Не буду вдаваться в подробности того, что он умеет — статья не об этом. Кому любопытно — по ссылке можно почитать документацию этого класса и его предка — kivy.uix.modalview.ModalView.
Но есть нюансы. Допустим, перед вами стоит банальная задача — вывести текстовое сообщение во всплывающем окне. Popup позволяет это сделать довольно просто, в одну строку:
Popup(title='Уведомление', content=Label(text='Получено новое сообщение')).open()
Немного усложним задачу — добавим кнопку, по нажатию на которую всплывающее окно должно закрыться:
layout = BoxLayout(orientation="vertical")
layout.add_widget(Label(text='Получено новое сообщение'))
button = Button(text='Закрыть')
layout.add_widget(button)
popup = Popup(title='Уведомление', content=layout)
button.bind(on_press=popup.dismiss)
popup.open()
Тоже ничего военного, но не совсем удобно для обычного MessageBox'а, правда?
Как бывший Delphiнист, имею дурную привычку вызывать MessageBox одной строкой. И в коем-то веке дурная привычка принесла пользу — спустя несколько
Главное — вовремя остановиться
Безобидная идея упростить себе жизнь со всплывающими сообщениями в итоге выросла в целый пакет, который с удовольствием был
Для сравнения — вышеописанная задача с кнопкой реализуется следующим образом:
XNotifyBase(title='Получено новое сообщение!', text='Что будем делать?',
buttons=['Открыть', 'Пометить прочитанным', 'Напомнить позже'])
Но пойдем по порядку. Иерархия классов пакета выглядит так:
Popup
Создаем удобства
Примечание. В этой статье будут рассмотрены классы уведомлений (XNotifyBase и его потомки). Подробнее об остальных классах — в следующей статье.
Класс XNotification
Всплывающее окно с заголовком и текстом, без кнопок. Его особенностью является способность автоматически закрываться по истечении указанного количества секунд:
XNotification(text='Это окно закроется через 3 секунды', show_time=3)
Если show_time не указывать — окно будет закрыто только при вызове метода .dismiss().
Класс XMessage
Аналог привычного MessageBox, т.е. окно, имеющее заголовок (title), некое сообщение (text) и кнопку «Ok». Пример:
XMessage(text='Некое сообщение', title='Заголовок')
Стандартная подпись кнопки легко меняется на любую другую:
XMessage(text='Некое сообщение', title='Заголовок', buttons=['Закрыть'])
Также просто заменяется набор отображаемых кнопок:
XMessage(text='Некое сообщение', title='Заголовок', buttons=['Закрыть', 'Напомнить позже'])
О том, как обработать нажатие кнопки — будет рассказано ниже.
Класс XError
По сути — тот же XMessage. Разница в том, что данный класс задает заголовок по умолчанию (т.е. title можно не указывать):
XError(text='Произошла сферическая ошибка в вакууме')
Не нравится стандартный заголовок — задаем свой:
XError(text='Произошла сферическая ошибка в вакууме', title='Что-то пошло не так...')
Чтобы постоянно не указывать title, делаем следующее:
class MyError(XError):
buttons = ListProperty(['Закрыть'])
title = StringProperty('Что-то пошло не так...')
MyError(text='Произошла сферическая ошибка в вакууме')
Класс XConfirmation
Всплывающее окно с заголовком «Confirmation» и кнопками «Yes» и «No». Удобно использовать в случаях, когда от пользователя необходимо получить подтверждение («Yes») какого-либо действия или запрет («No») на выполнение данного действия.
Для начала нам потребуется обработчик события закрытия диалога:
def my_callback(instance):
# Метод класса XConfirmation, возвращает True, если была нажата кнопка "Yes"
if instance.is_confirmed():
print('Вы согласились')
else:
print('Вы отказались')
После того, как обработчик написан, можно создавать всплывающее окно:
XConfirmation(text='Необходимо подтверждение действия. Вы согласны?', on_dismiss=my_callback)
Опять же, стандартные кнопки можно заменить своими. Но в таком случае перестанет работать метод .is_confirmed(), так как он ориентируется на нажатие кнопки «Yes». Это легко решается путем использования свойства .button_pressed, хранящим название нажатой кнопки. Внесем изменения в наш обработчик:
def my_callback(instance):
# Свойство доступно всем потомкам XBase
if instance.button_pressed == 'Подтвердить':
print('Вы согласились')
else:
print('Вы отказались')
Теперь смело можно создавать окно со своим набором кнопок:
XConfirmation(
text='Необходимо подтверждение действия. Вы согласны?',
on_dismiss=my_callback, buttons=['Подтвердить', 'Отказать'])
Класс XProgress
Всплывающее окно с индикатором прогресса и кнопкой «Cancel» (заголовок и сообщение — в комплекте). Для управления индикатором будем использовать следующие свойства:
- value — текущее состояние прогресса
- max — максимальное значение прогресса (по умолчанию, max=100)
Пример:
popup = XProgress(value=100, max=1000, text='Идет обработка запроса...', title='Ожидайте', buttons=[])
Данный код отобразит всплывающее окно без кнопок, с 10% выполненного прогресса. Дальнейшее изменение прогресса возможно двумя способами.
Способ 1-й — посредством свойства .value:
# просто указываем новое значение прогресса
popup.value = 20
Способ 2-й — используем метод .inc():
# увеличиваем текущий прогресс на 1 единицу
popup.inc()
# увеличиваем текущий прогресс на 10 единиц
popup.inc(10)
Особенность использования метода .inc() заключается в том, что при достижении максимального значения прогресса индикатор не останавливается на максимуме, а происходит «зацикливание», т.е. прогресс сбрасывается и отсчет идет с нуля.
Пример:
# Создаем окно и устанавливаем 90% прогресса
popup = XProgress(value=90, text='Идет обработка запроса...', title='Ожидайте')
# Добавляем 15 - теперь индикатор отображает 5%
popup.inc(15)
Этот метод очень удобно использовать в случаях, когда заранее неизвестен максимум или количество выполняемых итераций.
Наряду с методом .inc() будет полезен метод .complete(). Данный метод выполняет следующее:
- устанавливает прогресс в максимальное значение
- заменяет имеющееся сообщение на «Complete»
- прячет кнопки (если они есть)
- автоматически закрывает окно через 2 секунды
Класс XNotifyBase
Вышеописанных классов может не хватить на все случаи жизни. Не беда — берем за основу XNotifyBase и рисуем все, что душе угодно. Данный класс задает объекту следующее поведение:
- добавляет во всплывающее окно метку (kivy.uix.label.Label) для отображения сообщения
- содержит свойство для управления данной меткой (.text)
Свойство .buttons наследуется от предка XBase, но об этом позже.
Оперируя имеющимися свойствами, можно создать свое собственное уведомление для одноразового использования:
XNotifyBase(title='Получено новое сообщение!', text='Что будем делать?',
buttons=['Открыть', 'Пометить прочитанным', 'Напомнить позже'])
или для многократного:
class NotifyNewMail(XNotifyBase):
buttons = ListProperty(['Открыть', 'Пометить прочитанным', 'Напомнить позже'])
title = StringProperty('Получено новое сообщение!')
text = StringProperty('Что будем делать?')
popup = NotifyNewMail()
Осталось описать свой обработчик события закрытия диалога — и можно наслаждаться полученным результатом.
Послесловие
Наглядное пособие (видео демонстрацию) можно посмотреть здесь.
Скачать пакет XPopup — здесь.
Приятного всем кодинга.
Комментарии (10)
HeaTTheatR
25.05.2016 19:09Спасибо за материал. Писал что-то похожее для своих проектов. Модуль именован KDialog. Кстати, как насчет мультипоточности в Kivy — твой пакет прерывает выполнение программы при появлении окна выбора, скажем, "Да/Нет"?
ophermit
25.05.2016 19:23Не прерывает. Отображение всплывающего окна — это всего лишь создание нового виджета. Как только виджет прорисован — интерпретатор выполняет следующую строку кода.
ophermit
25.05.2016 19:23Кстати, интересно было бы посмотреть на KDialog. Он где-нибудь опубликован?
HeaTTheatR
25.05.2016 19:33Да. На github. kdialog.py
ophermit
25.05.2016 23:08Интересное решение, особенно с rst хорошо придумал. А реализацию param=«loaddialog» я бы реализовал по-другому.
Почитай про объекты kivy.clock.Clock и kivy.network.urlrequest.UrlrequestHeaTTheatR
25.05.2016 23:22Про эти объекты мне известно. Дело в том, что невозможно (по крайней мере, у меня не получилось) создать диалоговое окно загрузки чего-либо, закрыть его и вывести следующий экран. Поэтому в модуле реализованы такие костыли. Если тебе удалось решить это, с удовольствием посмутрю на твое решение.
ophermit
26.05.2016 00:55Если я правильно понял проблему, то вот решение (на примере твоего класса):
import kivy kivy.require("1.9.1") from kivy.app import App class Demo(App): def build(self): return Button(text='Press me!', on_release=self.show_dialog_async) @staticmethod def show_dialog_async(instance): progress = KDialog() def on_success(request, response): print('Success!') progress.body.dismiss() window = KDialog( background_color=[0.4823529411764706, 0.49411764705882355, 0.5215686274509804, 1.0]) window.show(text=response, rst=True, size_rst=(.9, .9), text_button_ok="OK", param="query") def on_redirect(request, result): new_url = request.resp_headers['location'] print('Redirected to %s' % new_url) print(result) send_req(new_url) def send_req(url): UrlRequest(url=url, debug=True, on_success=on_success, on_redirect=on_redirect) progress.show(image="timerloading.gif", param="loaddialog", text='Loading...') send_req('http://python.org') Demo().run()
Urlrequest — потомок класса Thread. В принципе здесь делаем то же самое, что и твой костыль, только еще и тянем данные по указанному url. А kivy main thread в это время спокойно может прорисовывать интерфейс.
Обрати внимание на то, как перед отображением 2-го окна gif замирает — в этот момент основной поток приложения занят обработкой этой строки:
window.show(text=response, rst=True, size_rst=(.9, .9), text_button_ok="OK", param="query")
Если хочешь избавиться и от этого — создавай RstDocument в отдельном потоке и callback'ом отображай 2-е окно. Имхо — это уже лишнее.HeaTTheatR
26.05.2016 01:04Нет, ты не понял. Точнее я не так обрисовал ситуацию. Ломается экран, когда пытаешься в процессе загрузки, например, менять лейбл окна, после чего собираешься вывести новый экран в ScreenManager. Утром в личку пришлю пример.
nikolay_karelin
Спасибо за материал.
Было бы здорово также почитать про написание расширений для Kivy — насколько это сложно, что нужно сделать или переопределить и пр.
ophermit
Спасибо за идею — возьму на заметку. Там тоже есть нюансы, о которых стоит рассказать. Если меня никто не опередит — будет статья и об этом.