Журнал LinuxFormat - перейти на главную

LXF91:Unix API

Материал из Linuxformat
Перейти к: навигация, поиск
Unix API Настоящее программирование для Unix – без прикрас и библиотек-«оберток»

Содержание

ncurses: привет Окнам!

ЧАСТЬ 11 Оконный интерфейс не обязательно должен быть графическим! Андрей Боровский покажет, как создавать удобные приложения, работающие прямо в консоли.
As honour, love, obedience, troops of friends,
I must not look to have; but, in their stead,
Curses, not loud but deep, mouth-honour, breath,
Which the poor heart would fain deny, and dare not.
William Shakespeare, The Tragedy of Macbeth.

В прошлый раз мы научились управлять текстовой консолью с помощью интерфейса termios. Однако, для того, чтобы представить текстовый экран во всем великолепии, возможностей termios недостаточно. Сегодня мы поговорим о дополнительном средстве управления терминалом – библиотеке ncurses. Она и вправду заставляет терминал переливаться всеми цветами радуги (вот почему во всей серии статей из серии Unix API, эта – единственная, в которой вы найдете снимки экранов).

Когда-то давным-давно графические терминалы были редкостью, а пользователи текстовых терминалов хотели работать с интерфейсами, похожими на графические (и, самое главное, использовать новое удобное средство ввода – мышь). Специально для того, чтобы предоставить интерфейс «покажи и щелкни» пользователям текстовыхm терминалов, была разработана библиотека curses (ее название происодит от ее важнейшей функции – управления курсором, а вовсе не от проклятия, которое она накладывает на программистов). Изначально библиотека curses создавалась для BSD UNIX. В Linux используется открытый (распространяющийся на условиях MIT License) клон curses – библиотека ncurses (new curses). Приложений, использующих ncurses в современной Linux-системе не так уж и много. Среди наиболее популярных проектов на базе ncurses можно назвать Midnight Commander, текстовый Web-браузер lynx, программу для чтения новостей tin и почтовый клиент mutt. Сравнительно невысокая популярность ncurses объясняется тем, что ниша ее применения сократилась. Большинство современных компьютеров поддерживают растровую графику, так что если вам нужно реализовать интерфейс «покажи и щелкни», вы, как правило, можете воспользоваться более совершенными графическими средствами. Выбирать ncurses как платформу для нового проекта следует, только если, с одной стороны, вашей программе совершенно необходим интерфейс «покажи и щелкни», а с другой – она должна работать в условиях полного отсутствия графической подсистемы.

Введение в ncurses

Основными понятиями в ncurses являются экран (screen), окно (window) и подокно (subwindow). Экраном называется все пространство, на котором ncurses может выводить данные. С точки зрения ncurses, экран – это матрица ячеек, в которые можно выводить символы. Если монитор работает в текстовом режиме, экран ncurses совпадает с экраном монитора. Если терминал эмулируется графической программой, экраном является рабочая область окна этой программы. Окном ncurses называется прямоугольная часть экрана, для которой определены особые параметры вывода. В частности, размеры окна влияют на перенос и прокрутку выводимых строк. В каком-то смысле окно можно назвать «экраном в экране». В процессе инициализации ncurses автоматически создается окно stdscr, размеры которого совпадают с размерами экрана. Кроме структуры stdscr' по умолчанию создается еще одна структура – curscr. Если окно stdscr предназначено в ncurses для стандартного вывода данных, то curscr содержит копию данных, отражающую текущее состояние экрана. Кода вы записываете данные в stdscr (или другое окно), эти данные не отображаются на экране монитора автоматически. Для того, чтобы сделать новый вывод видимым, вы должны вызывать специальную функцию обновления экрана (refresh() или wrefresh()). Эта функция сравнивает содержимое окна stdscr и curscr и обновляет экран на основе различий между ними, а затем вносит изменения в структуру curscr. Благодаря наличию окна curscr, приложению ncurses не требуется «помнить» весь свой предыдущий вывод и перерисовывать его всякий раз, когда в этом возникает потребность. Этим программы ncurses отличаются от графических программ. В старину, когда терминалы связывались с компьютерами через модемы, использование двух окон давало дополнительное преимущество в скорости обмена данными, ведь программе нужно было передавать на терминал не копию экрана целиком, а только последние изменения.

