Ncurses - nowa klątwa

W Linuksie istnieje wiele narzędzi ułatwiających programowanie. Można by rzec, że Linux jest wręcz do programowania stworzony. Masa kompilatorów różnych języków, masa rozszerzeń, bibliotek itp. Mimo tak wielkiego wyboru wiele osób decyduje się na języki C i C++. Dla tych dwóch właśnie dostępnych jest najwięcej bibliotek, edytorów itd. Właściwie w kompilatorach C/C++ nie ma zbyt wielkiego wyboru, a spowodowane jest to tym, że gcc bije swoich konkurentów (nie tylko na uniksowych platformach) na głowę. Ale może skupmy się na bibliotekach. W poniższym artykule postaram się przedstawić mało znaną szarym użytkownikom bibliotekę ncurses. Ncurses jest to linuksowa implementacja biblioteki curses (ang. przekleństwo, klątwa). Zawiera dużo usprawnień oraz ułatwień w porównaniu do wzorca. Ale czym jest to ncurses? A widzieliście kiedyś Midnight Commandera czy też vima? Oba te programy oparte są na ncurses. Na ncurses oparty jest także program dialog wykorzystywany między innymi podczas konfiguracji jądra za pomocą polecenia make menuconfig. Po prostu ncurses jest biblioteką pozwalającą na tworzenie graficznych "okienek" na tekstowym terminalu. Funkcje, które są dostępne w ncurses można również zrealizować przy pomocy ogólnego interfejsu terminala (termios). Jednakże posiłkowanie się termiosem wymaga pisania wielu, wielu linii niskopoziomowego kodu. Ncurses sprawia, że jest to łatwiejsze, szybsze i co najważniejsze wciągające ;-). Podstawowe sprawy zostały wyjaśnione. Czas na poznanie struktury programu korzystającego z ncurses. Do napisania programów będzie potrzebny jakikolwiek edytor tekstu, najlepiej z kolorowaniem kodu (polecam vima), kompilator gcc oraz zainstalowana biblioteka ncurses razem z nagłówkami. Do dzieła.

#include <ncurses/ncurses.h>
int main() {
	initscr();
	move(10, 10);
	printw("Witaj świecie!");
	refresh();
	endwin();
	exit(0);
}

Prawda, że prosty? Nadeszła pora na wyjaśnienia. Jak wiadomo pierwsza linijka oznajmia preprocesorowi, że ma dołączyć plik nagłówkowy ncurses. Zazwyczaj jest on w katalogu /usr/include/ncurses/ przez co w tej linii warto zamieścić do niego względną ścieżkę względem /usr/include. Możemy nie męczyć się z dopisywaniem katalogu jeżeli w katalogu /usr/include są utworzone dowiązania symboliczne do każdego pliku z katalogu ncurses. Przy kompilacji należy również plik obiektowy zlinkować z biblioteką ncurses. Ogólnie całość polecenia kompilacji powinna wyglądać mniej więcej tak: gcc -o program program.c -lncurses. Bez zlinkowania kompilator będzie zgłaszał mnóstwo błędów. OK, wracając do analizy kodu. Druga linijka, to oczywiście początek funkcji głównej (ciała programu). Trzecia linijka jest bardzo ważna. Bez niej każdy program zawierający w sobie funkcje ncurses będzie "wyrzucał" segmentation fault. Funkcja initscr(); inicjalizuje i przygotowuje ekran do pracy w trybie curses. Tworzy ona na ekranie okno główne (stdscr). Przed wykonaniem się tej funkcji program pracuje w zwykłym trybie tekstowym i może wykonywać zwykłe funkcje typu printf(); czy fgets();. Kolejna funkcja - move(); powoduje przesunięcie kursora do 11 wiersza, 11 kolumny (wiersze i kolumny liczone są od zera i lewego górnego rogu). Kiedy kursor jest na zamierzonym miejscu. Można teraz wyświetlić coś na ekranie. Służy do tego funkcja printw();. Jest ona analogiczna do funkcji printf();. Przyjmuje takie same parametry i jej działanie niczym się nie różni (oczywiście pomijając to, że jedna działa w "normalnej" konsoli, a druga w "okienkach"). Funkcja refresh(); powoduje odświeżenie ekranu. Bez tej funkcji cały program na nic by się zdał, ponieważ nic nie pojawiłoby się na ekranie. Jej użycie jest konieczne, jeżeli chcemy żeby cokolwiek pojawiło się na ekranie. Kolejna funkcja, jak wynika z nazwy, kończy byt okienka. Wszelkie dane w pamięci, których używały funkcje ncurses są uwalniane. Pozostaje tylko to co jest na ekranie. Exit(0); oczywiście powoduje zakończenie programu z zerowym kodem końcowym. Program wykonał się poprawnie. W ncurses istnieje wiele odpowiedników funkcji dostępnych w bibliotece standardowej. Odpowiednikiem printf(); jest printw();, odpowiednikiem scanf(); - scanw(); itd. Jednak nie wszystkie nazwy funkcji są tworzone według tego szablonu. Na przykład odpowiednikiem getc(); jest getch();. Ale może o tym później.

