Программирование Curses с помощью Python

Автор

А.М. Кучлинг, Эрик С. Реймонд

Релиз

2.04

Аннотация

В этом документе описывается, как использовать модуль расширения curses для управления дисплеями в текстовом режиме.

Что такое проклятия?

Библиотека curses предоставляет независимые от терминала средства рисования экрана и работы с клавиатурой для текстовых терминалов; к таким терминалам относятся VT100, консоль Linux и симулированный терминал, предоставляемый различными программами. Дисплейные терминалы поддерживают различные управляющие коды для выполнения общих операций, таких как перемещение курсора, прокрутка экрана и стирание областей. Различные терминалы используют совершенно разные коды и часто имеют свои собственные незначительные причуды.

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

Библиотека curses обеспечивает достаточно базовую функциональность, предоставляя программисту абстракцию дисплея, содержащего несколько не перекрывающихся окон с текстом. Содержимое окна можно изменить различными способами - добавить текст, стереть его, изменить внешний вид - и библиотека curses выяснит, какие управляющие коды нужно послать на терминал, чтобы получить нужный вывод. curses не предоставляет многих концепций пользовательского интерфейса, таких как кнопки, флажки или диалоги; если вам нужны такие возможности, рассмотрите библиотеку пользовательского интерфейса, такую как Urwid.

Библиотека curses изначально была написана для BSD Unix; в более поздних версиях System V Unix от AT&T было добавлено множество усовершенствований и новых функций. BSD curses больше не поддерживается, ее заменил ncurses, который является реализацией интерфейса AT&T с открытым исходным кодом. Если вы используете Unix с открытым исходным кодом, например, Linux или FreeBSD, ваша система почти наверняка использует ncurses. Поскольку большинство современных коммерческих версий Unix основаны на коде System V, все описанные здесь функции, вероятно, будут доступны. Однако старые версии curses, используемые в некоторых проприетарных Unix, могут поддерживать не все.

Версия Python для Windows не включает модуль curses. Доступна портированная версия под названием UniCurses.

Модуль Python curses

Модуль Python - это довольно простая обертка над функциями C, предоставляемыми curses; если вы уже знакомы с программированием curses на C, то перенести эти знания в Python будет очень просто. Самое большое отличие заключается в том, что интерфейс Python упрощает работу, объединяя различные функции C, такие как addstr(), mvaddstr() и mvwaddstr() в один метод addstr(). Более подробно об этом будет рассказано позже.

Это HOWTO является введением в написание программ в текстовом режиме с помощью curses и Python. Он не пытается быть полным руководством по API curses; для этого смотрите раздел ncurses в руководстве по библиотеке Python и страницы руководства по C для ncurses. Тем не менее, это даст вам основные идеи.

Запуск и завершение работы приложения curses

Прежде чем что-либо делать, curses должен быть инициализирован. Это делается вызовом функции initscr(), которая определяет тип терминала, посылает на него все необходимые коды настройки и создает различные внутренние структуры данных. В случае успеха initscr() возвращает объект окна, представляющий весь экран; обычно он вызывается stdscr после имени соответствующей переменной C.

import curses
stdscr = curses.initscr()

Обычно приложения curses отключают автоматическое эхо клавиш на экран, чтобы иметь возможность читать клавиши и отображать их только при определенных обстоятельствах. Для этого необходимо вызвать функцию noecho().

curses.noecho()

Приложениям также часто требуется реагировать на нажатие клавиш мгновенно, не требуя нажатия клавиши Enter; это называется режимом cbreak, в отличие от обычного режима буферизованного ввода.

curses.cbreak()

Терминалы обычно возвращают специальные клавиши, такие как клавиши курсора или навигационные клавиши, например, Page Up и Home, в виде многобайтовой управляющей последовательности. Хотя вы можете написать свое приложение, чтобы оно ожидало такие последовательности и обрабатывало их соответствующим образом, curses может сделать это за вас, возвращая специальное значение, такое как curses.KEY_LEFT. Чтобы заставить curses выполнять эту работу, вам придется включить режим клавиатуры.