Помимо стандартных окон ncurses, вы можете создавать собственные окна размера, меньшего stdscr. Ваша программа может работать с несколькими окнами одновременно, выполняя вывод в каждое из них. Кроме окон (windows), программы ncurses могут создавать подокна (subwindows), поведение которых несколько отличается от поведения стандартных окон.

Важнейшей особенностью ncurses является возможность указать произвольную позицию курсора для вывода (и ввода) данных. Позиция курсора отсчитывается от левого верхнего угла текущего окна. Ячейка в верхнем левом углу имеет координаты (0, 0). При работе с функциями ncurses важно помнить, что первой координатой является номер строки (что соответствует y в терминах графического программирования), а второй координатой – номер столбца (соответствует x в графическом режиме).

В случае ошибки функции ncurses обычно возвращают константу ERR. Если функция не должна возвращать какое-то информативное значение (как, например, функция getch()), в случае успешного выполнения она возвращает константу OK.

Первая программа ncurses

Написание первой программы ncurses (она называется cursed, исходный текст вы найдете на DVD в файле cursed.c) мы начнем с перечисления заголовочных файлов.

 #include <termios.h>
 #include <sys/ioctl.h>
 #include <signal.h>
 #include <stdlib.h>
 #include <curses.h>

Помимо уже знакомых нам заголовочных файлов, в программу включен curses.h, который содержит объявления функций, констант и структур данных, экспортируемых библиотекой ncurses.

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

Когда размеры окна терминала меняются, выполняющаяся в нем программа получает сигнал SIGWINCH. Это одновременно и хорошо, и плохо. Хорошо – потому, что терминал информирует программу об изменении своих размеров, плохо – потому, что сигналы имеют особенность вмешиваться в работу программы. Например, если вы напишете программу, использующую ncurses, и не позаботитесь об обработке сигнала SIGWINCH, при изменении размеров окна терминала ваша программа может неожиданно завершиться, оставив терминал в ненормальном состоянии. Давайте посмотрим, как обрабатывается сигнал SIGWINCH в программе cursed.

 void sig_winch(int signo)
 {
    struct winsize size;
    ioctl(fileno(stdout), TIOCGWINSZ, (char *) &size);
    resizeterm(size.ws_row, size.ws_col);
 }

Функция sig_winch() представляет собой обработчик сигнала SIGWINCH. Следует отметить, что изменение размеров экрана во время работы программы ncurses представляет собой довольно нетривиальную задачу и стандартного рецепта, описывающего, что нужно делать, не существует. Разработчики ncurses, как могли, постарались упростить решение этой задачи для программистов, введя функцию resizeterm(). Функцию resizeterm() следует вызывать сразу после изменения размеров окна терминала. Аргументами функции должны быть новые размеры экрана, заданные в строках и столбцах. Функция resizeterm() старается сохранить внешний вид и порядок работы приложения в окне терминала с новыми размерами, но это ей удается не всегда, с чем мы и столкнемся ниже. Необходимые для resizeterm() значения размеров окна мы получаем с помощью специального вызова ioctl(). При этом первым параметром функции ioctl() должен быть дескриптор файла устройства, представляющего терминал. Вторым параметром ioctl() должна быть константа TIOCGWINSZ, а третьим – адрес структуры struct winsize. Структура winsize, определенная в файле <sys/ioctl.h>, включает в себя поля ws_row и ws_col, в которых возвращается число строк и столбцов окна терминала.

