Программирование с ncurses: первые шаги
Программирование
Статья была опубликована 1 марта 2013 года в 12:00, а последний раз правилась 9 апреля 2015 года в 11:49.
Постоянная ссылка: http://www.nixp.ru/articles/102.html
Создание консольных утилит с использованием популярной свободной библиотеки создания текстовых интерфейсов ncurses: подключение библиотеки, основные функции, вывод текста, ввод и события мыши. Представлены примеры кода с комментариями.
Примечание: эта статья была впервые опубликована в электронном издании «Open Source» от журнала «Системный администратор» и размещена на nixp.ru по согласованию с редакцией.
Иногда нужно написать какую-нибудь программу, не прибегая к тяжеловесным библиотекам графических тулкитов вроде Qt или GTK, а возможностей обычного консольного вывода (printf, cout) всё же маловато. Как быть? На помощь приходит ncurses.
Предыстория
В незапамятные времена, в Беркли, существовала библиотека под названием curses. Создал её известный UNIX-гуру Кен Арнолд. По своим функциям curses напоминала модуль CRT для Pascal, а основным её назначением было взаимодействие с экраном (на уровне терминала) и клавиатурой. Со временем у неё появилось развитие под названием pcurses, а потом (в 1993 году) — ncurses (new curses), которая надолго поселилась под сенью проекта GNU (http://www.gnu.org/software/ncurses/ncurses.html). Ncurses используется довольно большим количеством программ — среди них, например, файловый менеджер Midnight Commander и веб-браузер lynx. Писать программу под ncurses выгодно: эта библиотека найдется в любом дистрибутиве *nix, и будет входить в эти дистрибутивы по крайней мере еще долгое время. В общем, пишем под ncurses — пишем на века!
Подключение ncurses
Подключить ncurses к своей программе на Си или С++ очень просто. Достаточно использовать в исходнике директиву #include <ncurses.h>, а для компиляции указать -lncurses для линковки:
$ g++ main.cpp -lncurses
Конечно, надо, чтобы были установлены заголовочные файлы ncurses. В Debian и Ubuntu пакет с ними называется ncurses-dev:
$ sudo apt-get install ncurses-dev
Для сборки программ, где более чем один файл, удобно использовать какую-нибудь систему сборки вроде autotools или Scons. В случае последней (я предпочитаю её), в каталоге с исходником программы нужно создать файл под названием SConstruct такого содержания:
import glob import os
env = Environment() SOURCES = glob.glob('*.cpp')
INST_PREFIX = '/usr/local/' INST_DIR_BIN = INST_PREFIX + 'bin' INST_PREFIX_DATA = INST_PREFIX + 'share'
env.Append(CCFLAGS = ['-g', '-Wall']) env.Append(LIBS = ['ncurses', 'other-libs']) mixtestbin = env.Program(target = 'mytest', source = SOURCES) env.MergeFlags('-DNIX=1')
env.Install(dir = INST_DIR_BIN, source = mytestbin) env.Alias('install', [INST_DIR_BIN])
Теперь при запуске в том же каталоге команды scons будут скомпилированы все .cpp-файлы и слинкованы с библиотеками ncurses и любыми другими (в примере — «other-libs»). Для сборки и установки программы надо выполнить с правами root:
# scons install
Начало работы
Для инициализации ncurses чаще всего вызываются три функции:
1. initscr(); — инициализирует работу с консолью. Эта функция выполняет довольно много работы, поскольку существует множество различных терминалов, а ncurses перед началом работы с каким-либо из них надо определить тип терминала и «адаптироваться» под него. Вызов initscr возвращает указатель WINDOW *, который потом можно использовать в других функциях. К последним относятся getyx() для получения координат курсора, putwin() для сохранения окна в файл, getwin() для чтения окна из файла, copywin() для копирования содержимого одного окна в другое… Кроме того, initscr() очищает окно.
2. keypad (stdscr, TRUE); — если так вызвать keypad(), вы сможете «отлавливать» нажатия клавиш ряда F и курсорных. stdscr — стандартный, определенный в ncurses указатель на окно «по умолчанию». Если не вызвать keypad с параметром TRUE, то функции вроде getch() вместо кода нажатой клавиши будут возвращать escape-последовательности.
3. noecho(); — если вызвана, то последующие нажатия клавиш не будут отображаться на экране автоматически. Это нормальный режим, если вы хотите сами обрабатывать нажатия на клавиши (например, реализуете собственное меню и тому подобное).
Завершить работу с библиотекой ncurses следует вызовом функции endwin(), которая освободит все распределенные библиотекой ресурсы и вернет терминал к состоянию до инициализации библиотеки вашей программой.
Для считывания нажатий на клавиши в общих целях используется функция getch(), а для считывания из конкретного окна — wgetch(). Цикл выглядит примерно так:
int ch = 0; //считываем клавиши, пока не будет нажата q while (ch != 'q') { //обновляем экран //ожидаем нажатия на клавишу и //получаем её код в ch ch = getch(); switch (ch) { case 48 ... 57: ; //нажата клавиша от 0 до 9 break;
case KEY_UP: ; //нажата курсорная "вверх" break; } }
В этом примере рассмотрены все три типа определения нажатой клавиши: непосредственно по символу, по коду и по именованной константе. Если функция noecho() выключена, то нажатые клавиши будут отображаться на экране, иначе — не будут.
Вывод текста
Для вывода текста в консоль чаще всего используют функцию printw(), которая принимает те же аргументы, что и printf(), то есть вы можете форматировать текст как угодно. Для строк C++ (std:string) в качестве строковых параметров надо передавать их через функцию c_str(), то есть:
std::string test; [...] printw ("%s \n", test.c_str());
При выводе строки без завершающего символа «\n» курсор остается в текущей строке, на последнем выведенном символе, так что следующий вызов printw() в этом случае дополнит текущую строку. Функция addch() служит для вывода одного символа, переданного в параметре. Сразу можно задать ему атрибуты начертания (символ комбинируется с атрибутами с помощью логического OR):
addch ('a' | A_BOLD | A_UNDERLINE);
Вывести символ по заданным координатам можно так:
move (row, col); //смещаем курсор в ряд row, колонку col addch (ch); //выводим символ ch
Или так:
mvaddch (row, col, ch);
Атрибуты текста — это цвет и начертание. Они актуальны и для printw(). Как можно изменить атрибуты?
Сначала нужно включить «цветовой движок» ncurses, вызывая (обычно после initscr()) функцию:
start_color();
Внутри неё создается палитра из 8 основных цветов (COLOR_BLACK, COLOR_RED, COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE, COLOR_MAGENTA, COLOR_CYAN, COLOR_WHITE), а также инициализируются глобальные переменные COLORS и COLOR_PAIRS. Допустим, стоит задача вывода некоего меню, списка, где обычные элементы будут отрисованы обычным цветом и начертанием, а текущий элемент — жирным и другими цветами.
Для этого надо создать две так называемые «цветовые пары». Первой будет присвоен красный цвет букв и черный для фона, а второй — белый цвет букв и черный для фона:
init_pair (1, COLOR_RED, COLOR_BLACK); init_pair (2, COLOR_WHITE, COLOR_BLACK);
Теперь допустим, что есть вектор со строковыми элементами списка. Тогда нужно вывести в цикле эти элементы, устанавливая им нужные атрибуты. Разберу пример на небольшой рабочей программе (test.cpp):
#include <iostream> #include <vector> #include <string>
#include <ncurses.h>
using namespace std;
vector <string> list; //наш вектор с элементами int current_element; //номер текущего элемента
//функция обновления экрана, //будет вызываться при каждом нажатии клавиш
void update_screen() { erase(); //чистим экран
//выводим все элементы вектора for (size_t i = 0; i < list.size(); i++) { //если счетчик равен текущему элементу if (i == current_element) { //включаем атрибут "жирный" attron(A_BOLD); //выбираем цветовую пару номер 1 attron (COLOR_PAIR(1)); //выводим строковое представление элемента списка printw ("%s\n", list[i].c_str()); //выключаем выбранные атрибуты attroff (COLOR_PAIR(1)); attroff(A_BOLD); } else //иначе выводим обычным цветом { attron (COLOR_PAIR(2)); printw ("%s\n", list[i].c_str()); attroff (COLOR_PAIR(2)); } } }
//главная функция int main (int argc, char *argv[]) { //всё инициализируем initscr(); start_color(); keypad (stdscr, TRUE); noecho();
//создаем цветовые пары init_pair (1, COLOR_RED, COLOR_BLACK); init_pair (2, COLOR_WHITE, COLOR_BLACK); //заполняем вектор тремя элементами list.push_back ("one"); list.push_back ("two"); list.push_back ("three"); //текущий элемент сбрасываем в ноль current_element = 0;
//объявляем переменную для хранения нажатой клавиши int ch = 0; //считываем клавиши, пока не будет нажата 'q' while (ch != 'q') { //обновляем экран update_screen(); //ожидаем нажатия на клавишу и //получаем её код в ch ch = getch(); switch (ch) {
//если нажата "вверх", то уменьшаем current_element case KEY_UP: if (current_element > 0) current_element--; break;
//если нажата "вниз", то увеличиваем current_element case KEY_DOWN: if (current_element < list.size()-1) current_element++; break;
//обработка нажатия Enter case '\n': ;//пользователь выбрал элемент //list[current_element] break;
} }
endwin(); return 0; }
Некоторые могут поинтересоваться, почему нажатие клавиши Enter отлавливается через '\n’, а не KEY_ENTER. Это очень тяжелый вопрос. Если при инициализации вызывать функцию nonl(), то можно отлавливать Enter вот так:
case 13:
Если не вызывать nonl(), то вот так:
case 10:
А константа KEY_ENTER действует и вовсе странным образом… В общем, проверка на соответствие '\n’ — самое верное средство для UNIX-систем.
Ввод и события мыши
Для ввода строковых значений в ncurses предусмотрены функции scanw(), getstr() и другие. Например, чтобы получить строку в массив str, можно поступить так:
char str[80]; getstr (str); //ожидать, пока пользователь наберет и нажмет Enter printw ("Вы набрали слово %s", str); //вывести набранное слово
Для обработки событий мыши сначала нужно провести инициализацию:
mousemask (ALL_MOUSE_EVENTS, NULL);
После этого в обработчике нажатий на клавиши можно поступить так:
case KEY_MOUSE: if (getmouse(&event) == OK) if (event.bstate & BUTTON1_PRESSED) printw ("Нажата левая клавиша по координатам %d,%d\n", event.x, event.y );
break;
Вспомогательные библиотеки
Таковы основы программирования при использовании библиотеки ncurses. Существует также ряд вспомогательных библиотек для создания распространенных элементов интерфейса на основе ncurses: menus (для меню), panels (для панелей или перекрываемых окон), forms (для полей ввода). Подключаются они в исходниках соответственно:
#include <menu.h> #include <panel.h> #include <form.h>
А линкуются так:
$ gcc test.cpp -lmenu -lpanel -lform -lncurses
Используйте их, если вашей программе нужно нечто большее, чем простое считывание нажатий на клавиши и позиционный вывод на экран.
Пролог
Консоль никогда не сдаст своих позиций. «Графические» тулкиты непрерывно развиваются, шагают дальше, но консольные программы вроде Midnight Commander занимают свою нишу и всегда будут востребованы определенными категориями пользователей.
-
Популярные в этом разделе:
- «Программирование с ncurses: первые шаги»,
- «Программирование на GTK2 в среде GNOME (Anjuta, Glade и LibGlade)»,
Последние комментарии
- OlegL, 17 декабря в 15:00 → Перекличка 21
- REDkiy, 8 июня 2023 года в 9:09 → Как «замокать» файл для юниттеста в Python? 2
- fhunter, 29 ноября 2022 года в 2:09 → Проблема с NO_PUBKEY: как получить GPG-ключ и добавить его в базу apt? 6
- Иванн, 9 апреля 2022 года в 8:31 → Ассоциация РАСПО провела первое учредительное собрание 1
- Kiri11.ADV1, 7 марта 2021 года в 12:01 → Логи catalina.out в TomCat 9 в формате JSON 1