|
||||
|
Глава 6Управление текстовыми экранами с помощью библиотеки curses В главе 5 вы узнали, как улучшить управление вводом символов и как обеспечить вывод символов способом, не зависящим от особенностей конкретного терминала. Проблема использования общего терминального интерфейса (GTI или termios) и манипулирование escape-последовательностями с помощью tparm и родственных функций заключается в необходимости применения большого объема программного кода низкого уровня. Для многих программ предпочтительней интерфейс высокого уровня. Мы хотели бы иметь возможность просто рисовать на экране и применять библиотеку функций для автоматического отслеживания аппаратных характеристик терминала. В этой главе вы узнаете именно о такой библиотеке, называемой curses. Стандарт curses очень важен как компромисс между простыми "строковыми" программами и полностью графическими (которые обычно труднее программировать) программами в графической оболочке X Window System, такими как GTK+/GNOME и Qt/KDE, В ОС Linux есть библиотека svgatib (Super VGA Library, библиотека низкоуровневой графики), но она не является стандартной библиотекой UNIX, поэтому обычно не доступна в других UNIX-подобных операционных системах. Библиотека curses применяется во многих полноэкранных приложениях как довольно легкий и аппаратно-независимый способ разработки полноэкранных, хотя и символьных программ. Такие программы почти всегда легче писать с помощью библиотеки curses, чем непосредственно применять escape-последовательности. Эта библиотека также может управлять клавиатурой, обеспечивая легкий в использовании, не блокирующий режим ввода символов. Вы можете столкнуться с тем, что несколько примеров из этой главы не всегда будут отображаться на простой консоли Linux так, как вы ожидали. Бывают случаи, когда сочетание библиотеки curses и определения консоли терминала получается немного не согласованным и приводит в результате к несколько странным компоновкам при использовании curses. Но если для отображения вывода применить графическую оболочку X Window System и окно xterm, все встанет на свои места. В этой главе обсуждаются следующие темы: □ применение библиотеки curses: □ основные идеи curses; □ управление базовыми вводом и выводом; □ использование множественных окон; □ применение режима дополнительной клавиатуры (keypad mode); □ добавление цвета. Мы закончим главу переработкой на языке С программы, управляющей коллекцией компакт-дисков, подытожив все, чему вы научились к этому моменту. Компиляция с библиотекой cursesБиблиотека curses получила свое название благодаря способности оптимизировать перемещение курсора и минимизировать необходимые обновления экрана, а следовательно, уменьшить количество символов, нуждающихся в пересылке на текстовый терминал. Несмотря на то, что сейчас количество символов вывода гораздо менее важно, чем во времена неинтеллектуальных терминалов и низкоскоростных модемов, библиотека curses выжила как полезное дополнение к набору инструментов программиста. Поскольку curses — это библиотека, для ее применения необходимо включить в программу заголовочный файл, объявления функций и макросы из соответствующей системной библиотеки. Существует несколько разных реализаций библиотеки curses. Первоначальная версия появилась в системе BSD UNIX и затем была включена в разновидности UNIX стиля System V прежде, чем была стандартизована группой X/Open. Система Linux использует вариант ncurses ("new curses") — свободно распространяемую версию System V Release 4.0 curses, разработанную для Linux. Эта реализация хорошо переносится на другие версии UNIX, хотя и содержит несколько непереносимых дополнительных функций. Есть даже версии библиотеки для MS-DOS и Windows. Если вы увидите, что библиотека curses, поставляемая с вашей версией системы UNIX, не поддерживает некоторые функции, попытайтесь получить копию альтернативной библиотеки ncurses. Обычно пользователи ОС Linux обнаруживают уже установленную библиотеку ncurses или, по крайней мере, ее компоненты, необходимые для выполнения программ на базе библиотеки curses. Если инструментальные библиотеки для нее заранее не установлены в вашем дистрибутиве (нет файла curses.h или файла библиотеки curses для редактирования связей), для большинства основных дистрибутивов их всегда можно найти в виде стандартного пакета с именем наподобие ibncurses5-dev. Примечание При компиляции программ, использующих curses, следует подключить заголовочный файл curses.h и на этапе редактирования связей саму библиотеку с помощью аргумента -lcurses. Во многих системах Linux вы можете применять просто библиотеку curses, а потом обнаружить, что на самом деле вы пользуетесь усовершенствованной, более новой реализацией ncurses. Для того чтобы проверить, как установлена библиотека curses в вашей системе, выполните команду ls -l /usr/include/*curses.h для просмотра заголовочных файлов и ls -l /usr/lib/lib*curses* для проверки библиотечных файлов. Если вы увидите, что curses.h и ncurses.h — прямо связанные файлы, и существует файл библиотеки ncurses, то у вас есть возможность компилировать файлы из этой главы с помощью следующей команды: $ gcc program. с -о program -lcurses Если установка curses в вашей системе не использует автоматически ncurses, вы сможете явно задать использование ncurses, включив файл ncurses.h вместо файла curses.h и выполнив следующую команду: $ gcc -I/usr/include/ncurses program.с -о program -lncurses в которой опция -Iзадает каталог для поиска заголовочного файла. Примечание Если вы точно не знаете, как установлена библиотека curses в вашей системе, обратитесь к страницам интерактивного справочного руководства, посвященным ncurses, или просмотрите другую интерактивную документацию; обычное место ее хранения — каталог /usr/share/doc/, в котором вы найдете каталог curses или ncurses часто с присоединенным в конце номером версии. Терминология библиотеки curses и общие представленияПодпрограммы curses действуют на экранах, в окнах и вложенных окнах или подокнах. Экран — это устройство (обычно экран терминала, но может быть и экран эмулятора терминала xterm), на который вы записываете информацию. Он занимает все доступное пространство дисплея этого устройства, Если экран — окно терминала в графическом окне, то он представляет собой совокупность всех доступных символьных позиций в окне терминала. Всегда существует, по крайней мере, одно окно curses с именем stdscr, совпадающее по размеру с физическим экраном. Вы можете создавать дополнительные окна с размером, меньшим, чем размер экрана. Окна могут накладываться друг на друга и иметь много вложенных окон, но каждое из них всегда должно находиться внутри родительского окна. Библиотека curses поддерживает две структуры данных, действующие как отображение экрана терминала: stdscrи curscr. Структура stdscr, наиболее важная из двух, обновляется, когда функции curses формируют вывод. Структура данных stdscr— "стандартный экран". Она действует во многом так же, как стандартный вывод stdout из библиотеки stdio. Эта структура — стандартное окно вывода в программах, использующих библиотеку curses. Структура curscrпохожа на stdscr, но хранит внешний вид отображаемого в текущий момент экрана. Вывод, записанный в структуру stdscr, не появляется на экране до тех пор, пока программа не вызовет функцию refresh, в которой библиотека curses сравнивает содержимое stdscr(как должен выглядеть экран) со второй структурой curscr(как выглядит экран в данный момент). Затем cursesиспользует различия между этими двумя структурами для обновления экрана. Некоторым программам с использованием curses нужно знать, что библиотека поддерживает структуру stdscr, которая применяется в нескольких функциях curses как параметр. Однако действительная структура stdscrреализуется по-разному, и к ней никогда не следует обращаться напрямую. У программ с использованием curses практически нет нужды в применении структуры curscr. Таким образом, процесс вывода символов в программе с применением curses выглядит следующим образом: 1. Используется функция библиотеки curses для обновления логического экрана. 2. Запрашивается у библиотеки curses обновление физического экрана с помощью функции refresh. Преимущество двухшагового подхода помимо большей легкости при программировании — очень эффективный вариант обновления экрана curses. Это может быть не столь важно для экрана консоли, но становится существенным показателем, если программа выполняется через медленное сетевое соединение. Программа с использованием curses выполнит множество вызовов функций вывода на логический экран, возможно, перемещая курсор по всему экрану для того, чтобы достичь нужной позиции вывода текста или рисования линий и рамок. На каком-то этапе пользователю потребуется увидеть весь этот вывод. Когда это произойдет (обычно во время вызова функции refresh), библиотека curses найдет оптимальный способ формирования физического экрана, соответствующего логическому. Применяя нужные характеристики терминала и оптимизируя перемещения курсора, curses часто обновляет экран, выводя гораздо меньше символов по сравнению со всеми операциями записи на экран, если бы они выполнялись немедленно. Макет логического экрана — это символьный массив, упорядоченный по строкам и столбцам, с начальной позицией экрана (0, 0) в левом верхнем углу (рис. 6.1). Рис. 6.1 Во всех функциях библиотеки curses применяются координаты со значением у (строки) перед значением х (столбцы). Каждая позиция хранит не только символ, расположенный в этом месте экрана, но и его атрибуты. Атрибуты, которые можно отобразить, зависят от физических характеристик терминала, но, как правило, они включают жирное начертание и подчеркивание символа. На консолях Linux вам также доступны негативное изображение и цвет, о которых речь пойдет далее в этой главе. Поскольку библиотека curses нуждается в создании и удалении некоторых временных структур данных, все программы с использованием curses должны инициализировать библиотеку перед применением и затем разрешить ей восстановить первоначальные установки после ее применения. Делается это с помощью вызовов пары функций: initscrи endwin(упражнение 6.1). Упражнение 6.1. Программа с использованием curses, выводящая приветствие В этом примере вы напишите очень простую использующую curses программу screen1.c, чтобы показать эти и другие базовые функции в действии. Далее будут описаны их прототипы. 1. Вставьте заголовочный файл curses.h и в функцию main, включите вызовы для инициализации и возврата в исходное состояние библиотеки curses: #include <unistd.h> #include <stdlib.h> #include <curses.h> int main() { initscr(); ... endwin(); exit(EXIT_SUCCESS); } 2. Внутрь поместите код для перемещения курсора в точку (5, 15) на логическом экране, выведите приветствие "Hello World" и обновите реальный экран. В заключение примените вызов sleep(2) для того, чтобы приостановить выполнение программы на две секунды и просмотреть вывод на экран перед ее завершением: move(5, 15); printw("%s", "Hello World"); refresh(); sleep(2); Пока программа выполняется, вы видите фразу "Hello World" в левом верхнем квадранте пустого экрана (рис. 6.2). Рис. 6.2 Как это работает Эта программа инициализирует библиотеку curses, перемещает курсор в заданную точку экрана и отображает некоторый текст. После короткой паузы она закрывает библиотеку и завершается. ЭкранКак: вы уже видели, все программы с использованием curses должны начинаться с вызова функции initscrи заканчиваться вызовом функции endwin. Далее приведены их описания из заголовочного файла. #include <curses.h> WINDOW *initscr(void); int endwin(void); Функция initscrдолжна вызываться только один раз в каждой программе. В случае успешного завершения она возвращает указатель на структуру stdscr. Если функция заканчивается аварийно, она просто выводит диагностическое сообщение об ошибке и вызывает завершение программы. Функция endwinвозвращает константу OK в случае успешного завершения и err в случае неудачи. Вы можете вызвать ее для того, чтобы покинуть curses, а позже возобновить функционирование библиотеки curses, вызвав clearok(stdscr, 1)и refresh. Это позволит библиотеке совершенно забыть, как выглядит физический экран, и заставит ее выполнить полное обновление экрана. Вывод на экранДля обновления экрана предоставляется несколько базовых функций. #include <curses.h> int addch(const chtype char_to_add); int addchstr(chtype *const string_to_add); int printw(char *format, ...); int refresh(void); int box(WINDOW *win_ptr, chtype vertical_char, chtype horizontal_char); int insch(chtype char_to_insert); int insertln(void); int delch(void); int deleteln(void); int beep(void); int flash(void); У библиотеки curses есть свой символьный тип данных chtype, который может содержать больше разрядов, чем стандартный тип char. В стандартной версии ncurses для ОС Linux chtypeна самом деле — синоним стандартного типа unsigned long. Функции addchи addchstrвставляют заданные символ или строку в текущую позицию на экране. Функция printwформатирует строку так же, как функция printf, и помещает в текущую позицию на экране. Функция refreshвызывает обновление физического экрана, возвращая OKв случае успеха и ERRпри возникновении ошибки. Функция boxпозволяет нарисовать рамку вокруг окна. Примечание Функция inschвставляет символ, сдвигая имеющиеся символы вправо. При этом не определено, что произойдет в конце строки, результат зависит от используемого терминала. Функция insertlnвставляет пустую строку, перемещая имеющиеся строки на одну вниз. Функции delchи deletelnаналогичны функциям insert. Для получения звука можно вызвать функцию beep. Немногие терминалы не способны издавать звуки, в этом случае некоторые установки библиотеки curses при вызове beepзаставят экран мигать. Если вы работаете в густонаселенном офисе и звуковые сигналы могут издавать многие компьютеры, возможно, вы сочтете мигание предпочтительным режимом. Как и ожидалось, функция flashвызывает мигание экрана, если это невозможно, она попробует заставить терминал издать звуковой сигнал взамен. Считывание с экранаВы можете считывать символы с экрана, хотя эта функциональная возможность применяется нечасто, поскольку гораздо легче отслеживать то, что выводится. Если вам все-таки это потребуется, выполняйте считывание с помощью следующих функций: #include <curses.h> chtype inch(void); int instr(char *string); int innstr(char *string, int number_of_characters); Функция inchдолжна быть всегда доступна, а функции instrи innstrне всегда поддерживаются. Функция inchвозвращает символ из текущей позиции курсора на экране и данные о его атрибутах. Обратите внимание на то, что функция возвращает значение не char, a chtype, в то время как функции instrи innstrпишут в массивы с элементами типа char. Очистка экранаСуществует четыре основных способа очистки области экрана: #include <curses.h> int erase (void); int clear(void); int clrtobot(void); int clrtoeol(void); Функция eraseзаписывает пробелы во все позиции экрана. Функция clear, как и erase, очищает экран, но вызывает перерисовку экрана с помощью внутреннего вызова низкоуровневой функции clear ok, которая выполняет последовательность очистки экрана и новое отображение экрана при следующем вызове refresh. Функция clearобычно применяет команду терминала, которая очищает весь экран, а не пытается стереть текущие непробельные символы во всех точках экрана. Это делает функцию clearнадежным средством очистки экрана. Сочетание функции clearс последующей функцией refreshможет обеспечить удобную команду перерисовки экрана в том случае, когда изображение на экране беспорядочно или испорчено каким-либо образом. Функция clrtobotочищает экран, начиная с текущей позиции курсора и далее до конца экрана, а функция clrtoeolочищает экран, начиная с текущей позиции курсора до конца строки, в которой находится курсор. Перемещение курсораДля перемещения курсора применяется единственная функция с дополнительной командой, управляющей положением курсора после обновления экрана. #include <curses.h> int move(int new_y, int new_x); int leaveok(WINDOW *window_ptr, bool leave_flag); Функция moveпросто переносит позицию логического курсора в заданное место на экране. Напоминаем о том, что начало экранных координат (0, 0) находится в левом верхнем углу экрана. В большинстве версий библиотеки curses две глобальные целочисленные переменные, LINESи COLUMNS, определяют размер физического экрана и могут применяться для определения максимально допустимых значений параметров new_yи new_x. Вызов moveсам по себе не приводит к перемещению физического курсора. Он только изменяет позицию на логическом экране, в которой появится следующий вывод. Если вы хотите, чтобы экранный курсор переместился немедленно после вызова функции move, вставьте следом за ним вызов функции refresh. Функция leaveokустанавливает флаг, управляющий положением курсора на физическом экране после его обновления. По умолчанию флаг равен false, и после вызова refreshаппаратный курсор остается в той же точке экрана, что и логический курсор. Если флаг равен true, аппаратный курсор можно оставить в случайно выбранной точке экрана. Как правило, значение, устанавливаемое по умолчанию, предпочтительней, т.к. курсор остается в не лишенной смысла позиции. Атрибуты символовУ всех символов, обрабатываемых curses, могут быть определенные атрибуты, управляющие способом отображения символа на экране при условии, что оборудование, применяемое для их отображения, поддерживает требуемый атрибут. Определены следующие атрибуты: A_BLINK, A_BOLD, A_DIM, A_REVERSE, A_STANDOUTи A_UNDERLINE. Вы можете использовать перечисленные далее функции для установки атрибутов по одному или все вместе. #include <curses.h> int attron(chtype attribute); int attroff(chtype attribute); int attrset(chtype attribute); int standout(void); int standend(void); Функция attrsetустанавливает атрибуты curses, функции attronи attroffвключают и отключают заданные атрибуты, не портя остальные, а функции standoutи standendобеспечивают более выразительный режим выделения или "лучший из всех" режим. На большинстве терминалов выбирается инверсия. Выполните упражнение 6.2. Упражнение 6.2. Перемещение, вставка и атрибутыТеперь, когда вы знаете больше об управлении экраном, можно испытать более сложный пример moveadd.c. Вы включите несколько вызовов функций refreshи sleepв этот пример, чтобы на каждом шаге видеть, как выглядит экран. Обычно программы с использованием библиотеки curses стараются обновлять экран как можно реже, поскольку это не слишком высокопроизводительная операция. Программный код написан с некоторой долей искусственности для обеспечения большей наглядности. 1. Для начала вставьте несколько заголовочных файлов, определите несколько символьных массивов и указатель на них, а затем инициализируйте структуры библиотеки curses: #include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <curses.h> int main() { const char witch_one[] = " First Witch "; const char witch_two[] = " Second Witch "; const char *scan_ptr; initscr(); 2. Теперь для трех начальных текстовых фрагментов, которые появляются на экране через определенные интервалы, включите и отключите соответствующие флаги атрибутов; move(5, 15); attron(A_BOLD); printw("%s", "Macbeth"); attroff(A_BOLD); refresh(); sleep(1); move(8, 15); attron(A_STANDOUT); printw("%s", "Thunder and Lightning"); attroff(A_STANDOUT); refresh(); sleep(1); move(10, 10); printw("%s", "When shall we three meet again"); move(11, 23); printw("%s", "In thunder, lightning, or in rain ?"); move(13, 10); printw("%s", "When the hurlyburly's done, "); move(14, 23); printw("%s", "When the battle's lost and won."); refresh(); sleep(1); 3. Действующие лица идентифицированы, и их имена выводятся посимвольно: attron(A_DIM); scan_ptr = witch_one + strlen(witch_one) - 1; while (scan_ptr != witch_one) { move(10, 10); insch(*scan_ptr--); } scan_ptr = witch_two + strlen(witch_two) - 1; while (scan_ptr != witch_two) { move(13, 10); insch(*scan_ptr--); } attroff(A_DIM); refresh(); sleep(1); 4. В заключение переместите курсор в правый нижний угол экрана, а затем подготовьте и выполните завершение: move(LINES - 1, COLS - 1); refresh(); sleep(1); endwin(); exit(EXIT_SUCCESS); } Когда вы выполните программу, заключительный экран будет выглядеть так, как показано на рис. 6.3. К сожалению, снимок экрана не дает полного впечатления и не показывает курсор, установленный в правом нижнем углу экрана. Эмулятор xterm может быть более подходящей средой для точного отображения программ, чем обычная консоль. Рис. 6.3 Как это работает После инициализации некоторых переменных и экрана с помощью библиотеки curses вы применили функции moveдля перемещения курсора по экрану. Посредством функций attronи attroffвы управляли атрибутами текста, выводимого в заданную точку экрана. Далее перед закрытием библиотеки curses и завершением программа продемонстрировала, как вставлять символы функцией insch. КлавиатураНаряду с предоставлением интерфейса, облегчающего управление экраном, библиотека curses также предлагает средства, облегчающие управление клавиатурой. Режимы клавиатурыПроцедуры считывания с клавиатуры управляются режимами. Режимы устанавливаются с помощью следующих функций: #include <curses.h> int echo(void); int noecho(void); int cbreak(void); int nocbreak(void); int raw(void); int noraw(void); Функции echoи noechoпросто включают и отключают отображение символов, набираемых на клавиатуре. Оставшиеся четыре функции управляют тем, как символы, набранные на терминале, становятся доступны программе с применением curses. Для того чтобы понять функцию cbreak, необходимо иметь представление о стандартном режиме ввода. Когда программа, использующая библиотеку curses, стартует с вызова функции initscr, устанавливается режим ввода, называемый режимом с обработкой (cooked mode). Это означает построчную обработку, т.е. ввод становится доступен программе после нажатия пользователем клавиши <Enter> (или <Return> на некоторых клавиатурах). Специальные символы на клавиатуре включены, поэтому набор соответствующих клавиатурных последовательностей может сгенерировать сигнал в программе. Управление потоком, если терминал запускается с терминала, также включено. Вызывая функцию cbreak, программа может установить режим ввода cbreak, в котором символы становятся доступными программе сразу после их набора, а не помещаются в буфер и передаются программе только после нажатия клавиши <Enter>. Как и в режиме с обработкой, специальные символы клавиатуры действуют, а простые клавиши, например <Backspace>, передаются для обработки непосредственно в программу, поэтому если вы хотите, чтобы нажатие клавиши <Backspace> приводило к привычным действиям, то вы должны запрограммировать их самостоятельно. Вызов функции rawотключает обработку специальных символов, поэтому становится невозможной генерация сигналов или управление потоком с помощью набранных на клавиатуре специальных символьных последовательностей. Вызов функции nocbreakвозвращает режим ввода в режим с обработкой символов, но режим обработки специальных символов не изменяет; вызов norawвосстанавливает и режим с обработкой, и обработку специальных символов. Клавиатурный вводЧтение с клавиатуры — очень простая операция. К основным функциям чтения относятся следующие: #include <curses.h> int getch(void); int getstr(char *string); int getnstr(char *string, int number_of_characters); int scanw(char *format, ...); Все они действуют подобно своим аналогам, не входящим в библиотеку curses, getchar, getsи scanf. Обратите внимание на то, что у функции getstrнет возможности ограничить длину возвращаемой строки, поэтому применять ее следует с большой осторожностью. Если ваша версия библиотеки curses поддерживает функцию getnstr, позволяющую ограничить количество считываемых символов, всегда применяйте ее вместо функции getstr. Это очень напоминает поведение функций getsи fgets, с которыми вы познакомились в главе 3. В упражнении 6.3 для демонстрации управления клавиатурой приведен пример короткой программы ipmode.c. Упражнение 6.3. Режим клавиатуры и ввод1. Наберите программу и включите в нее начальные вызовы библиотеки curses: #include <unistd.h> #include <stdlib.h> #include <curses.h> #include <string.h> #define PW_LEN 256 #define NAME_LEN 256 int main() { char name[NAME_LEN]; char password[PW_LEN]; const char *real_password = "xyzzy"; int i = 0; initscr(); move(5, 10); printw("%s", "Please login:"); move(7, 10); printw("%s", "User name: "); getstr(name); move(9, 10); printw("%s", "Password: "); refresh(); 2. Когда пользователь вводит свой пароль, необходимо остановить отображение символов на экране. Далее сравните введенный пароль со строкой xyzzy: cbreak(); noecho(); memset(password, '\0', sizeof(password)); while (i < PW_LEN) { password[i] = getch(); if (password[i] == '\n') break; move(8, 20 + i); addch('*'); refresh(); i++; } 3. В заключение восстановите отображение символов и выведите сообщение об успешном или неудачном завершении: echo(); nocbreak(); move(11, 10); if (strncmp(real_password, password, strlen(real_password)) == 0) printw("%s", "Correct"); else printw("%s", "Wrong"); printw("%s", " password"); refresh(); sleep(2); endwin(); exit(EXIT_SUCCESS); } Как это работает Остановив отображение клавиатурного ввода и установив режим cbreak, вы выделяете область памяти, готовую к приему пароля. Каждый введенный символ пароля немедленно обрабатывается, и на экран выводится *в следующей позиции курсора. Вам необходимо каждый раз обновлять экран и сравнивать с помощью функции strcmpдве строки: введенный и реальный пароли. Примечание ОкнаДо сих пор вы использовали терминал как средство полноэкранного вывода. Это вполне подходит для маленьких простых программ, но библиотека curses идет гораздо дальше. Вы можете на физическом экране одновременно отображать множество окон разных размеров. Многие из описанных в этом разделе функций поддерживаются в терминах стандарта X/Open так называемой "расширенной" версией curses. Но поскольку они поддерживаются библиотекой ncurses, не велика проблема сделать их доступными на большинстве платформ. Пора идти дальше и применить множественные окна. Вы увидите, как обобщаются до сих пор использовавшиеся команды и применяются в сценариях с множественными окнами. Структура WINDOWНесмотря на то, что мы уже упоминали стандартный экран stdscr, пока у вас не было необходимости в его применении, поскольку почти все рассматриваемые до сих пор функции полагали, что они работают на экране stdscr, и не требовалось передавать его как параметр. stdscr— это специальный случай структуры WINDOW, как stdout — специальный случай файлового потока. Обычно структура WINDOWобъявляется в файле curses.h и, несмотря на то, что ее просмотр может быть очень поучителен, программы никогда не используют эту структуру напрямую, т.к. она может различаться в разных реализациях. Вы можете создать и уничтожить окно с помощью вызовов функций newwin и delwin: #include <curses.h> WINDOW *newwin(int num_of_lines, int num_of_cols, int start_y, int start_x); int delwin(WINDOW *window_to_delete); Функция newwinсоздает новое окно в позиции экрана ( start_y, int start_x) и с заданным. количеством строк и столбцов. Она возвращает указатель на новое окно или NULL, если создать окно невозможно. Если вы хотите, чтобы правый нижний угол нового окна совпадал с правым нижним углом экрана, можно задать нулевое количество строк и столбцов. Все окна должны располагаться в пределах экрана. Функция newwinзавершится аварийно, если какая-либо часть окна окажется за пределами экрана. Новое окно, созданное newwin, абсолютно независимо от всех уже имеющихся окон. По умолчанию оно помещается поверх существующих окон, скрывая (но не изменяя) их содержимое. Функция delwinудаляет окно, созданное ранее с помощью функции newwin. Поскольку при вызове newwin, по всей вероятности, выделяется память, следует всегда удалять окна, когда в них больше нет нужды. Примечание Когда новое окно создано, как записать в него информацию? У всех уже рассмотренных функций есть универсальные версии, действующие в заданных окнах, и для удобства в них также включено перемещение курсора. Универсальные функцииВы уже применяли функции addchи printwдля вставки символов на экран. К этим функциям, как и ко многим другим, может быть добавлен префикс либо wдля окна, либо mvдля перемещения курсора, либо mvwдля перемещения и окна. Если вы посмотрите заголовочный файл большинства версий библиотеки curses, то увидите, что многие функции, применявшиеся до сих пор, — простые макросы ( #defines), вызывающие эти более универсальные функции. Когда добавляется префикс w, в начало списка аргументов должен быть вставлен указатель типа WINDOW. Когда добавляется префикс mv, в начало списка нужно вставить два дополнительных параметра, координаты y и х. Они задают позицию на экране, в которой выполняется операция, у и х — относительные координаты окна, точка (0, 0) находится в левом верхнем углу окна, а не экрана. Когда добавляется префикс mvw, необходимо передавать в функцию три дополнительных параметра: указатель WINDOWи значения у и х. Как ни странно, указатель WINDOWвсегда в списке предшествует экранным координатам, несмотря на то, что, судя по префиксу, у и х должны быть первыми. Далее для примера приведен полный набор прототипов для семейств функций addchи printw. #include <curses.h> int addch(const chtype char); int waddch(WINDOW *window_pointer, const chtype char); int mvaddch(int y, int x, const chtype char); int mvwaddch(WINDOW *window_pointer, int y, int x, const chtype char); int printw(char *format, ...); int wprintw(WINDOW *window_pointer, char *format, ...); int mvprintw(int y, int x, char *format, ...); int mvwprintw(WINDOW *window_pointer, int y, int x, char *format, ...); У многих других функций, например inch, также есть варианты оконные и с перемещением курсора. Перемещение и обновление окнаСледующие команды позволят вам перемещать и перерисовывать окна: #include <curses.h> int mvwin(WINDOW *window_to move, int new_y, int new x); int wrefresh(WINDOW *window_ptr); int wclear(WINDOW *window_ptr); int werase(WINDOW *window_ptr); int touchwin(WINDOW *window_ptr); int scrollok(WINDOW *window_ptr, bool scroll_flag); int scroll(WINDOW *window_ptr); Функция mvwinперемещает окно по экрану. Поскольку окно целиком должно располагаться в области экрана, функция mvwinзавершится аварийно, если вы попытаетесь переместить окно так, что какая-то его часть выйдет за пределы экрана. Функции wrefresh, wclearи werase— просто обобщения функций, с которыми вы встречались ранее; они только принимают указатель WINDOW, поэтому могут ссылаться на конкретное окно, а не на окно stdscr. Функция touchwinдовольно специальная. Она информирует библиотеку curses о том, что содержимое окна, на которое указывает ее параметр, было изменено. Это означает, что curses всегда будет перерисовывать такое окно при следующем вызове функции wrefresh, даже если вы на самом деле не меняли содержимое этого окна. Эта функция очень полезна для определения отображаемого окна при наличии нескольких перекрывающихся окон, загромождающих экран. Две функции scrollуправляют прокруткой окна. Функция scrollokпри передаче логического значения true (обычно ненулевого) включает прокрутку окна. По умолчанию окна не прокручиваются. Функция scrollпросто прокручивает окно на одну строку вверх. В некоторые реализации библиотеки curses входит и функция wsctl, которая также принимает количество строк для прокрутки, которое может быть и отрицательным числом. Мы вернемся к прокрутке немного позже в этой главе. А теперь выполните упражнение 6.4. Упражнение 6.4. Управление множественными окнамиТеперь, зная, как управлять несколькими окнами, вы можете включить эти новые функции в программу multiw1.c. Для краткости проверка ошибок не приводится. 1. Как обычно, вставьте первыми отсортированные объявления: #include <unistd.h> #include <stdlib.h> #include <curses.h> int main() { WINDOW *new_window_ptr; WINDOW *popup_windov_ptr; int x loop; int y_loop; char a_letter = 'a'; initscr(); 2. Заполните базовое окно символами, обновляя физический экран, когда заполнен логический экран: move(5, 5); printw("%s", "Testing multiple windows"); refresh(); for (y_loop = 0; y_loop < LINES - 1; y_loop++) { for (x_loop = 0; x_loop < COLS - 1; x_loop++) { mvwaddch(stdscr, y_loop, x_loop, a_letter); a_letter++; if (a_letter > 'z') a_letter = 'a'; } } /* Обновление экрана */ refresh(); sleep(2); 3. Теперь создайте окно 10×20 и вставьте в него текст перед прорисовкой окна на экране: new_window_ptr = newwin(10, 20, 5, 5); mvwprintw(new_window_ptr, 2, 2, "%s", "Hello World"); mwwprintw(new_window_ptr, 5, 2, "%s", "Notice how very long lines wrap inside the window"); wrefresh(new_window_ptr); sleep(2); 4. Измените содержимое фонового окна. Когда вы обновите экран, окно, на которое указывает new_window_ptr, будет затемнено: a_letter = '0'; for (y_lоор = 0; y_lоор < LINES - 1; y_lоор++) { for (х_lоор = 0; xloop < COLS - 1; х_lоор++) { mvwaddch(stdscr, y_loop, х_lоор, a_letter); a_letter++; if (a_letter > '9') a_letter = '0'; } } refresh(); sleep(2); 5. Если вы выполните вызов для обновления нового окна, ничего не изменится, поскольку вы не изменяли новое окно: wrefresh(new_window_ptr); sleep(2); 6. Но если вы сначала воспользуетесь функцией touchwinи заставите библиотеку curses думать, что окно было изменено, следующий вызов функции wrefresh снова отобразит новое окно на переднем плане. touchwin(new_window_ptr); wrefresh(new_window_ptr); sleep(2); 7. Добавьте еще одно накладывающееся окно с рамкой вокруг него. popup_window_ptr = newwin(10, 20, 8, 8); box(popup_window_ptr, '|', '-'); mvwprintw(popup_window_ptr, 5, 2, "%s", "Pop Up Window!"); wrefresh(popup_window_ptr); sleep(2); 8. Поиграйте с новыми всплывающими окнами перед их очисткой и удалением. touchwin(new_window_ptr); wrefresh(new_window_ptr); sleep(2); wclear(new_window_ptr); wrefresh(new_window_ptr); sleep(2); delwin(new_window_ptr); touchwin(popup_window_ptr); wrefresh(popup_window_ptr); sleep(2); delwin(popup_window_ptr); touchwin(stdscr); refresh(); sleep(2); endwin(); exit(EXIT_SUCCESS); } К сожалению, нет возможности продемонстрировать выполнение этого фрагмента в книге, но на рис. 6.4 показан снимок экрана после отображения первого всплывающего окна. Рис. 6.4 После того как будет изменен фон и появится новое всплывающее окно, вы увидите экран, показанный на рис. 6.5. Рис. 6.5 Как это работает После обычной инициализации программа заполняет стандартный экран цифрами, чтобы легче было увидеть новые окна, вставляемые на передний план. Далее показано, как можно наложить на фон новое окно с включенным в него текстом, разбитым на строки в соответствии с шириной окна. Далее вы видите, как с помощью функции touchwinзаставить curses перерисовать окно, даже если в нем ничего не менялось. Затем перед закрытием curses и завершением программы вставляется второе окно, перекрывающее первое, чтобы показать, как библиотека curses может управлять перекрывающимися окнами. Как видно из программного кода примера, при обновлении окон следует быть очень внимательным, чтобы они отображались в нужной очередности. Библиотека curses не хранит никаких сведений об иерархии окон, поэтому если вы попросите curses обновить несколько окон, управлять их иерархией придется вам. Примечание Оптимизация обновлений экранаКак вы видели в примере из упражнения 6.4, обновление множественных окон требует некоторой ловкости, но не слишком обременительно. Но может возникнуть более серьезная проблема, если нуждающийся в обновлении терминал подключен через медленное сетевое соединение. К счастью, в наши дни с ней сталкиваются очень редко, но ее обработка настолько легка, что мы рассмотрим ее просто для полноты картины. Задача состоит в минимизации количества символов, прорисовываемых на экране, поскольку при наличии медленных линий связи рисование на экране может оказаться утомительно долгим. Библиотека curses предлагает специальный метод обновления экрана с помощью пары функций wnoutrefreshи doupdate: #include <curses.h> int wnoutrefresh(WINDOW *window_ptr); int doupdate(void); Функция wnoutrefreshопределяет, какие символы необходимо отправить на экран, но не отправляет их на самом деле. Функция doupdateдействительно отправляет изменения на терминал. Если вы просто вызовите wnoutrefresh, а за ней тут же функцию doupdate, эффект будет такой же, как при вызове функции wrefresh. Однако если вы хотите перерисовать ряд окон, то можете вызвать функцию wnoutrefreshдля каждого окна (конечно, в нужном порядке) и затем вызвать doupdateтолько после последнего вызова wnoutrefresh. Это позволит библиотеке curses выполнить расчеты, связанные с обновлением экрана, по очереди для каждого окна и только после этого вывести обновленный экран. Такой подход почти всегда позволяет curses минимизировать количество символов, нуждающихся в пересылке. Вложенные окнаТеперь, когда мы рассмотрели множественные окна, остановимся на специальном случае множественных окон, называемом вложенными окнами или подокнами. Создаются и уничтожаются вложенные окна с помощью следующих вызовов: #include <curses.h> WINDOW *subwin(WINDOW *parent, int num_of_lines, int num_of_cols, int start_y, int start_x); int delwin(WINDOW *window_to_delete); У функции subwinпочти такой же список параметров, как у функции newwin, и удаляются вложенные окна так же, как другие окна с помощью вызова delwin. Для записи во вложенные окна, как и в новые окна, вы можете применять ряд функций mvw. На самом деле большую часть времени вложенные окна ведут себя почти так же, как новые окна, но есть одно важное отличие: подокна самостоятельно не хранят отдельный набор экранных символов; они используют ту же область хранения символов, что и родительское окно, заданное при создании вложенного окна. Это означает, что любые изменения, сделанные во вложенном окне, вносятся и в лежащее в основании родительское окно, поэтому, когда подокно удаляется, экран не меняется. На первый взгляд вложенные окна кажутся бесполезным экспериментом. Почему не изменять просто родительское окно? Основная сфера их применения — предоставление простого способа прокрутки другого окна. Потребность в прокрутке небольшой области экрана удивительно часто возникает при написании программ с использованием curses. Создав вложенное окно и прокручивая его, вы добьетесь желаемого результата. Примечание Выполните упражнение 6.5. Упражнение 6.5. Вложенные окнаТеперь, когда вы познакомились с новыми функциями, этот короткий пример покажет, как они действуют и чем отличаются от функций окна, применявшихся ранее. 1. Начальная секция кода программы subscl.c инициализирует отображение базового окна с некоторым текстом: #include <unistd.h> #include <stdlib.h> #include <curses.h> int main() { WINDOW *sub_window_ptr; int x_loop; int y_loop; int counter; char a_letter = '1'; initscr(); for (y_loop = 0; y_loop < LINES - 1; y_loop++) { for (x_loop = 0; x_loop < COLS - 1; x_loop++) { mvwaddch(stdscr, y_loop, x_loop, a_letter); a_letter++; if (a_letter > '9') a_letter = '1'; } } 2. Теперь создайте новое подокно с прокруткой. Как рекомендовалось, вам следует перед обновлением экрана "коснуться" родительского окна: ub_window_ptr = subwin(stdscr, 10, 20, 10, 10); scrollok(sub_window_ptr, 1); touchwin(stdscr); refresh(); sleep(1); 3. Сотрите содержимое вложенного окна, выведите в нем текст и обновите его. Прокрутка текста обеспечивается циклом: werase(sub_window_ptr); mvwprintw(sub_window_ptr, 2, 0, "%s", "This window will now scroll"); wrefresh(sub_window_ptr); sleep(1); for (counter = 1; counter < 10; counter++) { wprintw(sub_window_ptr, "%s", "This text is both wrapping and \ scrolling."); wrefresh(sub_window_ptr); sleep(1); } 4. Завершив цикл, удалите вложенное окно и обновите основной экран: delwin(sub_window_ptr); touchwin(stdscr); refresh(); sleep(1); endwin(); exit(EXIT_SUCCESS); } К концу программы вы увидите вывод, показанный на рис. 6.6. Рис. 6.6 Как это работает После присвоения указателю sub_window_ptrрезультата вызова subwinвы включаете прокрутку вложенного окна. Даже после удаления вложенного окна и обновления базового окна ( strdcr) текст на экране не меняется, поскольку вложенное окно на самом деле откорректировало символьные данные экрана strdcr. Дополнительная клавиатураВы уже познакомились с некоторыми средствами библиотеки curses для обработки клавиатурного ввода. У многих клавиатур, как минимум, есть клавиши управления курсором и функциональные клавиши. Кроме того, у многих клавиатур есть дополнительная клавиатура и другие клавиши, например, <Insert> и <Home>. Для большинства терминалов расшифровка этих клавиш — серьезная проблема, потому что они посылают строку символов, начинающуюся с escape-символа. Дело не только в том, что приложению трудно отличить одиночное нажатие клавиши <Esc> от строки символов, появившейся в результате нажатия функциональной клавиши, оно еще должно справляться с терминалами разных типов, применяющими разные управляющие последовательности для одних и тех же логических клавиш. К счастью, библиотека curses предоставляет элегантное решение для управления функциональными клавишами. Обычно в структуре terminfoдля каждого терминала хранится последовательность, отправляемая каждой функциональной клавишей, и во включенном в программу файле curses.h для логических клавиш есть набор определений, начинающихся с префикса KEY_. Когда curses стартует, преобразование последовательностей в логические клавиши отключено, и его следует включить вызовом функции keypad. Если вызов успешен, функция вернет OK, в противном случае ERR. #include <curses.h> int keypad(WINDOW *window_ptr, bool keypad_on); Когда режим дополнительной клавиатуры включен с помощью вызова функции keypadс параметром keypad_on, равным true, библиотека curses принимает на себя обработку клавиатурных последовательностей, так что чтение с клавиатуры может вернуть не только нажатую клавишу, но и одно из определений вида KEY_для логических клавиш. Отметьте три незначительных ограничения, налагаемых при использовании режима дополнительной клавиатуры. □ Распознавание escape-последовательностей требует разного времени, и многие сетевые протоколы сгруппируют символы в пакеты (что приведет к неверному распознаванию escape-последовательностей) или разделят их (что приведет к распознаванию последовательностей функциональных клавиш, как клавиши <Esc> и отдельных символов). Такое поведение чаще всего наблюдается в региональных сетях (Wide-Area Network, WAN) и других медленных линиях связи. Единственный выход — попытаться запрограммировать терминалы так, чтобы они отправляли единичные уникальные символы в ответ на нажатие каждой функциональной клавиши, используемой вами, хотя это ограничит количество управляющих символов. □ Для того чтобы библиотека curses могла отличить нажатие клавиши <Esc> от клавиатурной последовательности, начинающейся с символа Esc, ей требуется ожидание в течение короткого промежутка времени. Иногда при включенном режиме дополнительной клавиатуры можно заметить легкую задержку при обработке клавиши <Esc>. □ Библиотека curses не может обрабатывать неуникальные escape-последовательности. Если у вашего терминала есть две разные клавиши, отправляющие одну и ту же последовательность, библиотека просто не будет ее обрабатывать, поскольку не может решить, какую логическую клавишу следует вернуть. Выполните упражнение 6.6. Упражнение 6.6. Применение дополнительной клавиатурыДалее приведена короткая программа keypad.c, демонстрирующая применение режима дополнительной клавиатуры. После запуска программы нажмите клавишу <Esc> и отметьте незначительную задержку, в течение которой программа пытается понять: Esc — это начало управляющей последовательности или просто нажатие одной клавиши, 1. Инициализировав программу и библиотеку curses, включите режим дополнительной клавиатуры: #include <unistd.h> #include <stdlib.h> #include <curses.h> #define LOCAL_ESCAPE_KEY 27 int main() { int key; initscr(); crmode(); keypad(stdscr, TRUE); 2. Отключите отображение символов, чтобы помешать перемещению курсора при нажатии клавиш управления курсором. Экран очищается, и выводится некоторый текст. Программа ждет нажатия клавиши и до тех пор, пока не нажата клавиша <Q> или не возникла ошибка. Символ нажатой клавиши выводится на экран. Если нажатые клавиши соответствуют одной из последовательностей для дополнительной клавиатуры терминала, вместо символа выводится эта последовательность. noecho(); clear(); mvprintw(5, 5, "Key pad demonstration. Press 'q' to quit"); move(7, 5); refresh(); key = getch(); while (key != ERR && key i= 'q') { move(7, 5); clrtoeol(); if ((key >= 'A' && key <= 'Z') || (key >= 'a' && key <= 'z')) { printw("Key was%c", (char)key); } else { switch(key) { case LOCAL_ESCAPE_KEY: printw("%s", "Escape key"); break; case KEY_END: printw("%s", "END key"); break; case KEY_BEG: printw("%s", "BEGINNING key"); break; case KEY_RIGHT: printw("%s", "RIGHT key"); break; case KEY_LEFT: printw("%s", "LEFT key"); break; case KEY_UP: printw("%s", "UP key"); break; case KEY_DOWN: printw("%s", "DOWN key"); break; default: printw("Unmatched — %d", key); break; } /* switch */ } /* else */ refresh(); key = getch(); } /* while */ endwin(); exit(EXIT_SUCCESS); } Как это работает Включив режим дополнительной клавиатуры, вы увидите, как можно распознать различные функциональные клавиши на дополнительной клавиатуре, генерирующие escape-последовательности. Вы, возможно, сумеете заметить, что распознавание клавиши <Esc> немного медленнее, чем других клавиш. Применение цветаВ прошлом очень немногие терминалы ввода/вывода поддерживали цвета, поэтому у большей части самых старых версий библиотеки curses не было поддержки цветов. Цвета появились в библиотеке ncurses и других современных реализациях curses. К сожалению, на "неинтеллектуальный экран", первооснову библиотеки curses, повлиял API, и curses используют цвета очень ограниченным способом, отражающим слабые характеристики старых цветных терминалов. Каждая символьная ячейка на экране может быть записана одним цветом из набора разных цветов на фоне одного цвета из набора различных цветов фона. Например, можно вывести зеленый текст на красном фоне. Цветовая поддержка в библиотеке curses немного необычна, в том смысле, что цвет символа не определяется независимо от цвета фона. Вы должны задать цвет переднего плана и фона как пару, именуемую, что неудивительно, цветовой парой. Прежде чем применять цвета в curses, нужно убедиться в том, что текущий терминал поддерживает цвета, и инициализировать подпрограммы управления цветом библиотеки curses. Для этого примените две функции: has_colorsи start_color. #include <curses.h> bool has_colors(void); int start_color(void); Функция has_colorsвозвращает true, если терминал поддерживает цвета. Далее следует вызвать функцию start_color, которая вернет OK, если цветовая поддержка успешно инициализирована. После вызова start_colorи инициализации цветов переменная COLOR_PAIRSпринимает значение, равное максимальному количеству цветовых пар, которые может поддерживать терминал. Переменная COLORSопределяет максимальное число доступных цветов, которых, как правило, восемь. Внутри компьютера числа от 0 до 63 действуют как уникальные ID для каждого из доступных цветов. Прежде чем применять цвета как атрибуты, вы должны инициализировать цветовые пары, которые хотите использовать. Делается это с помощью функции init_pair. Обратиться к атрибутам, задающим цвет, можно с помощью функции COLOR_PAIR. #include <curses.h> int init_pair(short pair_number, short foreground, short background); int COLOR_PAIR(int pair_number); int pair_content(short pair_number, short *foreground, short *background); В файле curses.h обычно определены некоторые базовые цвета, начинающиеся с префикса COLOR_. Дополнительная функция pair_contentпозволяет извлечь сведения о ранее определенной цветовой паре. Для определения цветовой пары номер 1, как красный на зеленом, примените следующую строку: init_pair(1, COLOR_RED, COLOR_GREEN); Затем вы сможете получить доступ к этой цветовой паре, применив функцию COLOR_PAIRследующим образом: wattron(window_ptr, COLOR_PAIR(1)); Она установит вывод в будущем на экран красных символов на зеленом фоне. Поскольку COLOR_PAIR— это атрибут, вы можете комбинировать его с другими атрибутами. На ПК часто можно добиться на экране цветов повышенной яркости, объединив с помощью поразрядной операции ORатрибут COLOR_PAIRс дополнительным атрибутом A_BOLD: wattron(window_ptr, COLOR_PAIR(1) | A_BOLD); Давайте проверим эти функции в примере color.c (упражнение 6.7). Упражнение 6.7. Цвета1. Сначала проверьте, поддерживает ли цвета терминал, используемый программой. Если да, то инициализируйте отображение цветов: #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <curses.h> int main() { int i; initscr(); if (!has_colors()) { endwin(); fprintf(stderr, "Error — no color support on this terminal\n"); exit(1); } if (start_color() != OK) { endwin(); fprintf(stderr, "Error — could not initialize colors\n"); exit(2); } 2. Теперь можно вывести допустимое количество цветов и цветовые пары. Создайте семь цветовых пар и выведите их по очереди на экран: clear(); mvprintw(5, 5, "There are %d COLORS, and %d COLOR_PAIRS available", COLORS, COLOR_PAIRS); refresh(); init_pair(1, COLOR_RED, COLOR_BLACK); init_pair(2, COLOR_RED, COLOR_GREEN); init_pair(3, COLOR_GREEN, COLOR_RED); init_pair(4, COLOR_YELLOW, COLOR_BLUE); init_pair(5, COLOR_BLACK, COLOR_WHITE); init_pair(6, COLOR_MAGENTA, COLOR_BLUE); init_pair(7, COLOR_CYAN, COLOR_WHITE); for (i = 1; i <= 7; i++) { attroff(A_BOLD); attrset(COLOR_PAIR(i)); mvprintw(5 + i, 5, "Color pair %d", i); attrset(COLOR_PAIR(i) | A_BOLD); mwprintw(5 + i, 25, "Bold color pair %d", i); refresh(); sleep(1); } endwin(); exit(EXIT_SUCCESS); } Выполнение примера приведет к выводу, показанному на рис. 6.7, за вычетом реальных цветов, которые не отображаются на черно-белом снимке экрана. Рис. 6.7 Как это работает После проверки того, что экран поддерживает управление цветами, программа инициализирует цветовую обработку и определяет ряд цветовых пар. Далее на экран выводится текст с использованием цветовых пар для того, чтобы продемонстрировать комбинации разных цветов на экране. Переопределение цветовКак пережиток, оставшийся от старых неинтеллектуальных терминалов, которые могли отображать очень немного цветов в каждый момент времени, но позволяли настраивать текущую цветовую палитру, в библиотеке curses сохранилась возможность переопределения цветов с помощью функции init_color: #include <curses.h> int init_color(short color_number, short red, short green, short blue); Она позволяет переопределить существующий цвет (в диапазоне от 0 до COLORS) новыми значениями яркости цвета из диапазона от 0 до 1000. Такой подход немного напоминает определение цветовых характеристик в графических файлах формата GIF. ПанелиПри написании более сложных программ с использованием curses порой бывает легче построить логический экран и затем позже вывести весь или часть экрана на физический экран. В некоторых случаях лучше иметь логический экран большего размера, чем физический экран и отображать только часть логического экрана в любой конкретный момент времени. Это нелегко сделать с помощью функций библиотеки curses, с которыми вы познакомились к этому моменту, т.к. все окна должны быть не больше физического экрана. Библиотека curses предоставляет специальную структуру данных, панель (pad), для манипулирования данными логического экрана, которые не умещаются в стандартном окне. Структура панели похожа на структуру WINDOW, и все функции библиотеки curses, написанные для работы с окнами, можно применять и к панелям. Но у панелей есть и собственные функции для создания и обновления. Панели создаются во многом так же, как и обычные окна. #include <curses.h> WINDOW *newpad(int number_of_lines, int number_of_columns); Обратите внимание на то, что возвращаемое значение — указатель на структуру типа WINDOW, такое же, как у функции newwin. Удаляются панели, как и окна, функцией delwin. Но к панелям применяются другие подпрограммы обновления. Поскольку панель не привязана к конкретной точке экрана, вы должны задать область панели, которую хотите поместить на экран, и ее положение на экране. Делайте это с помощью функции prefresh. #include <сurses.h> int prefresh(WINDOW *pad_ptr, int pad_row, int pad_column, int screen_row_min, int screen_col_min, int screen_row_max, int screen_соl_max); Функция выполняет запись области панели, начинающейся в точке ( pad_row, pad_column), в область экрана, определенную от ( screen_row_min, screen_col_min) до ( screen_row_max, screen_col_max). Есть и дополнительная подпрограмма pnoutrefresh. Она действует так же, как функция wnoutrefresh, обеспечивая более производительное обновление экрана. Давайте проверим это на практике с помощью программы pad.с (упражнение 6.8). Упражнение 6.8. Применение панели1. В начале этой программы вы инициализируете структуру панели и затем формируете панель с помощью функции, которая возвращает указатель на нее. Вставьте символы, заполняющие структуру панели (панель на 50 символов шире и выше экрана терминала): #include <unistd.h> #include <stdlib.h> #include <curses.h> int main() { WINDOW *pad_ptr; int x, y; int pad_lines; int pad_cols; char disp_char; initscr(); pad_lines = LINES + 50; pad_cols = COLS + 50; pad_ptr = newpad(pad_lines, padcols); disp_char = 'a'; for (x = 0; x < pad_lines; x++) { for (у = 0; у < pad_cols; y++) { mvwaddch(pad_ptr, x, y, disp_char); if (disp_char == 'z') disp_char = 'a'; else disp_char++; } } 2. Теперь перед завершением программы нарисуйте разные области панели в разных местах экрана: prefresh(pad_ptr, 5, 7, 2, 2, 9, 9); sleep(1); prefresh(pad_ptr, LINES + 5, COLS + 7, -5, 5, 21, 19); sleep(1); delwin(pad_ptr); endwin(); exit(EXIT_SUCCESS); } Выполнив эту программу, вы увидите нечто подобное показанному на рис. 6.8. Рис. 6.8 Приложение, управляющее коллекцией компакт-дисковТеперь, когда вы узнали о средствах, которые предлагает библиотека curses, можно разработать типовое приложение. Далее приведена его версия, написанная на языке С и использующая библиотеку curses. К достоинствам приложения относятся более четкое отображение информации на экране и применение окна с прокруткой для просмотра списков. Все приложение занимает восемь страниц, поэтому мы разделили его на секции и отдельные функции внутри секций. Исходный код программы curses_app.c можно получить на Web-сайте издательства Wrox (http://www.wrox.com/WileyCDA/). Как и все программы из этой книги, оно подчиняется требованиям Общедоступной лицензии проекта GNU. Примечание Мы разбили программный код этого приложения на несколько отдельных секций, которые обозначены заголовками последующих разделов. Соглашения, принятые для оформления этого программного кода, немного отличаются от оформления большинства программ в этой книге; здесь выделение цветом применяется только для обозначения вызовов других функций приложения. Начало нового приложения для работы с коллекцией компакт-дисковПервая секция программного кода просто связана с объявлениями переменных и функций, которые вы будете применять позже, и инициализацией некоторых структур данных. 1. Включите в программу все приведенные заголовочные файлы и несколько глобальных переменных: #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <curses.h> #define MAX_STRING 80 /* Самый длинный допустимый ответ */ #define MAX_ENTRY 1024 /* Самый длинный допустимый элемент БД */ #define MESSAGE_LINE 6 /* В этой строке разные сообщения */ #define ERROR LINE 22 /* Строка для вывода ошибок */ #define Q_LINE 20 /* Строка для вопросов */ #define PROMPT_LINE 18 /* Строка для вывода приглашения */ 2. Теперь вам нужны глобальные переменные. Переменная current_cdisприменяется для хранения названия текущего компакт-диска, с которым вы работаете в данный момент. Она инициализируется так, что первый символ равен NULL, чтобы показать, что компакт-диск не выбран. Символ завершения \0, строго говоря, не обязателен, но он гарантирует инициализацию переменной, что само по себе хорошая вещь. Переменная current_catприменяется для записи номера текущего компакт-диска в каталоге: static char current_cd[MAX_STRING] = "\0"; static char current_cat[MAX_STRING]; 3. Теперь объявите имена файлов. Для простоты в этой версии имена файлов фиксированные, как и имя временного файла. Это может вызвать проблемы, если программа выполняется двумя пользователями в одном и том же каталоге. Лучше получать имена файлов базы данных как аргументы программы или из переменных окружения. Нам также потребуется улучшенный метод генерации уникального имени временного файла, для чего мы могли бы использовать функцию tmpnamиз стандарта POSIX. Мы обратимся к решению многих из этих проблем в главе 8, когда применим СУРБД MySQL для хранения данных. const char *title_file = "title.cdb"; const char *tracks_file = "tracks.cdb"; const char *temp_file = "cdb.tmp"; 4. И наконец, прототипы функций: void clear_all_screen(void); void get_return(void); int get_confirm(void); int getchoice(char *greet, char *choices[]); void draw_menu(char *options[], int highlight, int start_row, int start_col); void insert_title(char *cdtitle); void get_string(char *string); void add_record(void); void count_cds(void); void find_cd(void); void list_tracks(void); void remove_tracks(void); void remove_cd(void); void update_cd(void); 5. Прежде чем рассматривать их реализацию, введем некоторые структуры (на самом деле массив пунктов меню) для хранения меню. Когда выбирается пункт меню, возвращается первый символ выбранного пункта. Например, если это пункт меню add new CD (добавить новый CD), при его выборе будет возвращен символ а. Когда компакт-диск выбран, будет отображаться расширенное меню. char *main_menu[] = { "add new CD", "find CD", "count CDs and tracks in the catalog", "quit", 0, }; char *extended_menu[] = { "add new CD", "find CD", "count CDs and tracks in the catalog", "list tracks on current CD"; "remove current CD", "update track information", "quit", 0, }; На этом инициализация закончена. Теперь можно переходить к функциям программы, но сначала необходимо составить общее представление о взаимосвязях всех 16 функций. Функции разделены на три программных секции: □ отображение меню; □ добавление компакт-дисков в базу данных; □ извлечение и отображение данных компакт-диска. Визуальное представление дано на рис. 6.9. Рис. 6.9 Взгляд на функцию mainФункция mainпозволяет выбирать пункты меню, пока не выбран вариант выхода из меню (quit). Далее приведен соответствующий код. int main() { int choice; initscr(); do { choice = getchoice("Options:", current_cd[0] ? extended_menu : main_menu); switch (choice) { case 'q': break; case 'a': add_record(); break; case 'c': count_cds(); break; case 'f': find_cd(); break; case 'l': list_tracks(); break; case 'r': remove_cd(); break; case 'u': update_cd(); break; } } while (choice != 'q'); endwin(); exit(EXIT_SUCCESS); } Теперь давайте подробно рассмотрим функции, связанные с тремя секциями программы. Формирование менюВ этой секции рассматриваются три функции, относящиеся к пользовательскому интерфейсу программы. 1. Функция getchoice, вызываемая из функции main, — это основная функция данной секции. В функцию getchoiceпередается приглашение greetи указатель choicesна базовое или расширенное меню (в зависимости от того, выбран ли компакт-диск). Вы также увидите, как main_menuили extended_menuпередаются как параметры в описанную ранее функцию main. int get_choice(char *greet, char* choises[]) { static int selected_row = 0; int max_row = 0; int start_screenrow = MESSAGE_LINE, start_screencol = 10; char **option; int selected; int key = 0; option = choices; while (*option) { max_row++; option++; } if (selected_row >= max_row) selected_row = 0; clear_all_screen(); mvprintw(start_screenrow - 2, start_screencol, greet); keypad(stdscr, TRUE); cbreak(); noecho(); key = 0; while (key != 'q' && key != KEY_ENTER && key != '\n') { if (key == KEY_UP) { if (selected_row == 0) selected_row = max_row - 1; else selected_row--; } if (key == KEY_DOWN) { if (selected_row == (max_row - 1)) selected_row = 0; else selected_row++; } selected = *choices[selected_row]; draw_menu(choices, selected_row, start_screen_row, start_screencol); key = getch(); } keypad(stdscr, FALSE); nocbreak(); echo(); if (key == 'q') selected = 'q'; return(selected); } 2. Обратите внимание на то, как две локальные функции clear_all_screenи draw_menuвызываются внутри функции getchoice. Первой рассмотрим функцию draw_menu:
int current_row = 0; char **option_ptr; char *txt_ptr; option_ptr = options; while (*option_ptr) { if (current_row == current_highlight) attron(A_STANDOUT); txt_ptr = options[current_row]; txt_ptr++; mvprintw(start_row + current_row, start_col, "%s", txt_ptr); if (current_row == current_highlight) attroff(A_STANDOUT); current_row++; option_ptr++; } mvprintw(start_row + current_row + 3, start_col, "Move highlight then press Return "); refresh(); } 3. Далее рассмотрим функцию clear_all_screen, которая, как ни странно, очищает экран и перезаписывает заголовок. Если компакт-диск выбран, отображаются его данные: void clear all_screen() { clear(); mvprintw(2, 20, "%s", "CD Database Application"); if (current_cd[0]) { mvprintw(ERROR_LINE, 0, "Current CD: %s: %s\n", current_cat, current_cd); } refresh(); } Управление базой данныхВ этом разделе описаны функции пополнения или обновления базы данных компакт-дисков. Функции add_record, update_cdи remove_cdвызываются из функции main. Добавление записей 1. Добавьте сведения о новом компакт-диске в базу данных. void add_record { char catalog_number[MAX_STRING]; char cd_title[MAX_STRING]; char cd_type[MAX_STRING]; char cd_artist[MAX_STRING]; char cd_entry[MAX_STRING]; int screenrow = MESSAGE_LINE; int screencol = 10; clear_all_screen(); mvprintw(screenrow, screencol, "Enter new CD details"); screenrow += 2; mvprintw(screenrow, screencol, "Catalog Number: " ); get_string(catalog_number); screenrow++; mvprintw(screenrow, screencol, " CD Title: "); get_string(cd_title); screenrow++; mvprintw(screenrow, screencol, " CD Type: "); get_string(cd_type); screenrow++; mvprintw(screenrow, screencol, " Artist: "); get_string(cd_artist); screenrow++; mvprintw(PROMPT_LINE-2, 5, "About to add this new entry:"); sprintf(cd_entry, "%s, %s, %s, %s", catalog_number, cd_title, cd_type, cd_artist); mvprintw(PROMPT_LINE, 5, "%s", cd_entry); refresh(); move(PROMPT_LINE, 0); if (get_confirm()) { insert_title(cd_entry); strcpy(current_cd, cd_title); strcpy(current_cat, catalog_number); } } 2. Функция get_stringприглашает к вводу и считывает строку из текущей позиции экрана. Она также удаляет завершающую новую пустую строку: void get_string(char* string) { int len; wgetnstr(stdscr, string, MAX_STRING); len = strlen(string); if (len > 0 && string[len - 1] == '\n') string[len - 1] = '\0'; } 3. Функция get_confirmзапрашивает и считывает пользовательское подтверждение. Она читает введенную пользователем строку и проверяет, первый символ — Yили у. Если она обнаруживает другой символ, то не дает подтверждения. int get_confirm() { int confirmed = 0; char first_char; mvprintw(Q_LINE, 5, "Are you sure? "); clrtoeol(); refresh(); cbreak(); first_char = getch(); if (first_char == 'Y' || first_char == 'y') { confirmed = 1; } nocbreak(); if (!confirmed) { mvprintw(Q_LINE, 1, " Cancelled"); clrtoeol(); refresh(); sleep(1); } return confirmed; } 4. Последней рассмотрим функцию insert_title. Она вставляет в базу данных компакт-дисков заголовок, добавляя строку с заголовком в конец файла заголовков: void insert_title(char* cdtitle) { FILE *fp = fopen(title_file, "a"); if (!fp) { mvprintw(ERROR_LINE, 0, "cannot open CD titles database"); } else { fprintf(fp, "%s\n", cdtitle); fclose(fp); } }Обновление записей 1. Продолжим рассмотрение других управляющих функций, вызываемых из функции main. Следующая из них — функция update_cd. Эта функция использует обведенное рамкой вложенное окно с прокруткой и нуждается в нескольких константах, которые объявляются как глобальные, поскольку они позже потребуются функции list_tracks. #define BOXED_LINES 11 #define BOXED_ROWS 60 #define BOX_LINE_POS 8 #define BOX_ROW_POS 2 2. Функция update_cdпозволяет пользователю заново ввести сведения о дорожках текущего компакт-диска. Удалив предыдущие записи о дорожках, она приглашает ввести новую информацию. void update_cd() { FILE *tracks_fp; char track_name[MAX_STRING]; int len; int track = 1; int screen_line = 1; WINDOW *box_window_ptr; WINDOW *sub_window_ptr; clear_all_screen(); mvprintw(PROMPT_LINE, 0, "Re-entering tracks for CD. "); if (!get_confirm())
move(PROMP_TLINE, 0); clrtoeol(); remove_tracks(); mvprintw(MESSAGE_LINE, 0, "Enter a blank line to finish"); tracks_fp = fopen(tracks_file, "a"); Примечание box_window_ptr = subwin(stdscr, BOXED_LINES + 2, BOXED_ROWS + 2, BOX_LINE_POS - 1, BOX_ROW_POS - 1); if (!box_window_ptr) return; box(box_window_ptr, ACS_VLINE, ACS_HLINE); sub_window_ptr = subwin(stdscr, BOXED_LINES, BOXED_ROWS, BOX_LINE_POS, BOX_ROW_POS); if (!sub_window_ptr) return; scrollok(sub_window_ptr, TRUE); werase(sub_window_ptr); touchwin(stdscr); do { mvwprintw(sub_window_ptr, screen_line++, BOX_ROW_POS + 2, "Track %d: ", track); clrtoeol(); refresh(); wgetnstr(sub_window_ptr, track_name, MAX_STRING); len = strlen(track_name); if (len > 0 && track_name[len - 1] = '\n') track_name[len - 1] = '\0'; if (*track_name) fprintf(tracks_fp, "%s, %d, %s\n", current_cat, track, track_name); track++; if (screen_line > BOXED__LINES - 1) { /* время начать прокрутку */ scroll(sub_window_ptr); screen_line--; } } while (*track_name); delwin(sub_window_ptr); fclose(tracks_fp); }Удаление записей 1. remove_cd— последняя функция, вызываемая из функции main. void remove_cd() { FILE *titles_fp, *temp_fp; char entry[MAX_ENTRY]; int cat_length; if (current_cd[0] == '\0') return; clear_all_screen(); mvprintw(PROMPT_LINE, 0, "About to remove CD %s: %s. ", current_cat, current_cd); if (!get_confirm()) return; cat_length = strlen(current_cat); /* Файл заголовков копируется во временный, игнорируя данный CD */ titles_fp = fopen(title_file, "r"); temp_fp = fopen(temp_flie, "w"); while(fgets(entry, MAX_ENTRY, titles_fp)) { /* Сравнивает номер в каталоге и копирует элемент, если не найдено совпадение */ if (strncmp(current_cat, entry, cat_length) != 0) fputs(entry, temp_fp); } fclose(titles_fp); fclose(temp_fp); /* Удаляет файл заголовков и переименовывает временный файл */ unlink(title_file); rename(temp_file, title_file); /* Теперь делает то же самое для файла дорожек */ remove_tracks(); /* Устанавливает 'None' для текущего CD */ current_cd[0] = '\0'; } 2. Теперь вам только нужен программный код функции remove_tracks, удаляющей дорожки текущего компакт-диска. Она вызывается двумя функциями — update_cdи remove_cd. void remove_tracks() { FILE *tracks_fp, *temp_fp; char entry[MAX_ENTRY]; int cat_length; if (current_cd[0] == '\0') return; cat_length = strlen(current_cat); tracks_fp = fopen(tracks_file, "r"); if (tracks_fp == (FILE *)NULL) return; temp_fp = fopen(temp_file, "w"); while (fgets(entry, MAX_ENTRY, tracks_fp)) { /* Сравнивает номер в каталоге и копирует элемент, если не найдено совпадение */ if (strncmp(current_cat, entry, cat_length) != 0) fputs(entry, temp_fp); } fclose(tracks_fp); fclose(temp_fp); /* Удаляет файл дорожек и переименовывает временный файл */ unlink(tracks_file); rename(temp_file, tracks_file); } Запросы к базе данных компакт-дисковТеперь рассмотрим функции для доступа к данным, которые для упрощения доступа хранятся в паре простых файлов как поля, разделенные запятыми. 1. Страстным коллекционерам важно знать, каким богатством они обладают или сколько собрано. Следующая функция делает это превосходно; она просматривает базу данных, подсчитывая заголовки и дорожки. void count_cds() { FILE *titles_fp, *tracks_fp; char entry[MAX_ENTRY]; int titles = 0; int tracks = 0; titles_fp = fopen(title_file, "r"); if (titles_fp) { while (fgets(entry, MAX_ENTRY, titles_fp)) titles++; fclose(titles_fp); } tracks_fp = fopen(tracks_file, "r"); if (tracks_fp) { while (fgets(entry, MAX_ENTRY, tracks_fp)) tracks++; fclose(tracks_fp); } mvprintw(ERROR_LINE, 0, "Database contains %d titles, with a total of %d tracks.", titles, tracks); get_return(); } 2. Вы потеряли аннотацию к вашему любимому компакт-диску? Не волнуйтесь! Если вы аккуратно ввели подробную информацию в базу данных, теперь можно найти перечень дорожек с помощью функции find_cd. Она предлагает ввести подстроку, совпадение с которой нужно искать в базе данных, и устанавливает в глобальную переменную current_cdзаголовок найденного компакт-диска. void find_cd() { char match[MAX_STRING], entry[MAX_ENTRY]; FILE *titles_fp; int count = 0; char *found, *title, *catalog; mvprintw(Q_LINE, 0, "Enter a string to search for in CD titles: "); get_string(match); titles_fp = fopen(title_file, "r"); if (titles_fp) { while (fgets(entry, MAX_ENTRY, titles_fp)) { /* Пропускает прежний номер в каталоге */ catalog = entry; if (found == strstr(catalog, ", ")) { *found = '\0'; title = found + 1; /* Стирает следующую запятую в элементе, укорачивая его только до заголовка */ if (found == strstr(title, ", ")) { *found = '\0'; /* Теперь проверяет, есть ли совпадающая строка */ if (found == strstr(title, match)) { count++; strcpy(current_cd, title); strcpy(current_cat, catalog); } } } } fclose(titles_fp); } if (count != 1) { if (count == 0) { mvprintw(ERROR_LINE, 0, "Sorry, no matching CD found. "); } if (count > 1) { mvprintw(ERROR_LINE, 0, "Sorry, match is ambiguous: CDs found. ", count); } current_cd[0] = '\0'; get_return(); } } Хотя переменная catalogуказывает на массив, больший чем current_cat, и могла бы переписать память, проверка в функции fgetsпрепятствует этому. 3. Вам также нужно иметь возможность перечислить на экране дорожки выбранного компакт-диска. Для вложенных окон можно использовать директивы #define, применявшиеся в функции update_cdв предыдущем разделе. void list_tracks() { FILE *tracks_fp; char entry[MAX_ENTRY]; int cat_length; int lines_op = 0; WINDOW *track_pad_ptr; int tracks = 0; int key; int first_line = 0; if (current_cd[0] == '\0') { mvprintw(ERROR_LINE, 0, "You must select a CD first. "); get_return(); return; } clear_all_screen(); cat_length = strlen(current_cat); /* Сначала считает количество дорожек у текущего CD */ tracks_fp = fopen(tracks_file, "r"); if (!tracks_fp) return; while (fgets(entry, MAX_ENTRY, tracks_fp)) { if (strncmp(current_cat, entry, cat_length) == 0) tracks++; } fclose(tracks_fp); /* Создает новую панель, гарантируя, что даже при наличии одной дорожки панель достаточна большая, поэтому последующий вызов prefresh() всегда будет допустим. */ track_pad_ptr = newpad(tracks + 1 + ВОХЕD_LINES, BOXED_ROWS + 1); if (!track_pad_ptr) return; tracks_fp = fopen(tracks_file, "r"); if (!tracks_fp) return; mvprintw(4, 0, "CD Track Listing\n"); /* Записывает сведения о дорожке на панель */ while (fgets(entry, MAX_ENTRY, tracks_fp)) { /* Сравнивает номер каталога и оставшийся вывод элемента */ if (strncmp(current_cat, entry, cat_length) == 0) { mvwprintw(track_pad_ptr, lines_op++, 0, "%s", entry + cat_length + 1); } } fclose(tracks_fp); if (lines_op > BOXED_LINES) { mvprintw(MESSAGE_LINE, 0, "Cursor keys to scroll, RETURN or q to exit"); } else { mvprintw(MESSAGE_LINE, 0, "RETURN or q to exit"); } wrefresh(stdscr); keypad(stdscr, TRUE); cbreak(); noecho(); key = 0; while (key != "q" && key != KEY_ENTER && key != '\n') { if (key == KEY_UP) { if (first_line > 0) first_line--; } if (key == KEY_DOWN) { if (first_line + BOXED_LINES + 1 < tracks) first_line++; } /* Теперь рисует соответствующую часть панели на экране */ prefresh(track_pad_ptr, first_line, 0, BOX_LINE_POS, BOX_ROW_POS, BOX_LINE_POS + BOXED_LINES, BOX_ROW_POS + BOXED_ROWS); key = getch(); } delwin(track_pad_ptr); keypad(stdsсr, FALSE); nocbreak(); echo(); } 4. В последних двух функциях вызывается функция get_return, которая приглашает к вводу и считывает символ возврата каретки, игнорируя другие символы. void get_return() { int ch; mvprintw(23, 0, "is", " Press return "); refresh(); while ((ch = getchar()) != '\n' && ch != EOF); } Если вы выполните эту программу, то увидите на экране нечто похожее на рис. 6.10. Рис. 6.10 РезюмеВ этой главе вы изучили библиотеку curses. Она предлагает текстовым программам удобный способ управления экраном и считывания данных с клавиатуры. Хотя библиотека curses не обеспечивает такого уровня управления, как общий терминальный интерфейс (GTI) и прямой доступ к структуре terminfo, ею гораздо легче пользоваться. Если вы пишете полноэкранное текстовое приложение, стоит рассмотреть возможность применения в нем библиотеки curses для управления экраном и чтения данных с клавиатуры. |
|
||
Главная | В избранное | Наш E-MAIL | Прислать материал | Нашёл ошибку | Наверх |
||||
|