stdscr.keypad(True)

Завершить работу приложения curses гораздо проще, чем запустить его. Вам нужно вызвать:

curses.nocbreak()
stdscr.keypad(False)
curses.echo()

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

curses.endwin()

Частой проблемой при отладке приложения curses является повреждение терминала при завершении работы приложения без восстановления предыдущего состояния терминала. В Python это обычно происходит, когда ваш код содержит ошибки и вызывает не пойманное исключение. Например, клавиши больше не отражаются на экране, когда вы их набираете, что затрудняет использование оболочки.

В Python вы можете избежать этих сложностей и значительно упростить отладку, импортировав функцию curses.wrapper() и используя ее следующим образом:

from curses import wrapper

def main(stdscr):
    # Clear screen
    stdscr.clear()

    # This raises ZeroDivisionError when i == 10.
    for i in range(0, 11):
        v = i-10
        stdscr.addstr(i, 0, '10 divided by {} is {}'.format(v, 10/v))

    stdscr.refresh()
    stdscr.getkey()

wrapper(main)

Функция wrapper() принимает объект callable и выполняет инициализацию, описанную выше, также инициализируя цвета, если присутствует поддержка цветов. Затем wrapper() запускает предоставленный вами вызываемый объект. После возврата вызываемого объекта функция wrapper() восстановит исходное состояние терминала. Вызываемая переменная вызывается внутри tryexcept, который перехватывает исключения, восстанавливает состояние терминала, а затем снова вызывает исключение. Таким образом, ваш терминал не будет оставлен в смешном состоянии при исключении, и вы сможете прочитать сообщение об исключении и трассировку.

Окна и колодки

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

Объект stdscr, возвращаемый функцией initscr(), представляет собой объект окна, занимающий весь экран. Многим программам может понадобиться только это единственное окно, но вы можете захотеть разделить экран на более мелкие окна, чтобы перерисовывать или очищать их по отдельности. Функция newwin() создает новое окно заданного размера, возвращая объект нового окна.

begin_x = 20; begin_y = 7
height = 5; width = 40
win = curses.newwin(height, width, begin_y, begin_x)

Обратите внимание, что система координат, используемая в curses, необычна. Координаты всегда передаются в порядке y,x, а левый верхний угол окна имеет координаты (0,0). Это нарушает обычное правило работы с координатами, согласно которому на первом месте стоит координата x. Это досадное отличие от большинства других компьютерных приложений, но оно было частью curses с самого начала его написания, и сейчас уже слишком поздно что-то менять.

Ваше приложение может определить размер экрана, используя переменные curses.LINES и curses.COLS для получения размеров y и x. Тогда законные координаты будут простираться от (0,0) до (curses.LINES - 1, curses.COLS - 1).

Когда вы вызываете метод для отображения или стирания текста, эффект не сразу отображается на экране. Вместо этого вы должны вызвать метод refresh() для оконных объектов, чтобы обновить экран.

Это связано с тем, что curses изначально был написан с учетом медленных терминальных соединений 300 бод; на таких терминалах минимизация времени, необходимого для перерисовки экрана, была очень важна. Вместо этого curses накапливает изменения на экране и отображает их наиболее эффективным образом, когда вы вызываете refresh(). Например, если ваша программа отображает некоторый текст в окне, а затем очищает окно, нет необходимости передавать исходный текст, потому что он никогда не будет виден.

На практике, явное указание curses перерисовать окно не сильно усложняет программирование с помощью curses. Большинство программ начинают бурную деятельность, а затем приостанавливаются в ожидании нажатия клавиши или какого-либо другого действия со стороны пользователя. Все, что вам нужно сделать, это убедиться, что экран был перерисован перед паузой в ожидании пользовательского ввода, предварительно вызвав stdscr.refresh() или метод refresh() какого-нибудь другого соответствующего окна.

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