Перейдем теперь к функции main() программы cursed:

 int main(int argc, char ** argv)
 {
    initscr();
    signal(SIGWINCH, sig_winch);
    noecho();
    curs_set(0);
    attron(A_BOLD);
    move(5, 15);
    printw(“Hello, brave new curses world!\n”);
    attroff(A_BOLD);
    attron(A_BLINK);
    move(7, 16);
    printw(“Press any key to continue...”);
    refresh();
    getch();
    endwin();
    exit(EXIT_SUCCESS);
 }

Работа с ncurses начинается с вызова функции initscr(). Эта функции инициализирует структуры данных ncurses и переводит терминал в нужный режим. По окончании работы с ncurses следует вызвать функцию endwin(), которая восстанавливает то состояние, в котором терминал находился до инициализации ncurses.

После вызова initscr() мы устанавливаем обработчик сигнал SIGWINCH. Устанавливать обработчик SIGWINCH следует именно после инициализации ncurses, поскольку в нем используется функция resizeterm(), предполагающая, что библиотека ncurses уже инициализирована. Функция noecho() отключает отображение символов, вводимых с клавиатуры. Функция cur_set() управляет видимостью курсора. Если вызвать эту функцию с параметром 0, курсор станет невидимым, вызов же функции с ненулевым параметром снова «включит» его.

Функция attron() позволяет указать некоторые дополнительные атрибуты выводимого текста. Ей можно передать одну или несколько констант, обозначающих атрибуты (в последнем случае их следует объ- единить с помощью операции «|»). Например, атрибут A_UNDERLINE включает подчеркивание текста, атрибут A_REVERSE меняет местами цвет фона и текста, атрибут A_BLINK делает текст мигающим, атрибут A_DIM снижает яркость текста по сравнению с нормальной, атрибут A_BOLD делает текст жирным в монохромном режиме и управляет яркостью цвета в цветном режиме работы монитора. Специальный атрибут COLOR_PAIR() применяется для установки цветов фона и текста. На man-странице, посвященной функции attron(), вы найдете описания и других атрибутов. Все перечисленные выше атрибуты оказывают воздействие только на тот текст, который выводится после их установки.

Сбросить атрибуты можно с помощью функции attroff(). Так же, как и в случае с attron(), функции attroff() можно передать несколько констант, обозначающих атрибуты, разделенных символом «|». Сброс атрибута, как и его установка, влияет только на текст, напечатанный после сброса (текст, выведеный ранее с установленным атрибутом, остается без изменений). В нашей программе мы сначала устанавливаем атрибут A_BOLD. Теперь, до тех пор, пока мы не сбросим его, весь текст будет печататься жирным шрифтом. Но прежде, чем вывести текст этим шрифтом, мы используем еще одну возможность ncurses – вывод текста в произвольной области экрана. Функция move() устанавливает позицию курсора в окне stdscr. Первый аргумент функции – строка, второй аргумент – столбец, в котором должен находиться курсор. Последующий текст печатается, начиная с позиции курсора. Если попытаться поместить курсор за пределы окна, функция move() не станет выполнять никаких действий, и курсор останется на прежнем месте. Мы переводим курсор в позицию (5, 15) и выводим на экран строку “Hello, brave new curses world!” с помощью функции printw(). Функция printw() представляет собой аналог printf() для окна stdscr и имеет тот же список параметров, что и printf().

Затем мы сбрасываем атрибут A_BOLD с помощью функции attroff(), устанавливаем атрибут A_BLIK, переводим курсор в позицию (7,16) и распечатываем строку “Press any key to continue...”.

Хотя мы уже напечатали две строки, на экране терминала все еще ничего нет. Для того чтобы выведенные нами символы стали видимыми, необходимо вызывать функцию refresh(). Функция refresh() является, в некотором роде, избыточной (действительно, почему бы не отображать распечатанный текст сразу же после вызова printw()?). Фактически, она представляет собой пережиток тех времен, когда терминал связывался с компьютером при помощи модема. Контролируя частоту вызовов refresh(), можно было сократить трафик между терминалом и компьютером.

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

Рис. 1

Две строки от ncurses.

