Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Библиотека URWID: создаем интерфейсы текстового режима работы

Оригинал: URWID: create text mode interfaces
Автор: Ben Everard
Дата публикации: June 26, 2015
Перевод: Н.Ромоданов
Дата перевода:ноябрь 2015 г.

Пользовательские интерфейсы текстового режима еще не сданы в музеи — давайте разберемся, почему, и создадим такой интерфейс самостоятельно.

Сегодня вряд ли можно представить себе компьютер без графического рабочего стола. Даже на самых маленьких компьютерах, таких как Raspberry Pi, есть порт HDMI и мощный процессор, которого достаточно для использования графической среды. Может возникнуть чувство, что текстовые (или консольные) пользовательские интерфейсы (TUI) больше похожи на странные артефакты прошедших времен, которым самое место находиться в музеи, а не на вашем мониторе. Конечно, вы вряд ли будете пользоваться терминалом для общения в Facebook (хотя в терминале вы сможете, если захотите, бороздить веб с помощью браузера Links) или при написании отчета (хотя редактор Latex поможет вам справиться с документами, использующими самые современные форматы). Тем не менее, консольные программы пригодится в тех случаях, когда у вас нет настроенной графики (в инсталляторах или инструментальных средствах настройки) или когда вы пользуетесь медленным соединением (например, вы где-то в сельской местности, есть есть только сотовая сеть 2.75G, пытаетесь с помощью SSH получить доступ к вашему датчику, созданному на основе Raspberry Pi). Текстовые интерфейсы также часто предпочтительнее в специализированных приложениях, таких как торговые терминалы.

Есть эквиваленты, работающие в текстовом режиме, для многих графических программ, в том числе и для браузеров.

В этой статье будет рассказано о том, как делать консольные интерфейсы на языке Python с использованием библиотеки Urwid. Если вы когда-либо программировали с использованием Qt, GTK или любого другого инструментария, вы обнаружите много похожих, но не совпадающих концепций. Это, строго говоря, вызвано тем, что библиотека Urwid не является средством для работы с виджетами. Это инструментарий для создания виджетов и это тонкое различие иногда очень важно. В нем есть, как вы и ожидаете, некоторые элементы пользовательского интерфейса, такие как кнопки или поля ввода текста. Но многие более сложные виджеты, такие как диалоговые окна или выпадающие меню, отсутствуют (вы создадите их сами, и мы в течение минуты покажем вам, как это сделать). Также нет простого способа задать «порядок перехода при табуляции» (то есть, как должен перемещаться фокус при нажатии клавиши Tab). Это не означает, что библиотека Urwid ограниченная или примитивная - это полноценная библиотека с поддержкой мыши, с возможностью интеграции средств ввода вывода, предоставляемых сторонними разработчиками, а также интеграции с сервисами, что следовало бы ожидать от зрелого инструментария - но есть определенная особенность, которую необходимо иметь в виду при программировании с эти инструментарием.

Типы виджетов

Одна из задач, которую выполняет инструментарий, работающий с виджетами, это вычисление для виджетов позиции и занимаемого на экране места. Это не так просто, как это может показаться, и нет единого рецепта, подходящего для всех случаев. В некоторых старых библиотека, как правило, стараются в целом избегать этой работы, так что если некоторая метка слишком долго изображается на экране, то она просто «оторвалась» от интерфейса.

В подходе, предлагаемом в Urwid, используются три типа виджетов. Первый тип, "box" (прямоугольное пространство — прим.пер.), занимает столько места, сколько ему выделяет его контейнер; виджетом верхнего уровня в приложении Urwid всегда является один виджет box. В виджетах плавающего размера задается, сколько столбцов они должны занять, и на них возложена обязанность расчета количества строк на экране, которые они должны занимать (поскольку мы работаем в текстовом режиме, единицами измерения являются символы и размер виджета измеряется в количестве строк и столбцов, а не пикселей). В виджетах фиксированного размера все, наоборот, фиксировано; они независимо от того, сколько есть места, всегда занимают на экране одинаковое по размеру пространство, которое определяется их размером. Типичным примером виджета плавающего размера является виджет Text; обычно используемым виджетом типа box является виджет SolidFill, с помощью которого некоторое пространство заполняется заданным символом. Этот виджет полезен при создании фона. Виджеты фиксированного размера редки, и мы не будем здесь их обсуждать.