pad = curses.newpad(100, 100)
# These loops fill the pad with letters; addch() is
# explained in the next section
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# Displays a section of the pad in the middle of the screen.
# (0,0) : coordinate of upper-left corner of pad area to display.
# (5,5) : coordinate of upper-left corner of window area to be filled
#         with pad content.
# (20, 75) : coordinate of lower-right corner of window area to be
#          : filled with pad content.
pad.refresh( 0,0, 5,5, 20,75)

Вызов refresh() отображает участок панели в прямоугольнике, простирающемся от координаты (5,5) до координаты (20,75) на экране; левый верхний угол отображаемого участка - это координата (0,0) на панели. За исключением этого различия, пэды в точности похожи на обычные окна и поддерживают те же методы.

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

  1. Вызывает метод noutrefresh() каждого окна для обновления базовой структуры данных, представляющей желаемое состояние экрана.

  2. Вызывает функцию doupdate() для изменения физического экрана в соответствии с желаемым состоянием, записанным в структуре данных.

Вместо этого вы можете вызвать noutrefresh() на нескольких окнах для обновления структуры данных, а затем вызвать doupdate() для обновления экрана.

Отображение текста

С точки зрения программиста на языке Си, curses иногда может выглядеть как запутанный лабиринт функций, все из которых незначительно отличаются друг от друга. Например, addstr() выводит строку в текущем положении курсора в окне stdscr, а mvaddstr() сначала перемещается к заданной координате y,x, а затем выводит строку. waddstr() подобна addstr(), но позволяет указать окно, а не использовать stdscr по умолчанию. mvwaddstr() позволяет указать и окно, и координату.

К счастью, интерфейс Python скрывает все эти детали. stdscr - это объект окна, как и любой другой, и такие методы, как addstr(), принимают несколько форм аргумента. Обычно существует четыре различных формы.

Форма

Описание

str или ch

Вывести строку str или символ ch в текущей позиции

str или ch, attr

Вывести строку str или символ ch, используя атрибут attr в текущей позиции

y, x, str или ch

Перемещение в позицию y,x в окне и отображение str или ch.

y, x, str или ch, attr

Перемещение в позицию y,x в окне и отображение str или ch, используя атрибут attr.

Атрибуты позволяют отображать текст в выделенных формах, таких как полужирный шрифт, подчеркивание, обратный код или цвет. Более подробно они будут описаны в следующем подразделе.

Метод addstr() принимает строку Python или байтстринг в качестве отображаемого значения. Содержимое байтовых строк передается на терминал как есть. Строки кодируются в байты с помощью значения атрибута окна encoding; по умолчанию используется системная кодировка по умолчанию, возвращаемая методом locale.getpreferredencoding().

Методы addch() принимают символ, который может быть либо строкой длины 1, либо байтовой строкой длины 1, либо целым числом.

Для символов расширения предусмотрены константы; эти константы представляют собой целые числа больше 255. Например, ACS_PLMINUS - это символ +/-, а ACS_ULCORNER - левый верхний угол рамки (удобно для рисования границ). Вы также можете использовать соответствующий символ Unicode.

Windows запоминает, где был оставлен курсор после последней операции, поэтому если вы оставите координаты y,x, строка или символ будет отображаться там, где была оставлена последняя операция. Вы также можете перемещать курсор с помощью метода move(y,x). Поскольку некоторые терминалы всегда отображают мигающий курсор, вы можете убедиться, что курсор расположен в таком месте, где он не будет отвлекать внимание; мигающий курсор в каком-то случайном месте может сбить с толку.

Если вашему приложению вообще не нужен мигающий курсор, вы можете вызвать функцию curs_set(False), чтобы сделать его невидимым. Для совместимости со старыми версиями curses существует функция leaveok(bool), которая является синонимом curs_set(). Когда bool равно true, библиотека curses будет пытаться подавить мигающий курсор, и вам не нужно будет беспокоиться о том, чтобы оставлять его в странных местах.

Атрибуты и цвет

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

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

Атрибут

Описание

A_BLINK

Мигающий текст

A_BOLD