Okna

Jak już wiadomo, programy w ncurses pracują na strukturach zwanych oknami. Zawsze dostępne są okno główne - stdscr oraz okno, w którym jest kursor - curscr. Można tworzyć nowe okna. Wykonuje się to za pomocą funkcji newwin();. Wcześniej trzeba utworzyć wskaźnik do struktury WINDOW.

WINDOW *okno;
...
okno = newwin(w, x, y, z);

Wszystkie parametry są liczbami całkowitymi (int). Oznaczają odpowiednio: liczbę wierszy nowego okna, liczbę kolumn tegoż okna, wiersz ekranu, w którym zostanie umieszczony lewy górny róg nowego okna oraz kolumnę, w której to okno się utworzy. Usuwanie okna? Nic prostszego: delwin(okno);. Niestety, funkcje typu move();, printw(); czy też refresh(); działają tylko na oknie głównym. A to dlatego, że są one makrami, które mają zdefiniowane okno główne jako swoje pole pracy. Dużo łatwiej korzystać z uogólnionych wersji tych funkcji: wmove(WINDOW *okno);, wprintw(WINDOW *okno); czy wrefresh(WINDOW *okno);. W każdej z tych funkcji pierwszym (lub jedynym) parametrem jest nazwa okna.

Atrybuty

Przed przejściem do omawiania kolorów należy wspomnieć o atrybutach tekstu. Każdemu znakowi przeznaczonemu do wyświetlenia można nadać pewne atrybuty. Nie każdy terminal będzie je obsługiwał w sposób przewidziany przez autorów ncurses, ale wtedy będzie je zamieniał na inne - podobne. Dostępne są atrybuty: A_BLINK, A_BOLD, A_DIM, A_REVERSE, A_STANDOUT, A_UNDERLINE (oraz wiele innych, zależnych od implementacji) . Włącza się je za pomocą funkcji wattron(okno, atrybut);, a wyłącza przy pomocy wattroff(okno, atrybut);. Można to robić pojedynczo dla każdego atrybuty albo można skorzystać z funkcji wattrset(okno, atrybut | atrybut | atrybut);. Ta funkcja pozwala zmienić kilka ustawić kilka atrybutów za jednym razem przy pomocy "orowania" ich. Oczywiście tych funkcji można używać w dowolnym miejscu w programie.

Kolory

