Programujeme v C s ncurses
-=-=-=-=-=-=-=-=-=-=-=-=-=-=
Karel Žák, 25. října 1998
Tento článek by měl být malou rychlokuchařkou do světa ncurses.
Snad jen na začátek, malé vysvětlení co to ncurses jsou.
Název "ncurses" je složeninou ze slov "new curses". Jedná se
o volně distibuovatelný klon System V Release 4.0 (SVr4) curses.
A jak říkají FAQ, je to slovní hříčka k "cursor optimization".
Ale jinak (a vážně) jedná se o knihovnu k managamentu výstupu
na "character-cell terminals" tedy na znakově orientované terminály.
Ale rychle k programování. Tato knihovna není žádným drobečkem
(při pohledu do adresáře /lib je - alespoň na mém disku - po libc
tou největší) takže snad někteří prominou pokud se o některých
možnostech nezmíním. Pokusím se zaměřit pouze na to jak nenásilně
a rychle dosáhnout "počmárání screenu ve stylu ncurses" :-)
Inicializace
Jako první by si měl program nainicializovat obrazovku do curses
módu. To se provede funkcí initscr(). Po provedení této funkce,
by programátor nikdy neměl zapomenout, že po ukončení programu
by vše mělo být jako před jeho spuštěním. Návrat z curses módu se
provede funkcí endwin(). Pokud si nejste jisti, že jste tuto funkci
zavolali tak se o tom můžete přesvědčit pomocí isendwin(). Protože
nikdy není tak úplně jisté kdy a kde program skončí, tak např. já to
dělám tak, že si do atexit() přidám endwin(), tedy
atexit((void *) endwin)
Pokud se divíte, proč taková přemíra starostí o endwin(), tak
proto, že program, který se ukončí bez zavolání této funkce nechá
váš terminál v tom lepším stavu. Funkce initscr() vrací pointer
stdscr typu WINDOW (jedná se o globální proměnnou, ke které ncurses
vztahuje funkce, které nepoužívají okno jako parametr.
Po inicializaci je možné si doopravit některé další vlastnosti
screenu / terminálu. Například:
curs_set() - nastavení kursoru
echo() / noecho() - mají se zobrazovat znaky přicházející
z klávesnice (např. při volání funkce mvwgetstr())
cbreak() - nastaven "line bufferingu"
keypad() - nastavení používání kláves F1 až Fn
U poslední zmíněné funkci snad ještě poznámka: pokud používáte
ve svém programu WINDOW, tak je nutné pro každé nové okno tuto
funkci volat, jinak při nepoužívání oken je možné zavolat na počátku
keypad(stdscr, TRUE)
Po přepnutí do curses módu jsou dostupné dvě globální proměnné
(int) LINES a COLS, ve kterých je uložena aktuální velikost
obrazovky. Časté je např. pořadí inicializace: initscr(), cbreak(),
noecho() - pak už je obrazovka připravena ve stavu "neřádkování",
ale naopak ve stavu "kam přesně co chceš" (na jakou řádku
a do jakého sloupce).
Někdy je nutné se z ncurses programu přepnout nazpět
do původního (v terminologii ncurses - shell) módu. To je nutné
například při volání nějakého externího programu, který používá
terminál standardním způsobem. K tomu se používá skupina funkcí
okolo reset_shell_mode() (více manuál).
Pochopitelně, ze ncurses obsahují ještě další přepínače, ale
to už by mohlo být otázkou samostudia nad manuály :-) Podobně jako
záležitosti týkající se možnosti otevřít si vlastní screen pomocí
funkce newterm().
Výstup a okna
Už jsem se zmínil o WINDOW. Nové okno si vytvoříte pomocí funkce
newwin(). Jako parametry se zadávají poloha okna a to vzhledem
k levému hornímu rohu a velikost okna. Po vytvoření okna je možné
s ním zacházet jako se samostatnou podmnožinou a tedy volat pak
funkce s parametry řádek a sloupců vzhledem k levému hornímu roku
okna. Funkce newwin() vrací pointer na nové okno (funkce sama
alokuje pomocí malloc strukturu WINDOW) proto se okno deklaruje jako
pointer (podobně jako FILE). Naopak uvolnění paměti a zrušení okna
se provede funkcí delwin(). Smazání ze screenu se provede funkcí
werase() (a pak je vhodné wrefresh()). V okně může existovat i tzv.
subwin, jedná se to samé jako WINDOW, ale vzhledem k rodičovskému
oknu (narozdíl od okna, které je definováno vzhledem ke screenu).
S okny lze vytvářet docela zajímavé věci. Například zapsat
do souboru a pak naopak načíst okno ze souboru. Často je využívána
i například funkce redrawwin(), která znova nakreslí okno na screen.
Je možné s okny pohybovat pomocí mvwin(), duplikovat okna - dupwin()
atd.
Funkce zacházející s okny lze jednoduše poznat dle prefixu mvw-
nebo samotného w-. Takže obdoba klasického printf() je v ncurses při
použití oken
mvwprintw(okno, řádka, sloupec, "něco: %s", řetězec);
(pozor na znak \n - v ncurses se dostáváme na další řádku tím,
že zvýšíme "řádku" o jedna).
#include <curses.h> /* stdio.h neni nutne je v curses.h */
#include <ctype.h>
#define COLOR2 1 /* barvicka 1. */
#define COLOR1 2 /* barvicka 2. */
int main () {
int c;
char *s;
initscr ();
cbreak ();
noecho (); /* vypnuti echa */
start_color (); /* chceme barvy */
keypad (stdscr, TRUE); /* chceme klavesy pod makry KEY_neco */
curs_set(0); /* at tam ta mrska neblika */
if (!has_colors ()) { /* umi terminal barvy ? */
endwin ();
fputs ("Hmm.. tady barvy nejdou !", stderr);
exit (1);
}
/* barva - popredi - pozadi */
init_pair (COLOR1, COLOR_RED, COLOR_BLUE); /* barvicka 1. */
init_pair (COLOR2, COLOR_YELLOW, COLOR_BLACK); /* a druha */
attron (COLOR_PAIR( COLOR1 )); /* pouzivat barvu 1. */
mvaddstr (2, 5, "Cervene na modrem";);
attron (COLOR_PAIR( COLOR2 ));
mvaddstr (3, 5, "Zlute na cernem");
attron (A_BOLD); /* od ted vse BOLD */
mvaddstr (4, 5, "Zlute na cernem a tucne");
attroff (COLOR_PAIR( COLOR2 )); /* vypne barvu */
mvhline(LINES-2, 0, ACS_HLINE, COLS); /* nakresli caru */
mvaddstr (LINES-1, COLS-15, "F10 - konec");
mvaddstr (10, 5, "Jmeno klavesy");
while ( (c=getch()) != KEY_F(10)) {
s = (char *) keyname(c); /* jmeno klavesy ? */
mvhline(10, 20, ' ', COLS); /* smaz */
if (s)
mvprintw (10, 20, "'%s'", s);
else
mvprintw (10, 20, "'%c'", (isprint(c) ? c : '.'));
}
erase (); /* smaz nase vytvory */
refresh ();
endwin (); /* konec curses */
exit (0); /* ....bye */
}
Další možností jak tisknout na screen je nevyužívat WINDOW
a využívat funkcí, které nepožadují WINDOW jako svůj parametr. Lines
a cols v parametrech těchto funkcí se pak vztahují k levému hornímu
rohu screenu. Tyto funkce pak většinou začínají prefixem mv- např.
mvaddstr(y,x, str).
Ncurses ještě umožňují používat funkce bez parametrů line
a cols. Takové funkce pak vypisují svůj výstup na aktuální pozici
kursoru. Nutno říct, že tento způsob je asi nejméně programátorsky
příjemný. Takovouto funkcí je např. addstr(str). Jak je z uvedených
příkladů zřejmé, tak u většiny funkcí existují všechny tři
alternativy, tedy např. mvwaddstr, mvaddstr i addstr.
V ncurses jsou pak ještě alternativy těchto funkcí umožňující
přesné zacházení se řetězci (podobně jako známé stringové strcpy
a strncpy), tedy např. addstr() a addnstr().
Pokud potřebujete tisknout některé speciální znaky tak
doporučuji podívat se na man addch, kde jsou popsána makra
ASC_něco obsahující tyto znaky (např. čáry, různé šipky apod.).
Ke snadnějšímu kreslení čar existují i funkce hline() (horizontální)
a vline() (vertikální) a jejich alternativy pro okna. Často se
u oken používají rámečky. Rámeček můžete nakreslit pomocí výše
zmíněných funkcí a nebo pomocí, k těmto účelům předdefinované,
funkce wborder().
Pro přesnost snad ještě poznámku - většina funkcí které
nepožadují WINDOW jsou ve skutečnosti makra typu:
#define mvaddstr(y, x, str) mvwaddstr(stdscr, y, x, str)
Pro dobré zacházení s výstupem ještě doporučuji prohlédnut si
manuály k funkcím refresh(), wrefresh(), redrawwin() atd. Co tyto
dělají je myslím patrné už z jejich názvu.
Zaznělo zde již i něco o pozici kursoru, i tu lze pochopitelně
měnit. Pomocí wmove(y, x, win) v rámci okna a pomocí move() vzhledem
ke screenu. Další a již zmíněnou možností je zapínaní resp. vypínaní
cursoru a to pomocí curs_set(0) a curs_set(1).
Možná se ptáte, co je lepší - používat v programu okna nebo ne?
Abych upřímně odpověděl - tak nevím. Používání oken s sebou
nese několik výhod. Části výstupu jsou umístěny v podmnožinách,
se kterými lze samostatně zacházet. Například pokud jedním
oknem překryjete druhé, pak snadno to překryté obnovíte pomocí
redrawwin(), zatímco pokud okna nepoužíváte tak tím, co na screen
napíšete nenávratně přepíše to, co tam bylo (na dané pozici).
Ale při nepoužívání oken ve vašem programu ubude volání malloc (při
každém newwin), nebudete muset programem vláčet struktury WINDOW
a pravděpodobně program bude rychlejší. Na zamyšlení snad uvedu, že
např. v Midnight Commanderu okna používána nejsou, já sám jsem (malá
reklama :-)) např. v programu kim (manažer procesů) okna použil, ale
v následující verzi už nebudou. To vše, ale neznamená, že okna jsou
něco nedobrého, jen je někdy dobré, pokud se program stará o některé
věci sám, a i v programování platí, že méně je někdy více.
Za výstup snad lze ještě považovat to, co dělá funkce beep().
Jak už je z názvu patrno, jedná se o zvukové znamení.
Vstup
Aby mohl program "vnímat" uživatele tak pochopitelně knihovna
obsahuje funkce vracející znaky nebo řetězce zadané z klávesnice.
Základní je getch() a při použití oken wgetch(). Pozor, nezapomínat
při používání oken volat po vytvoření okna funkci keypad(). Těmito
funkcemi vrácená hodnota (int) obsahuje znak nebo v případě stisku
nějaké speciální (funkční) klávesy v curses.h nadefinované makro
mající prefix KEY_, např. při stisku klávesy "šipka nahoru" je
vráceno KEY_UP.
Klávesu je možné i vrátit (podobně jako u souboru znak)
pomocí ungetch(). Pochopitelně jsou definovány i funkce vracející
znak nejen do programu, ale opisující ho také na screen nebo okno
(pozor - nesmí být vypnuto echo pomocí noecho()). Např. mvgetch(),
mwvgetch(). Také je možné pomocí has_key() zjistit, zná-li terminál
některou klávesu (ta je jako parametr této funkce) nebo například
jméno klávesy a to pomocí keyname().
Pro přijetí celého řetězce lze využít funkce z rodiny getstr().
Tyto načítají všechny zadané znaky až do stisku klávesy Enter
do řetězce. Existují pochopitelně i bezpečnější varianty těchto
funkcí, tedy getnstr().
Zajímavostí může být nastavení timeoutů pro čtení klávesy. Toho
lze efektivně vyžít například k tomu, že během toho co program čeká
na to až se uživatel rozhodne něco stisknout tak může např. testovat
nějakou událost. Já to například používám pokud chci provést nějakou
složitější a náročnější reakci na signál, kdy není vhodné, aby tuto
činnost vykonával handler signálu. Nastavení timeoutu se provede
funkcí timeout() nebo wtimeout() s parametrem v milisekundách. Pokud
do nastavené doby nedošlo k stisknutí nějaké klávesy vrací např.
funkce getch() hodnotu ERR.
Další možností jak načítat "něco" do vaší aplikace je možnost
číst ze screenu nebo okna znak nebo řetězec ležící na zadaných
souřadnicích a to pomocí rodiny funkcí inch a instr (mvinch(),
mvwinch(), mvinstr()). U funkcí inch lze nejen zjistit znak na dané
pozici, ale i jeho barvu a atributy.
Barvy
Chcete-li používat ve svém programu barvy musí být splněno
několik základních podmínek. Prvně je nutné po inicializaci
screenu zavolat funkci start_color(), kterou sdělíte ncurses,
že máte takovýto úmysl. Podporuje-li terminál barvy zjistíte funkcí
has_colors().
Jak bude vypadat výstup je možné nastavit pomocí attron() nebo
pro okno watrron() a naopak zrušit pomocí attroff() (wattroff()).
Kde jako parametr je uvádí požadovaný stav, tedy např. A_BOLD,
A_BLINK, A_NORMAL atd. Od chvíle nastavení pomocí attron všechen
výstup používá toto nastavení a to až do další změny pomocí attron
nebo do jeho zrušení pomocí attroff.
Před používáním barev je nutné je nadefinovat. To se provede
pomocí init_pair(barva, popředí, pozadí). Kde barva je číslo
a popředí (písmo) a pozadí jsou jedna z nadefinovaných barev
(COLOR_RED, COLOR_BLUE, COLOR_YELLOW, COLOR_GREEN, COLOR_CYAN,
COLOR_BLACK, COLOR_WHITE a COLOR_MAGENTA).
Chceme-li pak použít takto nadefinovaných barev provedeme
to zase pomocí attron(). A to takto: attron(COLOR_PAIR(barva));.
Pochopitelně lze kombinovat i s bold. Použití bude asi nejlépe
viditelné na příkladu a na jeho výstupu.
Ncurses pochopitelně dovolují i další operace s barvami, ale to
je nad rámec tohoto článku a lze to poměrně pohodlně nastudovat
z manuálu.
Resize
Je to také trochu nad rámec tohoto článku, ale protože je
to častým dotazem na různých konferencích, tak to zde zmíním.
Pokud provozujete ncurses program pod xtermem, tak se běžně stává,
že uživatel mění velikost okna xtermu. Je tedy vhodné zjistit
novou velikost LINES a COLS. O změně velikosti okna je aplikace
informována signálem SIGWINCH. Občerstvit LINES a COLS můžete např.
takto:
struct winsize size;
size.ws_row = size.ws_col = 0;
ioctl(0, TIOCGWINSZ, &size);
if (size.ws_row && size.ws_col) {
LINES = size.ws_row;
COLS = size.ws_col;
}
Myška
Při důkladném pročítání manuálu ncurses zjistíte, že ncurses
obsahují funkce getmouse() atd. Tyto funkce, ale nejsou v původním
SVr4. Já osobně jsem těchto funkcí nikdy nepoužil. Stalo se zvykem
používat gpm a jsem toho názoru, že některé zvyky jsou dobré, tak
proč je měnit. Knihovna gpm dokonce na programy s ncurses pamatuje,
takže jsou zde vytvořené alternativy getch() a wgetch() (Gpm_Getch()
a Gpm_Wgetch()) pro používání myši. Každopádně gpm s ncurses pracuje
bezvadně. Ale o gpm až někdy příště.
Závěr
Protože některým programátorům nevyhovovaly pouze základní
možnosti ncurses, tak vznikla řada nástaveb. Standardně jsou
s ncurses distribuované menu, panel, form. Je otázkou jak moc
jsou tyto různá "udělátka" šikovná a hlavně útlá a jestli náhodou
programátora nesvazují a konečnému produktu nevtiskují tvář jakou
chtějí oni.
K přiloženým příkladům: kompilace programu s knihovnou se
provede např. takto:
gcc -Wall -O3 program.c -o program -lncurses
(to -Wall doporučuji, často ukáže programátorům věci "netušené"
:-) Ti dloubavější si možná v místech, kde se zmiňuji o mc vzpomněli
na knihovnu slang. Ano, v současné době se tento program distribuuje
kompilovaný s touto knihovnou (s ncurses to ale jde kompilovat
také). Programy psané pro ncurses, lze po malých úpravách kompilovat
i s knihovnou slang. Je jen nutné místo curses.h includovat
slcurses.h.
Jako další studijní materiál doporučuji obsáhlý man curses
a ještě (a to nejlépe) zdrojáky nějakého ncurses programu.
Přiložený příklad lze najít včetně Makefile
v adresáři s Linuxovými novinami nebo na adrese
http://home.zf.jcu.cz/zakkr/LN/.
výheň