Очень яркий или жирный текст

A_DIM

Полуяркий текст

A_REVERSE

Текст обратного видео

A_STANDOUT

Лучший режим выделения из всех существующих

A_UNDERLINE

Подчеркнутый текст

Так, для отображения строки состояния обратного видео в верхней строке экрана, вы можете записать код:

stdscr.addstr(0, 0, "Current mode: Typing mode",
              curses.A_REVERSE)
stdscr.refresh()

Библиотека curses также поддерживает цвет на тех терминалах, которые его предоставляют. Самым распространенным таким терминалом, вероятно, является консоль Linux, за ней следуют цветные xterms.

Чтобы использовать цвет, вы должны вызвать функцию start_color() вскоре после вызова initscr(), чтобы инициализировать набор цветов по умолчанию (функция curses.wrapper() делает это автоматически). После этого функция has_colors() возвращает TRUE, если используемый терминал действительно может отображать цвет. (Примечание: curses использует американское написание «color», а не канадское/британское написание «color». Если вы привыкли к британскому написанию, вам придется смириться с неправильным написанием ради этих функций).

Библиотека curses поддерживает конечное число цветовых пар, содержащих цвет переднего плана (или текста) и цвет фона. Вы можете получить значение атрибута, соответствующего цветовой паре, с помощью функции color_pair(); это значение может быть побитово-иронизировано с другими атрибутами, такими как A_REVERSE, но опять же, не гарантируется, что такие комбинации будут работать на всех терминалах.

Пример, в котором строка текста отображается с использованием цветовой пары 1:

stdscr.addstr("Pretty text", curses.color_pair(1))
stdscr.refresh()

Как я уже говорил, цветовая пара состоит из цвета переднего плана и цвета фона. Функция init_pair(n, f, b) изменяет определение цветовой пары n на цвет переднего плана f и цвет фона b. Цветовая пара 0 жестко привязана к белому на черном и не может быть изменена.

Цвета пронумерованы, и start_color() инициализирует 8 основных цветов при активации цветового режима. К ним относятся: 0:черный, 1:красный, 2:зеленый, 3:желтый, 4:синий, 5:пурпурный, 6:голубой и 7:белый. Модуль curses определяет именованные константы для каждого из этих цветов: curses.COLOR_BLACK, curses.COLOR_RED и так далее.

Давайте соберем все это вместе. Чтобы изменить цвет 1 на красный текст на белом фоне, нужно вызвать:

curses.init_pair(1, curses.COLOR_RED, curses.COLOR_WHITE)

При изменении цветовой пары любой текст, уже отображенный с использованием этой цветовой пары, изменится на новые цвета. Вы также можете отобразить новый текст в этом цвете с помощью:

stdscr.addstr(0,0, "RED ALERT!", curses.color_pair(1))

Очень причудливые терминалы могут изменять определения фактических цветов в соответствии с заданным значением RGB. Это позволяет вам изменить цвет 1, который обычно красный, на пурпурный, синий или любой другой цвет, который вам нравится. К сожалению, консоль Linux не поддерживает эту функцию, поэтому я не могу ее опробовать и не могу привести никаких примеров. Вы можете проверить, может ли ваш терминал делать это, вызвав команду can_change_color(), которая возвращает True, если такая возможность есть. Если вам повезло иметь такой талантливый терминал, обратитесь за дополнительной информацией к man-страницам вашей системы.

Пользовательский ввод

Библиотека C curses предлагает только очень простые механизмы ввода. Модуль Python curses добавляет базовый виджет ввода текста. (В других библиотеках, таких как Urwid, есть более обширные коллекции виджетов).

Существует два метода получения ввода из окна:

  • getch() обновляет экран, а затем ждет, пока пользователь нажмет клавишу, отображая ее, если echo() была вызвана ранее. Опционально можно указать координату, на которую должен быть перемещен курсор перед паузой.

  • getkey() делает то же самое, но преобразует целое число в строку. Отдельные символы возвращаются в виде 1-символьных строк, а специальные клавиши, такие как функциональные клавиши, возвращают более длинные строки, содержащие имя клавиши, например KEY_UP или ^G.

