Документация Python неофициальный перевод
Содержание страницы

Программирование с curses на PythonCurses Programming with Python

Автор:A.M. Kuchling, Eric S. Raymond
Версия:2.04

Аннотация

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

Что такое curses?What is curses?

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

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

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

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

Версия Python для Windows не включает curses модуль. Доступна портированная версия UniCurses. Можно также попробовать модуль Console, написанный Фредриком Лундом, который не использует тот же API, что curses, но предоставляет вывод текста с управлением курсором и полную поддержку мыши и клавиатуры.

Модуль curses в PythonThe Python curses module

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

Данное руководство (HOWTO) представляет собой введение в написание текстовых программ с использованием curses и Python. Оно не претендует на роль полного руководства по API curses; для этого обратитесь к разделу о ncurses в руководстве по библиотеке Python и к страницам руководства C по ncurses. Однако оно даст вам основные понятия.

Запуск и завершение приложения cursesStarting and ending a curses application

Прежде чем что-либо делать, 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 эту работу, необходимо включить режим keypad.

stdscr.keypad(True)

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

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

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

curses.endwin()

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

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

from curses import wrapper

def main(stdscr):
    # Очистить экран
    stdscr.clear()

    # Это вызывает ZeroDivisionError, когда 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() принимает вызываемый объект и выполняет описанные выше инициализации, а также инициализирует цвета, если поддерживается цвет. Затем wrapper() запускает переданный вызываемый объект. Когда вызываемый объект возвращает управление, wrapper() восстанавливает исходное состояние терминала. Вызываемый объект вызывается внутри tryexcept, который перехватывает исключения, восстанавливает состояние терминала и затем повторно возбуждает исключение. Таким образом, при возникновении исключения терминал не останется в странном состоянии, и можно будет прочитать сообщение об исключении и трассировку.

Окна и Pad’ыWindows and Pads

Окна – это основная абстракция в 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 перерисовать окно не сильно усложняет программирование. Большинство программ выполняют серию действий, а затем приостанавливаются в ожидании нажатия клавиши или другого действия пользователя. Всё, что нужно – убедиться, что экран перерисован перед паузой в ожидании ввода, вызвав stdscr.refresh() или метод refresh() другого соответствующего окна.

Pad – это частный случай окна; он может быть больше фактического экрана, и единовременно отображается только часть pad’а. Для создания pad’а требуются его высота и ширина, а для обновления pad’а необходимо указать координаты области на экране, в которой будет отображаться часть pad’а.

pad = curses.newpad(100, 100)
# Эти циклы заполняют pad буквами; addch() описана
# в следующем разделе
for y in range(0, 99):
    for x in range(0, 99):
        pad.addch(y,x, ord('a') + (x*x+y*y) % 26)

# Отображает участок pad в центре экрана.
# (0,0) : координата верхнего левого угла отображаемой области pad.
# (5,5) : координата верхнего левого угла области окна, которая будет заполнена
#         содержимым pad.
# (20, 75) : координата нижнего правого угла области окна, которая будет
#          : заполнена содержимым pad.
pad.refresh( 0,0, 5,5, 20,75)

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

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

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

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

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

С точки зрения программиста на C, 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 – это верхний левый угол рамки (удобно для рисования границ). Также можно использовать соответствующий символ Юникода.

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

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

Атрибуты и цветAttributes and Color

Символы можно отображать разными способами. Строки состояния в текстовых приложениях обычно выводятся инверсным видео, а в текстовом просмотрщике может потребоваться выделить определённые слова. 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, за ней следуют цветные терминалы xterm.

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

Библиотека 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, если такая возможность есть. Если вам повезло иметь такой талантливый терминал, обратитесь к системным справочным страницам за дополнительной информацией.

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

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

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

  • getch() обновляет экран, а затем ожидает нажатия клавиши пользователем, отображая клавишу, если ранее был вызван echo(). Можно дополнительно указать координаты, на которые следует переместить курсор перед ожиданием.
  • getkey() делает то же самое, но преобразует целое число в строку. Отдельные символы возвращаются как строки из одного символа, а специальные клавиши, такие как функциональные, возвращают более длинные строки, содержащие имя клавиши, например 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  # Выйти из цикла while
    elif c == curses.KEY_HOME:
        x = y = 0

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

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

curses.echo()            # Включить эхо-вывод символов

# Получить строку из 15 символов, курсор на верхней строке
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)

    # Дать пользователю редактировать, пока не будет нажато Ctrl-G.
    box.edit()

    # Получить полученное содержимое
    message = box.gather()

Дополнительные сведения см. в документации библиотеки curses.textpad.

Для получения дополнительной информацииFor More Information

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

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

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