Функция getch(), которую мы вызываем далее, предназначена для считывания символов из потока ввода терминала. Функция считывает по одному символу и может работать в двух режимах: блокирующем (по умолчанию) и неблокирующем. В блокирующем режиме выполнение программы приостанавливается до появления символа в потоке ввода, а в неблокирующем – возвращает значение сразу же, независимо от того, есть ли символ в потоке ввода или нет (если символа в потоке ввода нет, функция getch() в неблокирующем режиме возвращает значение ERR). В режиме cbreak() (о котором будет подробнее рассказано в следующей статье), функция, считавшая символ, передает его программе, не дожидаясь, пока пользователь нажмет Enter. Таким образом, программа cursed завершается сразу же после нажатия на любую клавишу.

Библиотека ncurses не является частью стандартной библиотеки glibc, поэтому во время сборки программы ее нужно подключать явным образом. Например, чтобы скомпилировать cursed, следует набрать:

gcc cursed.c -o cursed -lncurses

Окна

В текстовых интерфейсах, построенных на основе ncurses, окна играют такую же важную роль, как и в графических интерфейсах. Прежде чем переходить к созданию приложений, использующих окна, необходимо внести некоторые уточнения в описание интерфейса ncurses. Прежде всего, вы должны понимать, что при работе с ncurses вы всегда имеете дело с окнами. В рассмотренной выше программе cursed мы работали с окном stdscr. Многие из функций ncurses, с которыми мы работали в программе cursed (attron(), move(), printw(), attroff(), getch()) являются частными вариантами функций, предназначенными специально для работы с окном stdscr. У этих функций есть обобщенные аналоги, способные работать с любым окном. Списки параметров обобщенных функций совпадают со списками параметров специальных функций, за исключением того, что первым параметром обобщенной функции должен быть указатель на структуру, определяющую окно. Например, для установки атрибутов текста в окне применяется функция wattron(). Первым параметром этой функции служит указатель на структуру, определяющую окно, а второй параметр wattron() полностью аналогичен параметру функции attron().

Как получить указатель на структуру, определяющую окно? Переменная stdscr, которую предоставляет библиотека ncurses, является указателем на структуру, представляющую корневое окно stdscr, занимающее весь экран. Эта переменная определена как

extern WINDOW * stdscr;

Тип WINDOW как раз и является структурой, описывающей окно. Из того, что stdscr является обычным окном ncurses, следует, что вместо функций, предназначенных специально для stdscr, мы можем использовать их обобщенные аналоги, указывая переменную stdscr в качестве идентификатора окна. Например, вызов attron(A_BOLD) эквивалентен вызову wattron(stdscr, A_BOLD). Обобщенным вариантом функции attroff() является функция wattroff(), а обобщенным вариантом функции move() функция wmove(). Вызов

move(5, 15);

из программы cursed можно заменить вызовом

wmove(stdscr, 5, 15);

Функции printw() соответствует обобщенная функция wprintw(). Функции getch() соответствует функция wgetch(), аргументом которой должен быть все тот же указатель на WINDOW.

Отметим, что многообразие функций ввода/вывода символов ncurses не исчерпывается парами getch()/wgetch() и printw()/wprintw(). В описании API библиотеки вы найдете множество других полезных возможностей.

Перейдем, наконец, к созданию собственных окон. Смысл создания окна заключается в том, чтобы ограничить область вывода текста (и область применения различных атрибутов) отдельными участками экрана. Библиотека ncurses предоставляет в наше распоряжение несколько функций, создающих новые окна. Самой простой и часто используемой является функция newwin(). Она принимает четыре параметра. Первые два соответствуют количеству строк и столбцов в создаваемом окне, а вторые указывают положение верхнего левого угла нового окна (строка и столбец) относительно окна stdscr. Функция newwin() возвращает указатель на структуру WINDOW или NULL в случае ошибки. После завершения работы с окном, выделенные ему ресурсы следует высвободить с помощью функции delwin(). Единственный параметр этой функции – указатель на структуру WINDOW, которую следует удалить.