Можно не ждать пользователя, используя метод nodelay() окна. После nodelay(True), getch() и getkey() для окна становятся неблокирующими. Чтобы сигнализировать, что вход не готов, getch() возвращает curses.ERR (значение -1), а getkey() вызывает исключение. Существует также функция halfdelay(), которая может быть использована для (фактически) установки таймера на каждом getch(); если в течение заданной задержки (измеряемой десятыми долями секунды) вход не становится доступным, curses выдает исключение.

Метод getch() возвращает целое число; если оно находится в диапазоне от 0 до 255, то представляет собой ASCII-код нажатой клавиши. Значения больше 255 - это специальные клавиши, такие как Page Up, Home или клавиши управления курсором. Вы можете сравнить возвращаемое значение с константами, такими как curses.KEY_PPAGE, curses.KEY_HOME или curses.KEY_LEFT. Главный цикл вашей программы может выглядеть примерно так:

while True:
    c = stdscr.getch()
    if c == ord('p'):
        PrintDocument()
    elif c == ord('q'):
        break  # Exit the while loop
    elif c == curses.KEY_HOME:
        x = y = 0

Модуль curses.ascii предоставляет функции принадлежности к классу ASCII, которые принимают либо целочисленные, либо строковые аргументы из 1 символа; они могут быть полезны при написании более читабельных тестов для таких циклов. Он также поставляет функции преобразования, которые принимают либо целочисленные, либо строковые аргументы с 1 символом и возвращают один и тот же тип. Например, curses.ascii.ctrl() возвращает управляющий символ, соответствующий аргументу.

Существует также метод извлечения всей строки, getstr(). Он используется нечасто, поскольку его функциональность весьма ограничена; единственными доступными клавишами редактирования являются клавиша Backspace и клавиша Enter, которая завершает строку. По желанию можно ограничить строку фиксированным количеством символов.

curses.echo()            # Enable echoing of characters

# Get a 15-character string, with the cursor on the top line
s = stdscr.getstr(0,0, 15)

Модуль curses.textpad предоставляет текстовое поле, которое поддерживает набор привязок клавиш, подобный Emacs. Различные методы класса Textbox поддерживают редактирование с проверкой ввода и сбор результатов редактирования с пробелами или без них. Вот пример:

import curses
from curses.textpad import Textbox, rectangle

def main(stdscr):
    stdscr.addstr(0, 0, "Enter IM message: (hit Ctrl-G to send)")

    editwin = curses.newwin(5,30, 2,1)
    rectangle(stdscr, 1,0, 1+5+1, 1+30+1)
    stdscr.refresh()

    box = Textbox(editwin)

    # Let the user edit until Ctrl-G is struck.
    box.edit()

    # Get resulting contents
    message = box.gather()

Более подробную информацию смотрите в документации по библиотеке curses.textpad.

Дополнительная информация

В этом HOWTO не рассматриваются некоторые продвинутые темы, такие как чтение содержимого экрана или захват событий мыши из экземпляра xterm, но страница библиотеки Python для модуля curses теперь достаточно полная. Вам следует просмотреть ее дальше.

Если вы сомневаетесь в детальном поведении функций curses, обратитесь к страницам руководства для вашей реализации curses, будь то ncurses или проприетарная реализация Unix. На страницах руководства будут описаны все причуды и предоставлены полные списки всех доступных вам функций, атрибутов и символов ACS_*.

Поскольку API curses очень велик, некоторые функции не поддерживаются в интерфейсе Python. Часто это происходит не потому, что их трудно реализовать, а потому, что они еще никому не понадобились. Кроме того, Python пока не поддерживает библиотеку меню, связанную с ncurses. Патчи, добавляющие поддержку этих функций, будут приветствоваться; смотрите the Python Developer’s Guide, чтобы узнать больше об отправке патчей для Python.

Вернуться на верх