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
int mvwprintw(WINDOW *okno, int y, int x, char *format, ...);- wyświetlenie tekstu w podanym oknie i w podanym położeniuint wrefresh(WINDOW *okno);- odświeżenie obrazu oknaint beep(void);- wydanie dźwięku przy pomocy głośnika systemowegoint flash(void);- błyśnięcie ekranu, na niektórych terminalach jest to zastępowanebeep();int wclear(WINDOW *okno);- wyczyszczenie oknaint wclrtobot(WINDOW *okno);- wyczyszczenie ekranu od kursora do początku linii w oknieint wclrtoeol(WINDOW *okno);- wyczyszczenie ekranu od kursora do końca linii w oknieint wmove(WINDOW *okno, int y, int x);- przesunięcie kursora w oknie do zadanej pozycjiint wattron(WINDOW *okno, chtype atrybut);- ustawienie atrybutu dla oknaint watrroff(WINDOW *okno, chtype atrybut);- wyłączenie atrybutu dla okna (inne ustawione atrybuty pozostają bez zmian)int wattrset(WINDOW *okno, chtype atrybut);- ustawienie atrybutu dla okna (można łączyć atrybuty przy pomocy|)int wgetch(WINDOW *okno);- pobranie znaku z klawiaturyint wgetstr(WINDOW *okno, char *ciąg);- pobranie ciągu znaków z klawiatury i zapisanie go do tablicyciągint wscanw(WINDOW *okno, char *format,...);- odpowiednik funkcjiscanf();z biblioteki standardowejvoid getyx(WINDOW *okno, int y, int x);- makro pobierające współrzędne kursora i zapisujące je do odpowiednich zmiennych.
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.