Есть также "decoration widgets" (виджеты - декораторы), которые представляют собой обертки вокруг других виджетов и изменяют их внешний вид или поведение. Таким образом, виджеты плавающего размера можно создавать с помощью виджетов box (например, с помощью виджета Filler, который изменяет размер виджета, убирая строки, которые не используются еего дочерним элементом) или виджеты box с помощью виджетов плавающего размера (смотрите виджет BoxAdapter). Внешний вид всех этих типов виджетов приводится в разделе «Included Widgets» в руководстве по библиотеке Urwid (http://urwid.org/manual).

Иногда вы можете выбрать неверный виджет и поместить виджет типа box туда, где ожидался виджет плавающего размера или что-то другое. В этом случае библиотека Urwid не очень дружелюбна, и все, что вы получаете, это следующее загадочное исключение ValueError:

... Здесь несколько других обращений к виджетам ...
File “/path/to/urwid/widget.py”, line 1004, in render
(maxcol,) = size
ValueError: too many values to unpack

Оно берет свое начало из механизмов рендеринга виджетов. Вам не нужно копаться в деталях этой обратной трассировки; просто запомните, что если вы видите это исключение, то вы, вероятно, пропустили виджет - декоратор.

Руководство по Urwid в том, что касается типов виджетов и много другого, всегда поддерживается в актуальном состоянии.

О таймере

Основной цикл интерфейса является не только диспетчером событий, но и таймером. Эти две роли могут показаться слишком разными, но они тесно связаны в том случае, когда вы спускаетесь до уровня системных вызовов.

Здесь мы не будем вдаваться в подробности, а вместо этого покажем, как в Urwid использовать таймеры. На самом деле, все очень просто, и API напоминает window.setTimeout() из JavaScript:

def callback(main_loop, user_data):
# Вызов будет через 10 секунд
handle = main_loop.set_alarm_in(10,
callback, user_data=[])

user_data используется для передачи произвольных значений в вашу функцию обратного вызова; если это вам не нужно, то просто опустите этот аргумент. Далее идет функция also set_alarm_at(), которая в заданный момент должна выдать сообщение. Кода сообщение станет ненужным, удалите с помощью следующей команды:

main_loop.remove_alarm(handle)

Выдача сообщений в Urwid не осуществляется периодически, так если сообщение уже было выдано, то нет никакой необходимости его удалять.

Пример Hello, Urwid world!

Пришло время писать код. Как и многие другие (если не все) фреймворки пользовательского интерфейса, Urwid построен вокруг главного цикла, представленного классом MainLoop. Этот цикл осуществляет диспетчеризацию событий, таких как нажатие клавиш или щелчков мыши, в иерархии виджетов, корнем которой является самый верхний виджет типа box, который передается в качестве первого аргумента в конструктор MainLoop (и доступен позже как атрибут ‘widget’ в объекте главного цикла). Таким образом, самая простая программа Urwid может выглядеть следующим образом:

from urwid import MainLoop, SolidFill
mainloop = MainLoop(SolidFill(‘#’))
mainloop.run()

Она заполняет экран символами «#». Метод run() запускает главный цикл. Для того, чтобы его завершить, нужно создать исключение ExitMainLoop:

def callback(key):
raise ExitMainLoop()
mainloop = MainLoop(SolidFill(‘#’),
unhandled_input=callback)

Функция обратного вызова unhandled_input предназначена для получение любого события, которое не обрабатывается в самом верхнем виджете (или в его потомках). Поскольку SolidFill () не реагирует на нажатия клавиш, можно будет нажатием любой клавиши остановить программу. Вы можете проверить это самостоятельно - просто с помощью вашего менеджера пакетов проверьте, что вы установили библиотеку Urwid (пакет называется python-urwid или каким-нибудь аналогичным образом).

Добавим немного цвета

Черно-белый текст выглядит скучно. В Urwid можно использовать различные цвета, но сначала нужна палитра:

single_color = [(‘basic’, ‘yellow’, ‘dark blue’)]
mainloop = MainLoop(AttrMap(SolidFill(‘#’),
‘basic’), palette=single_color)

Здесь в палитре содержится один цвет colour: желтый текст на синем фоне. (Цвет colour — это тройка, заключенная в круглые скобки - прим.пер.) Вы можете определить в палитре столько цветов, сколько пожелаете, но имейте в виду, что не все цвета (и атрибуты) поддерживаются во всех терминалах. Если вы не намерены использовать вполне конкретную среду, то лучше придерживаться «безопасных» цветов, которые указаны в разделе "Display Attributes" в руководстве Urwid.

Аргумент "palette = ключевое слово" устанавливает палитру для вашего приложения, а виджет-декоратор AttrMap — это то, к чему на амом деле применяется палитра. "basic" является идентификатором, он может быть любым, каким вы захотите.

Давайте открывать окна

Программы, как правило, взаимодействуют с пользователями через некоторые диалоговые окна. В текстовом режиме, они выглядят как прямоугольные области, ограниченные некоторым контуром; так давайте создадим одну из таких областей. Для того, чтобы это было более интересным, мы также будем использовать несколько основных виджетов. Синий фон можно создать обычным образом с помощью виджета SolidFill(‘ ‘) (давайте называть этот виджет креативно как фон "background"). Чтобы создать контур области, мы можем использовать виджет — декоратор LineBox() (как только виджеты появляются в текте, не забывайте их импортировать из пакета urwid):

window = LineBox(interior)

По умолчанию виджет LineBox рисует одну линию вокруг предоставляемого виджета; но вы можете настроить каждый элемент рамки при помощи символов псевдографики Unicode box drawing (http://unicode-table.com/en/#box-drawing). На данный момент забудьте о «внутренней» части виджета - мы вернемся к ней в ближайшее время. Но сейчас вопрос в том, как разместить диалоговое окно над фоном? Для этого в Urwid предлагается виджет Overlay():

topw = Overlay(window, background,
‘center’, 30, ‘middle’, 10)
main_loop = MainLoop(topw,
palette=some_palette)
main_loop.run()

Здесь окно размером 30×10 помещается в центре над фоном и запускается главный цикл. Обратите внимание, что мы использовали Overlay как самый верхний виджет. Если нам потребуется менять внешний вид окна то виджет main_loop.widget должен быть задан по -другому.

Теперь вернемся к «внутренней части» окна. Мы хотим, чтобы вертикально один элемент над другим были размещены несколько меток (Text), полей ввода текста (Edit) и кнопка (Button). В Urwid это можно сделать с помощью контейнера Pile:

caption = Text((‘caption’, ‘Enter some words:’),
align=’center’)
input = Edit(multiline=False)
# Will be set from the code
scratchpad = Text(‘’)
button = Button(‘Push me’)
button_wrap = Padding(AttrMap(button,
‘button.normal’, ‘button.focus’),
align=’center’, width=15)
interior = Filler(Pile()

Здесь мы видим два новых способа применения атрибутов (цвет — colours). В виджете Text можно в качестве параметра использовать разметку (кортеж или список кортежей), а в AttrMap можно указывать различные атрибуты для виждетов, на которые установлен фокус и на которые фокус не установлен. По мере того, как мы создаем виджеты, мы сохраняем их в переменных для дальнейшего использования.

Если вы попытаетесь запустить этот код сейчас, вы увидите, что завершится с сообщением ValueError, о котором мы уже рассказывали. Это связано с тем, что тип виджета Pile определяется его дочерними элементами детей, а элементы Text, Edit и Button являются виджетами с плавающими размерами. Виджет LineBox работает точно так же, поэтому финальное «окно» в нашей программе является виджетом плавающего размера. Однако то, как мы используем виджет Overlay, подразумевает, что верхний виджет является виджетом типа box (поскольку мы самостоятельно для него задаем ширину и высоту), и в этом проблема. Мы должны обернуть interior во что-то, чтобы сделать его виджетом типа box. Естественным выбираемым вариантом является виджет Filler: давайте мы позволим внутреннему виджету решить, сколько ему нужно строк, а все остальное сделает виджет Filler . По умолчанию, виджет Filler отцентрирует окно по вертикали, и это также то, что нам нужно:

interior = Filler(Pile([...]))

Теперь программа работает; Однако, кнопка шире, чем это необходимо. Это потому, что виджет Pile делает все дочерние элементы равными по ширине, поэтому для кнопки надо задать отступы:

button_wrap = Padding(AttrMap(...),
align=’center’, width=15)

По умолчанию, виджет Padding выравнивает все содержимое пот левому краю, поэтому нам необходимо явно указать, что мы хотим элемент разместить по центру. Ширина может быть задана любым целым числом (точное количество столбцов), значением "pack" (попытаться найти оптимальную ширину, что может не работать) или, если вы хотите, чтобы содержимое масштабировалось одновременно с с масштабом контейнера, то относительное значение, указываемое в процентах.

Теперь, интерфейс выглядит так, как необходимо, однако, до сих пор он ничего не делает. Давайте будем менять содержимое scratchpad, когда будет нажиматься кнопка (либо с помощью клавиши Enter, либо с помощью мыши):

from urwid import connect_signal
def button_clicked(button, user_data):
input, scratchpad = user_data
scratchpad.set_text(‘You entered: %s’ %
input.edit_text)
connect_signal(button, ‘click’, button_clicked,
[input, scratchpad])

Мы передавать ссылки на input и scratchpad, которые находятся в user_data; в реальном коде они, вероятно, будут некоторыми атрибутами объекта. Если вы не хотите, чтобы кнопка продолжала работать, вы можете отключить работу с сигналом с помощью функции disconnect_signal(). Для виджета Button вы можете добиться таких же результатов с помощью аргументов конструктора on_press= и user_data=, однако подход, о котором мы только что рассказали, работает для любого события и виджета (например, когда текст будет изменен, виджет Edit выдаст сигнал "change").

Наша простая программа теперь полностью функциональна, за исключением лишь того, что нет никакого способа выйти из нее. Мы можем повторно использовать трюк unhandled_input, но на этот раз, давайте выйдем только если пользователь нажимает клавишу F10:

def unhandled_input(key):
if key == ‘f10’:
raise ExitMainLoop()

Если вы захотите, вы также можете добавить еще одну кнопку для закрытия приложения.

Наша первая программа Urwid: базовая, но полностью функциональная.

Секретное оружие

Как мы уже узнали, в Urwid отсутствует множество виджетов, имеющие расширенные возможности. Тем не менее, в ней есть один очень мощный виджет: ListBox. Вы можете предположить, что это окно с несколькими строками текста, одна из которых с подсветкой, но ListBox из Urwid отличается (хотя он может выглядеть точно также и может вести себя точно таким же образом). Это список (или даже дерево) с возможностью прокрутки, состоящий из произвольных виджетов, которые генерируются динамически, и это можно использовать для различных целей, в том числе для создания меню, последовательно работающих редакторов и почти для всего остального (кроме кофемашины, как вы знаете).

ListBox немного похож на Pile тем, что он берет список виджетов и размещает их вертикально. Тем не менее, есть много отличий и они очень важны. Во-первых, передача в ListBox списка виджетов более простая, ограниченная и несколько обескураживающая тем, как добавляется его содержимое. Во-вторых, ListBox всегда является виджетом типа box, в котором находятся виджеты плавающего размера; другими словами, в нем решается, какая часть содержимого будет показан в данный момент времени. Чтобы принять такое решение, в ListBox происходит управление фокусом: если, например, вы нажимаете клавишу Вниз, то фокус будет смещен к дочернему элементу, и соответствуюжщим образом произойдет скроллинг его содержимого.

Хотя виджет ListBox является настоящим "швейцарским армейским ножиком", мы будем использовать его для создания простого меню. Давайте начнем с класса MenuItem. Пункт простого меню является всего лишь текстовой меткой, которая подсвечивается когда на нее попадает фокус, и реагирует каким-то образом на активацию (например, на нажатие клавиши Enter). Это означает, что идеальным для такого пункта является виджет Text. Мы должны зарегистрировать сигнал (давайте называть его "activate"), перехватить нажатие клавиши Enter и сделать так, чтобы виджет мог переключаться (это основное свойство всех виджетов в Urwid; фокус в контейнере ListBox получают только переключаемые виджеты).

from urwid import register_signal, emit_signal
class MenuItem(Text):
def __init__(self, caption):
Text.__init__(self, caption)
register_signal(self.__class__, [‘activate’])
def keypress(self, size, key):
if key == ‘enter’:
emit_signal(self, ‘activate’)
else:
return key
def selectable(self):
return True

Сигналы регистрируются в каждом классе с помощью egister_signal() и затем выдаются с помощью emit_signal(). Метод keypress() определен в базовом классе Widget и переопределяется любым виджетом, в которых нужно реагировать на клавиатуру (его размер равен размеру текущего виджета). Если виджет успешно обрабатывает нажатие клавиши, то он ничего не возвращает; в противном случае он возвращает сообщение о том, что была нажата клавиша. Есть аналогичный метод mouse_event(), но мы не будем здесь его обсуждать.

Далее, нам нужно упаковать объекты MenuItem в ListBox. Чтобы сделать текущий фокус видимым, мы будем использовать виджет AttrMap точно также, как это мы сделали ранее для кнопки:

def exit_app():
raise ExitMainLoop()
contents = []
for caption in [‘Item 1’, ‘Item 2’, ‘Item 3’]:
item = MenuItem(caption)
connect_signal(item, ‘activate’, exit_app)
contents.append(AttrMap(item,
‘item.normal’, ‘item.focus’))
interior = ListBox(SimpleFocusListWalker(contents))

Здесь предполагается, что общая структура программы точно такая же, как и в предыдущем примере; но поскольку ListBox является виджетом типа box, нет необходимости обертывать ''внутреннее содержимое'' при помощи виджета Filler. Мы подключили сигнал "activate" к функции exit_app(), которая просто завершает программу.

Класс SimpleFocusListWorker является базовым адаптером, который выполняет работу виджетаListBox над всем списком статических списка виджетов. Он осуществляется из класса ListWalker и здесь вы можете пользоваться другими своими подклассами, в том числе также теми, которые вы создаете сами. Основная причина этого в том, чтобы сделать содержимое ListBox динамическим и, например, читать строки из файла, только когда пользователь осуществляет прокрутку вниз к этим строка. Здесь ListBox используется в полую меру своих возможностей.

Виджет ListBox является естественным выбором, например, для виджета типа box в списке.


Если вам потребуется помощь программиста, вы всегда можете найти квалифицированного разработчика на сайте Toptal. Здесь вы найдете нужного вам специалиста по любым языкам программирования и другим инструментам разработчика.