Jaki ma sens tworzenie programu konsolowego opartego na okienkach, jeżeli nie jest on kolorowy? Właściwie żaden. Kolory bardzo dobrze nadają się do odróżnienia od siebie bloków interfejsu i oczywiście sprawiają, że dużo przyjemniej pracuje się z programem. Biblioteka ncurses umożliwia używanie kolorów. Aby móc z tej możliwości skorzystać należy zainicjalizować ich obsługę. Służy do tego funkcja start_color();. Nie przyjmuje ona żadnych parametrów, ponieważ odnosi się do całego ekranu, a nie do pojedynczego okna. Jednakże dla pewności czy terminal, na którym działa program ma możliwość wyświetlania kolorów powinniśmy jeszcze wcześniej użyć funkcji has_colors();, która zwraca wartość logiczną TRUE albo FALSE zależnie od możliwości terminala. Kolory definiuje się w pary. Para składa się z koloru tekstu i koloru tła. Funkcja definiująca pary ma postać: init_pair(numer, pierwszy_kolor, drugi_kolor);. Dostępne są kolory: COLOR_BLACK, COLOR_WHITE, COLOR_RED, COLOR_GREEN, COLOR_BLUE, COLOR_CYAN, COLOR_MAGENTA, COLOR_YELLOW (więcej kolorów zdefiniowanych jest w pliku nagłówkowym ncurses.h). A więc wywołanie: init_pair(1, COLOR_BLACK, COLOR_BLUE); zainicjalizuje parę kolorów dostępną pod numerem pierszym, gdzie tło będzie niebieskie, a tekst czarny. Jak łatwo wyliczyć do zdefiniowania możliwe są 64 pary kolorów podstawowych. Ale jak ustawić kolor dla jakiegoś tekstu? Nie bez powodu wcześniej wspomniałem o atrybutach. Mianowicie para kolorów jest atrybutem, więc można ją ustawić przy pomocy wattrset(COLOR_PAIR(1));. COLOR_PAIR(numer_pary) jest makrem. Można to wywnioskować chociażby z tego, że jego nazwa jest pisana wielkimi literami. Ponieważ pary kolorów są atrybutami można je mieszać również ze standardowymi atrybutami typu A_BOLD czy A_BLINK.
Przykład:

#include <ncurses/ncurses.h>

#include <unistd.h>
int main() {
	int i;
	initscr();
	if (has_colors) {
		start_color();
		init_pair(1, COLOR_BLUE, COLOR_RED);
		attrset(COLOR_PAIR(1));
		for(i = 0;  i <= (COLS*LINES); i++) /* COLS - liczba kolumn
ekranu, LINES - liczba wierszy */
			printw("*");
		refresh();
		sleep(10);
	}
	endwin();
}

Ten program na dziesięć sekund zamaluje ekran niebieskimi gwiazdkami na czerwonym tle. Uwaga! Nie patrzcie na ten obraz zbyt długo! To naprawdę wciąga :-)

Tryb Keypad

Tryb Keypad jest pomocny, gdy program powinien posiadać obsługę klawiszy niealfanumerycznych tj. strzałek kursorów, klawiszy funkcyjnych, klawiszy Home, End, Insert, Delete, PgUp, PgDown oraz Escape. Klawisze specjalne wysyłane są do terminala w postaci sekwencji znaków zaczynającej się od Escape. Kiedy tryb Keypad jest włączony program ma dostęp do tego typu znaków. Większość tych znaków jest opisana łatwiejszymi do zapamiętania nazwami (normalnie są one wartościami liczbowymi). I tak KEY_UP, KEY_DOWN, KEY_LEFT, KEY_RIGHT to oczywiście strzałki kursorów, KEY_END i KEY_HOME to odpowiednio End i Home. Wszsytkie te nazwy można znaleźć studiując plik nagłówkowy ncurses.h.

Tryb Keypad podobnie jak praca w kolorze wymaga inicjalizacji. W przeciwieństwie jednak do kolorów działa on tylko we wskazanych oknach. I tak keypad(stdscr, TRUE); włączy tryb Keypad dla okna głównego, a keypad(stdscr, FALSE); go wyłączy. Muszę jeszcze wspomnieć o czterech ważnych funkcjach. Mianowicie są to cbreak(); nocbreak(); echo(); noecho();. W normalnych warunkach do programu za pomocą klawiatury dane można wprowadzać liniami tzn. program otrzyma dane wejściowe dopiero po naciśnięciu Enter (Return). Wywołanie cbreak(); powoduje, że program otrzymuje każdy znak wprowadzony bezpośrednio, bez konieczności potwierdzania Enterem. Dzięki temu może na bieżąco reagować na każdy nowootrzymany znak. Funkcja nocbreak(); ma działanie odwrotne. Funkcja noecho(); powoduje, że wciskane klawisze nie będą się pojawiały na ekranie po jego odświeżeniu. W wielu sytuacjach jest to przydatne, a w niektórych wręcz pożądane. W jakich? Przekonacie się sami. Oczywiście echo(); jest funkcją odwrotną. Przydatnymi funkcjami podczas pracy z trybem Keypad mogą się okazać clrtoeol(); i clrtobot();. Pierwsza czyści ekran od miejsca położenia kursora do końca linii, a druga od kursora do początku linii.

Przykład użycia trybu Keypad:

#include <ncurses/ncurses.h>