Помимо функции newwin(), нам будет полезно познакомиться еще с двумя функциями: subwin() и derwin(). Эти две функции предназначены для создания подокон. Списки параметров у этих функций такие же, как и у newwin(), с той лишь разницей, что первым параметром каждой функции является указатель на структуру WINDOW, соответствующую родительскому окну. Последние два аргумента у subwin() и derwin() интерпретируются по-разному. У функции subwin() они задают положение верхнего левого угла окна относительно экрана, а у функции derwin() – относительно родительского окна.

Чем же подокно отличается от обычного окна? Окно и его подокно разделяют массив, в котором хранятся символы и их атрибуты. Новое подокно наследует все атрибуты своего родителя. Эти атрибуты затем могут быть изменены, что не повлияет на атрибуты родительского окна.

Мы займемся созданием окон в программе cursedwindows (файл cursedwindows.c на диске). На всякий случай заявляю, что у меня нет ни малейшего желания оскорбить Microsoft. Список заголовочных файлов и обработчик сигнала SIGWINCH у программы cursedwindows такие же, как и у программы cursed, так что в листинге мы их пропустим и рассмотрим только функцию main().

 int main(int argc, char ** argv)
 {
    WINDOW * wnd;
    WINDOW * subwnd;
    initscr();
    signal(SIGWINCH, sig_winch);
    curs_set(0);
    refresh();
    wnd = newwin(6, 18, 2, 4);
    box(wnd,|,-);
    subwnd = derwin(wnd, 4, 16, 1, 1);
    wprintw(subwnd, “Hello, brave new curses world!\n”);
    wrefresh(wnd);
    delwin(subwnd);
    delwin(wnd);
    move(9, 0);
    printw(“Press any key to continue...”);
    refresh();
    getch();
    endwin();
    exit(EXIT_SUCCESS);
 }

В функции main() мы, как и прежде, инициализируем ncurses с помощью функции initscr() и устанавливаем обработчик SIGWINCH. Далее, как и в предыдущем примере, мы делаем курсор невидимым. После этого мы обновлем экран с помощью refresh(), а затем – создаем окно wnd с помощью функции newwin(). Оно насчитывает 6 строк и 18 столбцов, а его верхний левый угол находится в ячейке (2, 4) окна stdscr. Функция box(), которую мы вызываем далее, позволяет создать рамку вдоль границы окна. Ее аргументы: идентификатор окна и символы, используемые, соответственно, для рисования вертикальной и горизонтальной границы. Теперь было бы логично вывести в окно, украшенное рамкой, но тут возникает одна сложность. Поскольку символы рамки сами находятся внутри окна, символы текста могут затереть их в процессе вывода. Мы решаем эту проблему с помощью создания подокна subwnd внутри окна wnd и вывода текста в это под окно. Поскольку окно subwnd по размерам меньше, чем окно wnd, символы рамки не будут затерты.

Теперь мы можем распечатать текст – делается это с помощью функции wprintw(), которой передается идентификатор окна subwnd Для того, чтобы символы, напечатаны в окне, стали видимыми, мы должны воспользоваться функцией wrefresh(). Мы вызываем ее толь ко для окна wnd, поскольку именно оно содержит символьный массив и окно subwnd. Обратите внимание, что символы строки “Hello, brave new curses world!”, которую мы печатаем в окне с помощью функции wprintw(), переносятся при достижении границы окна (Рис. 2). После завершения работы с окнами следует удалить структуры wnd и subwnd воспользовавшись функциями delwin(). Весь вывод, выполненный в окне wnd, останется на экране (точнее, в окне curscr) до тех пор, пока вы не перезапишете его другим выводом.

Рис. 2

Вывод текста в окне и за его пределами.

На этом наше знакомство с ncurses не заканчивается. В следующей статье мы рассмотрим управление цветом и ввод данных средствами ncurses. LXF

Персональные инструменты
купить
подписаться
Яндекс.Метрика