#include <unistd.h>
#define START_LINIA 2
#define MAX 7 
#define INFOKOLUMNA 15
#define INFOWIERSZ START_LINIA+MAX+3
#define STRZALA "<<---"
#define ENTER 0xa /*w ncurses można również użyć KEY_ENTER*/
int main() {
	int klawisz, wybor=1;
	initscr();
	cbreak();
	keypad(stdscr, TRUE);
	noecho();
	start_color();
	init_pair(1, COLOR_RED, COLOR_BLACK);
	init_pair(2, COLOR_BLUE, COLOR_BLACK);
	clear();
	move(1, 1);
	printw("MENU");
	attrset(COLOR_PAIR(2));
	mvprintw(START_LINIA+1, 3, "Wybor 1");
	mvprintw(START_LINIA+2, 3, "Wybor 2");
	mvprintw(START_LINIA+3, 3, "Wybor 3");
	mvprintw(START_LINIA+4, 3, "Wybor 4");
	mvprintw(START_LINIA+5, 3, "Wybor 5");
	mvprintw(START_LINIA+6, 3, "Wybor 6");
	mvprintw(START_LINIA+7, 3, "Wybor 7");
	refresh();
	attroff(COLOR_PAIR(2));
	attrset(COLOR_PAIR(1) | A_BOLD);
	mvprintw((START_LINIA+1), 15, STRZALA);
	klawisz = getch();
#ifdef BEEP
	beep();
#endif
	while ((klawisz != 'q') && (klawisz != ERR) ) {
		move(INFOWIERSZ, INFOKOLUMNA);
		clrtoeol();
		mvprintw(INFOWIERSZ,INFOKOLUMNA, "Klawisz %d -- 0%o -- 0x%x -- %c", klawisz, klawisz, klawisz, klawisz);
		switch (klawisz) {
			case KEY_UP:
				if (wybor == 1) {
					klawisz = getch();
#ifdef BEEP
					beep();
#endif
					continue;
				}
				--wybor;
				move((START_LINIA+wybor+1), 15);
				clrtoeol();
				mvprintw((START_LINIA+wybor), 15, STRZALA);
				break;
			case KEY_DOWN:
				if (wybor == MAX) {
					klawisz = getch();
#ifdef BEEP
					beep();
#endif
					continue;
				}
				++wybor;
				move((START_LINIA+wybor-1), 15);
				clrtoeol();
				mvprintw((START_LINIA+wybor), 15, STRZALA);
				break;
			case KEY_HOME:
				if (wybor == 1) {
					klawisz = getch();
#ifdef BEEP
					beep();
#endif
					continue;
				}
				move((START_LINIA+wybor), 15);
				clrtoeol();
				wybor = 1;
				mvprintw((START_LINIA+1), 15, STRZALA);
				break;
			case KEY_END:
				if (wybor == MAX) {
					klawisz = getch();
#ifdef BEEP
					beep();
#endif
					continue;
				}
				move((START_LINIA+wybor), 15);
				clrtoeol();
				wybor = MAX;
				mvprintw((START_LINIA+MAX), 15, STRZALA);
				break;
			case ENTER:
				move(LINES-1, 1);
				clrtoeol();
				mvprintw(LINES-1, 1, "Zatwierdzono: %d", wybor);
				refresh();
				break;
			default: 
				klawisz = getch(); 
#ifdef BEEP
				beep();
#endif
				continue;
				break;
		}
		refresh();
		usleep(50000);
		klawisz = getch();
#ifdef BEEP
		beep();
#endif
	}
	endwin();
	exit(0);
}

Ten przykładowy program wyświetli proste menu z możliwością wyboru. Przy zdefiniowaniu makra BEEP (przy pomocy parametru kompilatora gcc -DBEEP), program będzie dodatkowo wydawał dźwięki przy pomocy systemowego głośniczka.

Krótki spis najważniejszych i najciekawszych funkcji biblioteki ncurses

Spis wszystkich funkcji biblioteki jest zawarty w pliku nagłówkowym ncurses.h. Naprawdę gorąco polecam przejrzenie jego zawartości. Mam nadzieję, że choć trochę zainteresowałem Was biblioteką ncurses.

Tekst został napisany dla magazynu komputerowego @t i ukazał się w numerze 32.